Compare commits

..

1 Commits

Author SHA1 Message Date
Stavros dd5d0d0359 feat: preserve oidc params in oauth flow 2026-04-08 14:24:19 +03:00
4 changed files with 107 additions and 79 deletions
+57 -33
View File
@@ -1,40 +1,64 @@
import { z } from "zod";
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;
};
export const oidcParamsSchema = z.object({
scope: z.string().min(1),
response_type: z.string().min(1),
client_id: z.string().min(1),
redirect_uri: z.string().min(1),
state: z.string().optional(),
nonce: z.string().optional(),
code_challenge: z.string().optional(),
code_challenge_method: z.string().optional(),
});
export const useOIDCParams = (
params: URLSearchParams,
): {
values: z.infer<typeof oidcParamsSchema>;
issues: string[];
isOidc: boolean;
interface IuseOIDCParams {
values: OIDCValues;
compiled: string;
} => {
const obj = Object.fromEntries(params.entries());
const parsed = oidcParamsSchema.safeParse(obj);
isOidc: boolean;
missingParams: string[];
}
if (parsed.success) {
return {
values: parsed.data,
issues: [],
isOidc: true,
compiled: new URLSearchParams(parsed.data).toString(),
};
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();
}
return {
issues: parsed.error.issues.map((issue) => issue.path.toString()),
values: {} as z.infer<typeof oidcParamsSchema>,
isOidc: false,
compiled: "",
values,
compiled,
isOidc,
missingParams,
};
};
}
+20 -14
View File
@@ -72,27 +72,36 @@ export const AuthorizePage = () => {
const scopeMap = createScopeMap(t);
const searchParams = new URLSearchParams(search);
const oidcParams = useOIDCParams(searchParams);
const {
values: props,
missingParams,
isOidc,
compiled: compiledOIDCParams,
} = useOIDCParams(searchParams);
const scopes = props.scope ? props.scope.split(" ").filter(Boolean) : [];
const getClientInfo = useQuery({
queryKey: ["client", oidcParams.values.client_id],
queryKey: ["client", props.client_id],
queryFn: async () => {
const res = await fetch(
`/api/oidc/clients/${encodeURIComponent(oidcParams.values.client_id)}`,
);
const res = await fetch(`/api/oidc/clients/${props.client_id}`);
const data = await getOidcClientInfoSchema.parseAsync(await res.json());
return data;
},
enabled: oidcParams.isOidc,
enabled: isOidc,
});
const authorizeMutation = useMutation({
mutationFn: () => {
return axios.post("/api/oidc/authorize", {
...oidcParams.values,
scope: props.scope,
response_type: props.response_type,
client_id: props.client_id,
redirect_uri: props.redirect_uri,
state: props.state,
nonce: props.nonce,
});
},
mutationKey: ["authorize", oidcParams.values.client_id],
mutationKey: ["authorize", props.client_id],
onSuccess: (data) => {
toast.info(t("authorizeSuccessTitle"), {
description: t("authorizeSuccessSubtitle"),
@@ -106,17 +115,17 @@ export const AuthorizePage = () => {
},
});
if (oidcParams.issues.length > 0) {
if (missingParams.length > 0) {
return (
<Navigate
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: oidcParams.issues.join(", ") }))}`}
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: missingParams.join(", ") }))}`}
replace
/>
);
}
if (!isLoggedIn) {
return <Navigate to={`/login?${oidcParams.compiled}`} replace />;
return <Navigate to={`/login?${compiledOIDCParams}`} replace />;
}
if (getClientInfo.isLoading) {
@@ -143,9 +152,6 @@ export const AuthorizePage = () => {
);
}
const scopes =
oidcParams.values.scope.split(" ").filter((s) => s.trim() !== "") || [];
return (
<Card>
<CardHeader className="mb-2">
+22 -27
View File
@@ -51,12 +51,15 @@ export const LoginPage = () => {
const formId = useId();
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri") || undefined;
const oidcParams = useOIDCParams(searchParams);
const {
values: props,
isOidc,
compiled: compiledOIDCParams,
} = useOIDCParams(searchParams);
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
providers.find((provider) => provider.id === oauthAutoRedirect) !==
undefined && redirectUri !== undefined,
undefined && props.redirect_uri,
);
const oauthProviders = providers.filter(
@@ -74,16 +77,12 @@ export const LoginPage = () => {
variables: oauthVariables,
} = useMutation({
mutationFn: (provider: string) => {
const getParams = function (): string {
if (oidcParams.isOidc) {
return `?${oidcParams.compiled}`;
}
if (redirectUri) {
return `?redirect_uri=${encodeURIComponent(redirectUri)}`;
}
return "";
};
return axios.get(`/api/oauth/url/${provider}${getParams()}`);
const params = isOidc
? `?${compiledOIDCParams}`
: props.redirect_uri
? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}`
: "";
return axios.get(`/api/oauth/url/${provider}${params}`);
},
mutationKey: ["oauth"],
onSuccess: (data) => {
@@ -114,12 +113,8 @@ export const LoginPage = () => {
mutationKey: ["login"],
onSuccess: (data) => {
if (data.data.totpPending) {
if (oidcParams.isOidc) {
window.location.replace(`/totp?${oidcParams.compiled}`);
return;
}
window.location.replace(
`/totp${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
`/totp${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
);
return;
}
@@ -129,12 +124,12 @@ export const LoginPage = () => {
});
redirectTimer.current = window.setTimeout(() => {
if (oidcParams.isOidc) {
window.location.replace(`/authorize?${oidcParams.compiled}`);
if (isOidc) {
window.location.replace(`/authorize?${compiledOIDCParams}`);
return;
}
window.location.replace(
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
);
}, 500);
},
@@ -153,7 +148,7 @@ export const LoginPage = () => {
!isLoggedIn &&
isOauthAutoRedirect &&
!hasAutoRedirectedRef.current &&
redirectUri !== undefined
props.redirect_uri
) {
hasAutoRedirectedRef.current = true;
oauthMutate(oauthAutoRedirect);
@@ -164,7 +159,7 @@ export const LoginPage = () => {
hasAutoRedirectedRef,
oauthAutoRedirect,
isOauthAutoRedirect,
redirectUri,
props.redirect_uri,
]);
useEffect(() => {
@@ -179,14 +174,14 @@ export const LoginPage = () => {
};
}, [redirectTimer, redirectButtonTimer]);
if (isLoggedIn && oidcParams.isOidc) {
return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
if (isLoggedIn && isOidc) {
return <Navigate to={`/authorize?${compiledOIDCParams}`} replace />;
}
if (isLoggedIn && redirectUri !== undefined) {
if (isLoggedIn && props.redirect_uri !== "") {
return (
<Navigate
to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
to={`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`}
replace
/>
);
+8 -5
View File
@@ -27,8 +27,11 @@ export const TotpPage = () => {
const redirectTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri") || undefined;
const oidcParams = useOIDCParams(searchParams);
const {
values: props,
isOidc,
compiled: compiledOIDCParams,
} = useOIDCParams(searchParams);
const totpMutation = useMutation({
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
@@ -39,13 +42,13 @@ export const TotpPage = () => {
});
redirectTimer.current = window.setTimeout(() => {
if (oidcParams.isOidc) {
window.location.replace(`/authorize?${oidcParams.compiled}`);
if (isOidc) {
window.location.replace(`/authorize?${compiledOIDCParams}`);
return;
}
window.location.replace(
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
);
}, 500);
},