Skip to content

Commit 3c89caa

Browse files
committed
feat(ui-react): complete OAuth authorization in login flow
1 parent 04de400 commit 3c89caa

1 file changed

Lines changed: 55 additions & 2 deletions

File tree

ui-react/apps/console/src/pages/Login.tsx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, FormEvent } from "react";
1+
import { useState, useEffect, useRef, FormEvent } from "react";
22
import { isSdkError } from "../api/errors";
33
import {
44
useNavigate,
@@ -76,6 +76,11 @@ export default function Login() {
7676
const [searchParams] = useSearchParams();
7777
const queryToken = searchParams.get("token");
7878
const missingAssertions = searchParams.get("missing_assertions");
79+
const oauthClientId = searchParams.get("oauth_client_id");
80+
const oauthRedirectUri = searchParams.get("oauth_redirect_uri");
81+
const oauthCodeChallenge = searchParams.get("oauth_code_challenge");
82+
const oauthState = searchParams.get("oauth_state");
83+
const isOAuthFlow = !!(oauthClientId && oauthRedirectUri && oauthCodeChallenge);
7984
const [tokenLoading, setTokenLoading] = useState(!!queryToken);
8085
const [authentication, setAuthentication] = useState<{
8186
local?: boolean;
@@ -100,7 +105,52 @@ export default function Login() {
100105
const [error, setError] = useState<string | null>(null);
101106
const [lockoutEndEpoch, setLockoutEndEpoch] = useState<number | null>(null);
102107
const { login, loading } = useAuthStore();
108+
const sessionToken = useAuthStore((s) => s.token);
103109
const navigate = useNavigate();
110+
const oauthFinalizedRef = useRef(false);
111+
112+
useEffect(() => {
113+
if (!isOAuthFlow || !sessionToken || oauthFinalizedRef.current) return;
114+
oauthFinalizedRef.current = true;
115+
116+
void (async () => {
117+
try {
118+
const res = await fetch("/api/oauth/authorize/callback", {
119+
method: "POST",
120+
headers: {
121+
"Content-Type": "application/json",
122+
Authorization: `Bearer ${sessionToken}`,
123+
},
124+
body: JSON.stringify({
125+
client_id: oauthClientId,
126+
redirect_uri: oauthRedirectUri,
127+
code_challenge: oauthCodeChallenge,
128+
state: oauthState ?? "",
129+
}),
130+
});
131+
132+
if (!res.ok) {
133+
throw new Error(`OAuth callback failed: ${res.status}`);
134+
}
135+
136+
const data = await res.json() as { code: string; state: string };
137+
const redirect = new URL(oauthRedirectUri ?? "");
138+
redirect.searchParams.set("code", data.code);
139+
if (data.state) redirect.searchParams.set("state", data.state);
140+
window.location.assign(redirect.toString());
141+
} catch (err) {
142+
oauthFinalizedRef.current = false;
143+
setError(err instanceof Error ? err.message : "Failed to complete authorization.");
144+
}
145+
})();
146+
}, [
147+
isOAuthFlow,
148+
sessionToken,
149+
oauthClientId,
150+
oauthRedirectUri,
151+
oauthCodeChallenge,
152+
oauthState,
153+
]);
104154
const { display: countdownDisplay, expired: lockoutExpired }
105155
= useLoginCountdown(lockoutEndEpoch);
106156

@@ -146,6 +196,9 @@ export default function Login() {
146196
? `/mfa-login?redirect=${encodeURIComponent(redirect)}`
147197
: "/mfa-login";
148198
void navigate(mfaPath);
199+
} else if (isOAuthFlow) {
200+
// The useEffect watching sessionToken will POST to the OAuth callback
201+
// and redirect to redirect_uri. Don't navigate elsewhere.
149202
} else {
150203
void navigate(redirect);
151204
}
@@ -187,7 +240,7 @@ export default function Login() {
187240
const showLocalForm = !isEnterprise || authentication?.local === true;
188241
const ssoOnly = isEnterprise && authentication?.local === false;
189242

190-
if (tokenLoading) {
243+
if (tokenLoading || (isOAuthFlow && sessionToken && !error)) {
191244
return (
192245
<div className="flex items-center justify-center min-h-[60vh]">
193246
<span className="w-6 h-6 border-2 border-primary/30 border-t-primary rounded-full animate-spin" />

0 commit comments

Comments
 (0)