Skip to content

Commit ff64d64

Browse files
committed
refactor: use zod for oidc params
1 parent 646e24d commit ff64d64

4 files changed

Lines changed: 69 additions & 104 deletions

File tree

frontend/src/lib/hooks/oidc.ts

Lines changed: 36 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,41 @@
1-
export type OIDCValues = {
2-
scope: string;
3-
response_type: string;
4-
client_id: string;
5-
redirect_uri: string;
6-
state: string;
7-
nonce: string;
8-
code_challenge: string;
9-
code_challenge_method: string;
10-
};
11-
12-
interface IuseOIDCParams {
13-
values: OIDCValues;
14-
compiled: string;
1+
import { z } from "zod";
2+
3+
export const oidcParamsSchema = z.object({
4+
scope: z.string(),
5+
response_type: z.string(),
6+
client_id: z.string(),
7+
redirect_uri: z.string(),
8+
state: z.string().optional(),
9+
nonce: z.string().optional(),
10+
code_challenge: z.string().optional(),
11+
code_challenge_method: z.string().optional(),
12+
prompt: z.string().optional(),
13+
});
14+
15+
export const useOIDCParams = (
16+
params: URLSearchParams,
17+
): {
18+
values: z.infer<typeof oidcParamsSchema>;
19+
issues: string[];
1520
isOidc: boolean;
16-
missingParams: string[];
17-
}
18-
19-
const optionalParams: string[] = [
20-
"state",
21-
"nonce",
22-
"code_challenge",
23-
"code_challenge_method",
24-
];
25-
26-
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
27-
let compiled: string = "";
28-
let isOidc = false;
29-
const missingParams: string[] = [];
30-
31-
const values: OIDCValues = {
32-
scope: params.get("scope") ?? "",
33-
response_type: params.get("response_type") ?? "",
34-
client_id: params.get("client_id") ?? "",
35-
redirect_uri: params.get("redirect_uri") ?? "",
36-
state: params.get("state") ?? "",
37-
nonce: params.get("nonce") ?? "",
38-
code_challenge: params.get("code_challenge") ?? "",
39-
code_challenge_method: params.get("code_challenge_method") ?? "",
40-
};
41-
42-
for (const key of Object.keys(values)) {
43-
if (!values[key as keyof OIDCValues]) {
44-
if (!optionalParams.includes(key)) {
45-
missingParams.push(key);
46-
}
47-
}
48-
}
49-
50-
if (missingParams.length === 0) {
51-
isOidc = true;
52-
}
53-
54-
if (isOidc) {
55-
compiled = new URLSearchParams(values).toString();
21+
compiled: string;
22+
} => {
23+
const obj = Object.fromEntries(params.entries());
24+
const parsed = oidcParamsSchema.safeParse(obj);
25+
26+
if (parsed.success) {
27+
return {
28+
values: parsed.data,
29+
issues: [],
30+
isOidc: true,
31+
compiled: new URLSearchParams(parsed.data).toString(),
32+
};
5633
}
5734

5835
return {
59-
values,
60-
compiled,
61-
isOidc,
62-
missingParams,
36+
issues: parsed.error.issues.map((issue) => issue.path.toString()),
37+
values: {} as z.infer<typeof oidcParamsSchema>,
38+
isOidc: false,
39+
compiled: "",
6340
};
64-
}
41+
};

frontend/src/pages/authorize-page.tsx

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -72,36 +72,27 @@ export const AuthorizePage = () => {
7272
const scopeMap = createScopeMap(t);
7373

7474
const searchParams = new URLSearchParams(search);
75-
const {
76-
values: props,
77-
missingParams,
78-
isOidc,
79-
compiled: compiledOIDCParams,
80-
} = useOIDCParams(searchParams);
81-
const scopes = props.scope ? props.scope.split(" ").filter(Boolean) : [];
75+
const oidcParams = useOIDCParams(searchParams);
8276

8377
const getClientInfo = useQuery({
84-
queryKey: ["client", props.client_id],
78+
queryKey: ["client", oidcParams.values.client_id],
8579
queryFn: async () => {
86-
const res = await fetch(`/api/oidc/clients/${props.client_id}`);
80+
const res = await fetch(
81+
`/api/oidc/clients/${oidcParams.values.client_id}`,
82+
);
8783
const data = await getOidcClientInfoSchema.parseAsync(await res.json());
8884
return data;
8985
},
90-
enabled: isOidc,
86+
enabled: oidcParams.isOidc,
9187
});
9288

9389
const authorizeMutation = useMutation({
9490
mutationFn: () => {
9591
return axios.post("/api/oidc/authorize", {
96-
scope: props.scope,
97-
response_type: props.response_type,
98-
client_id: props.client_id,
99-
redirect_uri: props.redirect_uri,
100-
state: props.state,
101-
nonce: props.nonce,
92+
...oidcParams.values,
10293
});
10394
},
104-
mutationKey: ["authorize", props.client_id],
95+
mutationKey: ["authorize", oidcParams.values.client_id],
10596
onSuccess: (data) => {
10697
toast.info(t("authorizeSuccessTitle"), {
10798
description: t("authorizeSuccessSubtitle"),
@@ -115,17 +106,17 @@ export const AuthorizePage = () => {
115106
},
116107
});
117108

118-
if (missingParams.length > 0) {
109+
if (oidcParams.issues.length > 0) {
119110
return (
120111
<Navigate
121-
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: missingParams.join(", ") }))}`}
112+
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: oidcParams.issues.join(", ") }))}`}
122113
replace
123114
/>
124115
);
125116
}
126117

