Skip to content

Commit 8b91ce0

Browse files
authored
refactor: use zod for oidc params (#771)
* refactor: use zod for oidc params * fix: review comments * fix: use min instead of nonempty
1 parent 298f1bf commit 8b91ce0

4 files changed

Lines changed: 81 additions & 109 deletions

File tree

frontend/src/lib/hooks/oidc.ts

Lines changed: 35 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,40 @@
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().min(1),
5+
response_type: z.string().min(1),
6+
client_id: z.string().min(1),
7+
redirect_uri: z.string().min(1),
8+
state: z.string().optional(),
9+
nonce: z.string().optional(),
10+
code_challenge: z.string().optional(),
11+
code_challenge_method: z.string().optional(),
12+
});
13+
14+
export const useOIDCParams = (
15+
params: URLSearchParams,
16+
): {
17+
values: z.infer<typeof oidcParamsSchema>;
18+
issues: string[];
1519
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();
20+
compiled: string;
21+
} => {
22+
const obj = Object.fromEntries(params.entries());
23+
const parsed = oidcParamsSchema.safeParse(obj);
24+
25+
if (parsed.success) {
26+
return {
27+
values: parsed.data,
28+
issues: [],
29+
isOidc: true,
30+
compiled: new URLSearchParams(parsed.data).toString(),
31+
};
5632
}
5733

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

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/${encodeURIComponent(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: 27 additions & 22 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(
@@ -77,12 +74,16 @@ export const LoginPage = () => {
7774
variables: oauthVariables,
7875
} = useMutation({
7976
mutationFn: (provider: string) => {
80-
const params = isOidc
81-
? `?${compiledOIDCParams}`
82-
: props.redirect_uri
83-
? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}`
84-
: "";
85-
return axios.get(`/api/oauth/url/${provider}${params}`);
77+
const getParams = function (): string {
78+
if (oidcParams.isOidc) {
79+
return `?${oidcParams.compiled}`;
80+
}
81+
if (redirectUri) {
82+
return `?redirect_uri=${encodeURIComponent(redirectUri)}`;
83+
}
84+
return "";
85+
};
86+
return axios.get(`/api/oauth/url/${provider}${getParams()}`);
8687
},
8788
mutationKey: ["oauth"],
8889
onSuccess: (data) => {
@@ -113,8 +114,12 @@ export const LoginPage = () => {
113114
mutationKey: ["login"],
114115
onSuccess: (data) => {
115116
if (data.data.totpPending) {
117+
if (oidcParams.isOidc) {
118+
window.location.replace(`/totp?${oidcParams.compiled}`);
119+
return;
120+
}
116121
window.location.replace(
117-
`/totp${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
122+
`/totp${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
118123
);
119124
return;
120125
}
@@ -124,12 +129,12 @@ export const LoginPage = () => {
124129
});
125130

126131
redirectTimer.current = window.setTimeout(() => {
127-
if (isOidc) {
128-
window.location.replace(`/authorize?${compiledOIDCParams}`);
132+
if (oidcParams.isOidc) {
133+
window.location.replace(`/authorize?${oidcParams.compiled}`);
129134
return;
130135
}
131136
window.location.replace(
132-
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
137+
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
133138
);
134139
}, 500);
135140
},
@@ -148,7 +153,7 @@ export const LoginPage = () => {
148153
!isLoggedIn &&
149154
isOauthAutoRedirect &&
150155
!hasAutoRedirectedRef.current &&
151-
props.redirect_uri
156+
redirectUri !== undefined
152157
) {
153158
hasAutoRedirectedRef.current = true;
154159
oauthMutate(oauthAutoRedirect);
@@ -159,7 +164,7 @@ export const LoginPage = () => {
159164
hasAutoRedirectedRef,
160165
oauthAutoRedirect,
161166
isOauthAutoRedirect,
162-
props.redirect_uri,
167+
redirectUri,
163168
]);
164169

165170
useEffect(() => {
@@ -174,14 +179,14 @@ export const LoginPage = () => {
174179
};
175180
}, [redirectTimer, redirectButtonTimer]);
176181

177-
if (isLoggedIn && isOidc) {
178-
return <Navigate to={`/authorize?${compiledOIDCParams}`} replace />;
182+
if (isLoggedIn && oidcParams.isOidc) {
183+
return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
179184
}
180185

181-
if (isLoggedIn && props.redirect_uri !== "") {
186+
if (isLoggedIn && redirectUri !== undefined) {
182187
return (
183188
<Navigate
184-
to={`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`}
189+
to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
185190
replace
186191
/>
187192
);

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)