1- import { useState , useEffect , FormEvent } from "react" ;
1+ import { useState , useEffect , useRef , FormEvent } from "react" ;
22import { isSdkError } from "../api/errors" ;
33import {
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