diff --git a/frontend/src/lib/hooks/oidc.ts b/frontend/src/lib/hooks/oidc.ts index 6d9ca85..3090eae 100644 --- a/frontend/src/lib/hooks/oidc.ts +++ b/frontend/src/lib/hooks/oidc.ts @@ -1,64 +1,41 @@ -export type OIDCValues = { - scope: string; - response_type: string; - client_id: string; - redirect_uri: string; - state: string; - nonce: string; - code_challenge: string; - code_challenge_method: string; -}; +import { z } from "zod"; -interface IuseOIDCParams { - values: OIDCValues; - compiled: string; +export const oidcParamsSchema = z.object({ + scope: z.string(), + response_type: z.string(), + client_id: z.string(), + redirect_uri: z.string(), + state: z.string().optional(), + nonce: z.string().optional(), + code_challenge: z.string().optional(), + code_challenge_method: z.string().optional(), + prompt: z.string().optional(), +}); + +export const useOIDCParams = ( + params: URLSearchParams, +): { + values: z.infer; + issues: string[]; isOidc: boolean; - missingParams: string[]; -} + compiled: string; +} => { + const obj = Object.fromEntries(params.entries()); + const parsed = oidcParamsSchema.safeParse(obj); -const optionalParams: string[] = [ - "state", - "nonce", - "code_challenge", - "code_challenge_method", -]; - -export function useOIDCParams(params: URLSearchParams): IuseOIDCParams { - let compiled: string = ""; - let isOidc = false; - const missingParams: string[] = []; - - const values: OIDCValues = { - scope: params.get("scope") ?? "", - response_type: params.get("response_type") ?? "", - client_id: params.get("client_id") ?? "", - redirect_uri: params.get("redirect_uri") ?? "", - state: params.get("state") ?? "", - nonce: params.get("nonce") ?? "", - code_challenge: params.get("code_challenge") ?? "", - code_challenge_method: params.get("code_challenge_method") ?? "", - }; - - for (const key of Object.keys(values)) { - if (!values[key as keyof OIDCValues]) { - if (!optionalParams.includes(key)) { - missingParams.push(key); - } - } - } - - if (missingParams.length === 0) { - isOidc = true; - } - - if (isOidc) { - compiled = new URLSearchParams(values).toString(); + if (parsed.success) { + return { + values: parsed.data, + issues: [], + isOidc: true, + compiled: new URLSearchParams(parsed.data).toString(), + }; } return { - values, - compiled, - isOidc, - missingParams, + issues: parsed.error.issues.map((issue) => issue.path.toString()), + values: {} as z.infer, + isOidc: false, + compiled: "", }; -} +}; diff --git a/frontend/src/pages/authorize-page.tsx b/frontend/src/pages/authorize-page.tsx index 2809f92..6edc8a5 100644 --- a/frontend/src/pages/authorize-page.tsx +++ b/frontend/src/pages/authorize-page.tsx @@ -72,36 +72,27 @@ export const AuthorizePage = () => { const scopeMap = createScopeMap(t); const searchParams = new URLSearchParams(search); - const { - values: props, - missingParams, - isOidc, - compiled: compiledOIDCParams, - } = useOIDCParams(searchParams); - const scopes = props.scope ? props.scope.split(" ").filter(Boolean) : []; + const oidcParams = useOIDCParams(searchParams); const getClientInfo = useQuery({ - queryKey: ["client", props.client_id], + queryKey: ["client", oidcParams.values.client_id], queryFn: async () => { - const res = await fetch(`/api/oidc/clients/${props.client_id}`); + const res = await fetch( + `/api/oidc/clients/${oidcParams.values.client_id}`, + ); const data = await getOidcClientInfoSchema.parseAsync(await res.json()); return data; }, - enabled: isOidc, + enabled: oidcParams.isOidc, }); const authorizeMutation = useMutation({ mutationFn: () => { return axios.post("/api/oidc/authorize", { - scope: props.scope, - response_type: props.response_type, - client_id: props.client_id, - redirect_uri: props.redirect_uri, - state: props.state, - nonce: props.nonce, + ...oidcParams.values, }); }, - mutationKey: ["authorize", props.client_id], + mutationKey: ["authorize", oidcParams.values.client_id], onSuccess: (data) => { toast.info(t("authorizeSuccessTitle"), { description: t("authorizeSuccessSubtitle"), @@ -115,17 +106,17 @@ export const AuthorizePage = () => { }, }); - if (missingParams.length > 0) { + if (oidcParams.issues.length > 0) { return ( ); } if (!isLoggedIn) { - return ; + return ; } if (getClientInfo.isLoading) { @@ -152,6 +143,9 @@ export const AuthorizePage = () => { ); } + const scopes = + oidcParams.values.scope.split(" ").filter((s) => s.trim() !== "") || []; + return ( diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx index fe033b5..48203ec 100644 --- a/frontend/src/pages/login-page.tsx +++ b/frontend/src/pages/login-page.tsx @@ -51,15 +51,12 @@ export const LoginPage = () => { const formId = useId(); const searchParams = new URLSearchParams(search); - const { - values: props, - isOidc, - compiled: compiledOIDCParams, - } = useOIDCParams(searchParams); + const redirectUri = searchParams.get("redirect_uri") || undefined; + const oidcParams = useOIDCParams(searchParams); const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState( providers.find((provider) => provider.id === oauthAutoRedirect) !== - undefined && props.redirect_uri, + undefined && redirectUri !== undefined, ); const oauthProviders = providers.filter( @@ -78,7 +75,7 @@ export const LoginPage = () => { } = useMutation({ mutationFn: (provider: string) => axios.get( - `/api/oauth/url/${provider}${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, + `/api/oauth/url/${provider}${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`, ), mutationKey: ["oauth"], onSuccess: (data) => { @@ -110,7 +107,7 @@ export const LoginPage = () => { onSuccess: (data) => { if (data.data.totpPending) { window.location.replace( - `/totp${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, + `/totp${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`, ); return; } @@ -120,12 +117,12 @@ export const LoginPage = () => { }); redirectTimer.current = window.setTimeout(() => { - if (isOidc) { - window.location.replace(`/authorize?${compiledOIDCParams}`); + if (oidcParams.isOidc) { + window.location.replace(`/authorize?${oidcParams.compiled}`); return; } window.location.replace( - `/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, + `/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`, ); }, 500); }, @@ -144,7 +141,7 @@ export const LoginPage = () => { !isLoggedIn && isOauthAutoRedirect && !hasAutoRedirectedRef.current && - props.redirect_uri + redirectUri !== undefined ) { hasAutoRedirectedRef.current = true; oauthMutate(oauthAutoRedirect); @@ -155,7 +152,7 @@ export const LoginPage = () => { hasAutoRedirectedRef, oauthAutoRedirect, isOauthAutoRedirect, - props.redirect_uri, + redirectUri, ]); useEffect(() => { @@ -170,14 +167,14 @@ export const LoginPage = () => { }; }, [redirectTimer, redirectButtonTimer]); - if (isLoggedIn && isOidc) { - return ; + if (isLoggedIn && oidcParams.isOidc) { + return ; } - if (isLoggedIn && props.redirect_uri !== "") { + if (isLoggedIn && redirectUri !== "") { return ( ); diff --git a/frontend/src/pages/totp-page.tsx b/frontend/src/pages/totp-page.tsx index 1b723eb..d0b9726 100644 --- a/frontend/src/pages/totp-page.tsx +++ b/frontend/src/pages/totp-page.tsx @@ -27,11 +27,8 @@ export const TotpPage = () => { const redirectTimer = useRef(null); const searchParams = new URLSearchParams(search); - const { - values: props, - isOidc, - compiled: compiledOIDCParams, - } = useOIDCParams(searchParams); + const redirectUri = searchParams.get("redirect_uri") || undefined; + const oidcParams = useOIDCParams(searchParams); const totpMutation = useMutation({ mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values), @@ -42,13 +39,13 @@ export const TotpPage = () => { }); redirectTimer.current = window.setTimeout(() => { - if (isOidc) { - window.location.replace(`/authorize?${compiledOIDCParams}`); + if (oidcParams.isOidc) { + window.location.replace(`/authorize?${oidcParams.compiled}`); return; } window.location.replace( - `/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`, + `/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`, ); }, 500); },