127118
if (!isLoggedIn) {
128-
return <Navigate to={`/login?${compiledOIDCParams}`} replace />;
119+
return <Navigate to={`/login?${oidcParams.compiled}`} replace />;
129120
}
130121

131122
if (getClientInfo.isLoading) {
@@ -152,6 +143,9 @@ export const AuthorizePage = () => {
152143
);
153144
}
154145

146+
const scopes =
147+
oidcParams.values.scope.split(" ").filter((s) => s.trim() !== "") || [];
148+
155149
return (
156150
<Card>
157151
<CardHeader className="mb-2">

frontend/src/pages/login-page.tsx

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,12 @@ export const LoginPage = () => {
5151
const formId = useId();
5252

5353
const searchParams = new URLSearchParams(search);
54-
const {
55-
values: props,
56-
isOidc,
57-
compiled: compiledOIDCParams,
58-
} = useOIDCParams(searchParams);
54+
const redirectUri = searchParams.get("redirect_uri") || undefined;
55+
const oidcParams = useOIDCParams(searchParams);
5956

6057
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
6158
providers.find((provider) => provider.id === oauthAutoRedirect) !==
62-
undefined && props.redirect_uri,
59+
undefined && redirectUri !== undefined,
6360
);
6461

6562
const oauthProviders = providers.filter(
@@ -78,7 +75,7 @@ export const LoginPage = () => {
7875
} = useMutation({
7976
mutationFn: (provider: string) =>
8077
axios.get(
81-
`/api/oauth/url/${provider}${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
78+
`/api/oauth/url/${provider}${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
8279
),
8380
mutationKey: ["oauth"],
8481
onSuccess: (data) => {
@@ -110,7 +107,7 @@ export const LoginPage = () => {
110107
onSuccess: (data) => {
111108
if (data.data.totpPending) {
112109
window.location.replace(
113-
`/totp${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
110+
`/totp${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
114111
);
115112
return;
116113
}
@@ -120,12 +117,12 @@ export const LoginPage = () => {
120117
});
121118

122119
redirectTimer.current = window.setTimeout(() => {
123-
if (isOidc) {
124-
window.location.replace(`/authorize?${compiledOIDCParams}`);
120+
if (oidcParams.isOidc) {
121+
window.location.replace(`/authorize?${oidcParams.compiled}`);
125122
return;
126123
}
127124
window.location.replace(
128-
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
125+
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
129126
);
130127
}, 500);
131128
},
@@ -144,7 +141,7 @@ export const LoginPage = () => {
144141
!isLoggedIn &&
145142
isOauthAutoRedirect &&
146143
!hasAutoRedirectedRef.current &&
147-
props.redirect_uri
144+
redirectUri !== undefined
148145
) {
149146
hasAutoRedirectedRef.current = true;
150147
oauthMutate(oauthAutoRedirect);
@@ -155,7 +152,7 @@ export const LoginPage = () => {
155152
hasAutoRedirectedRef,
156153
oauthAutoRedirect,
157154
isOauthAutoRedirect,
158-
props.redirect_uri,
155+
redirectUri,
159156
]);
160157

161158
useEffect(() => {
@@ -170,14 +167,14 @@ export const LoginPage = () => {
170167
};
171168
}, [redirectTimer, redirectButtonTimer]);
172169

173-
if (isLoggedIn && isOidc) {
174-
return <Navigate to={`/authorize?${compiledOIDCParams}`} replace />;
170+
if (isLoggedIn && oidcParams.isOidc) {
171+
return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
175172
}
176173

177-
if (isLoggedIn && props.redirect_uri !== "") {
174+
if (isLoggedIn && redirectUri !== "") {
178175
return (
179176
<Navigate
180-
to={`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`}
177+
to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
181178
replace
182179
/>
183180
);

frontend/src/pages/totp-page.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,8 @@ export const TotpPage = () => {
2727
const redirectTimer = useRef<number | null>(null);
2828

2929
const searchParams = new URLSearchParams(search);
30-
const {
31-
values: props,
32-
isOidc,
33-
compiled: compiledOIDCParams,
34-
} = useOIDCParams(searchParams);
30+
const redirectUri = searchParams.get("redirect_uri") || undefined;
31+
const oidcParams = useOIDCParams(searchParams);
3532

3633
const totpMutation = useMutation({
3734
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
@@ -42,13 +39,13 @@ export const TotpPage = () => {
4239
});
4340

4441
redirectTimer.current = window.setTimeout(() => {
45-
if (isOidc) {
46-
window.location.replace(`/authorize?${compiledOIDCParams}`);
42+
if (oidcParams.isOidc) {
43+
window.location.replace(`/authorize?${oidcParams.compiled}`);
4744
return;
4845
}
4946

5047
window.location.replace(
51-
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
48+
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
5249
);
5350
}, 500);
5451
},

0 commit comments

Comments
 (0)