feat: allow for prompt to skip authorize screen

This commit is contained in:
Stavros
2026-06-19 13:32:46 +03:00
parent 80bb4f1bc8
commit 32e899e77e
5 changed files with 60 additions and 19 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ type ScreenParams = {
oidc_ticket?: string; oidc_ticket?: string;
oidc_scope?: string; oidc_scope?: string;
oidc_name?: string; oidc_name?: string;
oidc_login?: boolean; oidc_prompt?: "none" | "login";
}; };
const zodScreenParams = z.object({ const zodScreenParams = z.object({
@@ -15,7 +15,7 @@ const zodScreenParams = z.object({
oidc_ticket: z.string().optional(), oidc_ticket: z.string().optional(),
oidc_scope: z.string().optional(), oidc_scope: z.string().optional(),
oidc_name: z.string().optional(), oidc_name: z.string().optional(),
oidc_login: z.stringbool().optional(), oidc_prompt: z.enum(["none", "login"]).optional(),
}); });
export function useScreenParams(params: URLSearchParams): ScreenParams { export function useScreenParams(params: URLSearchParams): ScreenParams {
+21 -5
View File
@@ -25,6 +25,7 @@ import {
recompileScreenParams, recompileScreenParams,
useScreenParams, useScreenParams,
} from "@/lib/hooks/screen-params"; } from "@/lib/hooks/screen-params";
import { useEffect } from "react";
type Scope = { type Scope = {
id: string; id: string;
@@ -90,7 +91,15 @@ export const AuthorizePage = () => {
const isOidc = screenParams.login_for === "oidc"; const isOidc = screenParams.login_for === "oidc";
const compiledParams = recompileScreenParams(screenParams); const compiledParams = recompileScreenParams(screenParams);
const authorizeMutation = useMutation({ // TODO: maybe a better way to do this
const shouldAutoAuthorize =
auth.authenticated &&
isOidc &&
screenParams.oidc_ticket !== undefined &&
screenParams.oidc_scope !== undefined &&
screenParams.oidc_prompt === "none";
const { mutate: authorizeMutate, isPending: authorizePending } = useMutation({
mutationFn: () => { mutationFn: () => {
return axios.post("/api/oidc/authorize-complete", { return axios.post("/api/oidc/authorize-complete", {
ticket: screenParams.oidc_ticket, ticket: screenParams.oidc_ticket,
@@ -110,6 +119,12 @@ export const AuthorizePage = () => {
}, },
}); });
useEffect(() => {
if (shouldAutoAuthorize) {
authorizeMutate();
}
}, [shouldAutoAuthorize, authorizeMutate]);
if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) { if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) {
return ( return (
<Navigate <Navigate
@@ -119,7 +134,7 @@ export const AuthorizePage = () => {
); );
} }
if (!auth.authenticated || screenParams.oidc_login) { if (!auth.authenticated || screenParams.oidc_prompt === "login") {
return <Navigate to={`/login${compiledParams}`} replace />; return <Navigate to={`/login${compiledParams}`} replace />;
} }
@@ -168,14 +183,15 @@ export const AuthorizePage = () => {
)} )}
<CardFooter className="flex flex-col items-stretch gap-3"> <CardFooter className="flex flex-col items-stretch gap-3">
<Button <Button
onClick={() => authorizeMutation.mutate()} onClick={() => authorizeMutate()}
loading={authorizeMutation.isPending} loading={authorizePending}
disabled={shouldAutoAuthorize}
> >
{t("authorizeTitle")} {t("authorizeTitle")}
</Button> </Button>
<Button <Button
onClick={() => navigate(`/logout${compiledParams}`)} onClick={() => navigate(`/logout${compiledParams}`)}
disabled={authorizeMutation.isPending} disabled={authorizePending || shouldAutoAuthorize}
variant="outline" variant="outline"
> >
{t("cancelTitle")} {t("cancelTitle")}
+2 -2
View File
@@ -65,7 +65,7 @@ export const LoginPage = () => {
const screenParams = useScreenParams(searchParams); const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams({ const compiledParams = recompileScreenParams({
...screenParams, ...screenParams,
oidc_login: false, oidc_prompt: undefined,
}); });
const loginForUrl = useLoginFor({ const loginForUrl = useLoginFor({
login_for: screenParams.login_for, login_for: screenParams.login_for,
@@ -199,7 +199,7 @@ export const LoginPage = () => {
}; };
}, [redirectTimer, redirectButtonTimer]); }, [redirectTimer, redirectButtonTimer]);
if (auth.authenticated && !screenParams.oidc_login) { if (auth.authenticated && screenParams.oidc_prompt !== "login") {
return <Navigate to={loginForUrl} replace />; return <Navigate to={loginForUrl} replace />;
} }
+9 -10
View File
@@ -69,11 +69,11 @@ type ClientCredentials struct {
} }
type AuthorizeScreenParams struct { type AuthorizeScreenParams struct {
LoginFor FrontendLoginFor `url:"login_for"` LoginFor FrontendLoginFor `url:"login_for"`
OIDCTicket string `url:"oidc_ticket"` OIDCTicket string `url:"oidc_ticket"`
OIDCScope string `url:"oidc_scope"` OIDCScope string `url:"oidc_scope"`
OIDCName string `url:"oidc_name"` OIDCName string `url:"oidc_name"`
OIDCLogin bool `url:"oidc_login"` OIDCPrompt service.OIDCPrompt `url:"oidc_prompt,omitempty"`
} }
type AuthorizeCompleteRequest struct { type AuthorizeCompleteRequest struct {
@@ -168,6 +168,8 @@ func (controller *OIDCController) authorize(c *gin.Context) {
return return
} }
prompt := controller.oidc.GetPrompt(req.Prompt)
userContext, err := new(model.UserContext).NewFromGin(c) userContext, err := new(model.UserContext).NewFromGin(c)
if err != nil { if err != nil {
@@ -176,7 +178,7 @@ func (controller *OIDCController) authorize(c *gin.Context) {
} }
} }
if (err != nil || !userContext.Authenticated) && req.Prompt == "none" { if (err != nil || !userContext.Authenticated) && prompt == service.OIDCPromptNone {
controller.authorizeError(c, authorizeErrorParams{ controller.authorizeError(c, authorizeErrorParams{
err: errors.New("user not logged in"), err: errors.New("user not logged in"),
reason: "User not logged in", reason: "User not logged in",
@@ -195,10 +197,7 @@ func (controller *OIDCController) authorize(c *gin.Context) {
OIDCTicket: ticket, OIDCTicket: ticket,
OIDCScope: req.Scope, OIDCScope: req.Scope,
OIDCName: client.Name, OIDCName: client.Name,
} OIDCPrompt: prompt,
if req.Prompt == "login" {
values.OIDCLogin = true
} }
queries, err := query.Values(values) queries, err := query.Values(values)
+26
View File
@@ -44,6 +44,15 @@ var (
ErrInvalidClient = errors.New("invalid_client") ErrInvalidClient = errors.New("invalid_client")
) )
type OIDCPrompt string
const (
OIDCPromptLogin OIDCPrompt = "login"
OIDCPromptNone OIDCPrompt = "none"
)
var SupportedPrompts = []string{string(OIDCPromptLogin), string(OIDCPromptNone)}
// This is not spec-compliant, the ID token SHOULD NOT contain user info claims but, // This is not spec-compliant, the ID token SHOULD NOT contain user info claims but,
// it has became a "standard" and apps are looking for the claims in the ID tokens // it has became a "standard" and apps are looking for the claims in the ID tokens
// instead of calling the userinfo endpoint, so we include them in the ID token as well // instead of calling the userinfo endpoint, so we include them in the ID token as well
@@ -937,3 +946,20 @@ func (service *OIDCService) DecodeAuthorizeJWT(tokenString string) (*AuthorizeRe
Prompt: get("prompt"), Prompt: get("prompt"),
}, nil }, nil
} }
// Return the first prompt in the list of prompts, or an empty string if no prompt is specified
func (service *OIDCService) GetPrompt(prompt string) OIDCPrompt {
if prompt == "" {
return ""
}
prompts := strings.Split(prompt, " ")
for _, p := range prompts {
if slices.Contains(SupportedPrompts, p) {
return OIDCPrompt(p)
}
}
return ""
}