Skip to content

Commit bc360c1

Browse files
committed
feat: Implement OAuth callback handling; add handleOAuthCallbackAction and integrate with AsgardeoProvider
1 parent be5600c commit bc360c1

File tree

6 files changed

+160
-33
lines changed

6 files changed

+160
-33
lines changed

packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
} from '@asgardeo/node';
2828
import {I18nProvider, FlowProvider, UserProvider, ThemeProvider, AsgardeoProviderProps} from '@asgardeo/react';
2929
import {FC, PropsWithChildren, useEffect, useMemo, useState} from 'react';
30-
import {useRouter} from 'next/navigation';
30+
import {useRouter, useSearchParams} from 'next/navigation';
3131
import AsgardeoContext, {AsgardeoContextProps} from './AsgardeoContext';
3232

3333
/**
@@ -38,6 +38,7 @@ export type AsgardeoClientProviderProps = Partial<Omit<AsgardeoProviderProps, 'b
3838
signOut: AsgardeoContextProps['signOut'];
3939
signIn: AsgardeoContextProps['signIn'];
4040
signUp: AsgardeoContextProps['signUp'];
41+
handleOAuthCallback: (code: string, state: string, sessionState?: string) => Promise<{success: boolean; error?: string; redirectUrl?: string}>;
4142
isSignedIn: boolean;
4243
userProfile: UserProfile;
4344
user: User | null;
@@ -49,6 +50,7 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
4950
signIn,
5051
signOut,
5152
signUp,
53+
handleOAuthCallback,
5254
preferences,
5355
isSignedIn,
5456
signInUrl,
@@ -57,10 +59,61 @@ const AsgardeoClientProvider: FC<PropsWithChildren<AsgardeoClientProviderProps>>
5759
userProfile,
5860
}: PropsWithChildren<AsgardeoClientProviderProps>) => {
5961
const router = useRouter();
62+
const searchParams = useSearchParams();
6063
const [isDarkMode, setIsDarkMode] = useState(false);
6164
const [isLoading, setIsLoading] = useState<boolean>(true);
6265
const [_userProfile, setUserProfile] = useState<UserProfile | null>(userProfile);
6366

67+
// Handle OAuth callback automatically
68+
useEffect(() => {
69+
// Don't handle callback if already signed in
70+
if (isSignedIn) return;
71+
72+
const processOAuthCallback = async () => {
73+
try {
74+
const code = searchParams.get('code');
75+
const state = searchParams.get('state');
76+
const sessionState = searchParams.get('session_state');
77+
const error = searchParams.get('error');
78+
const errorDescription = searchParams.get('error_description');
79+
80+
// Check for OAuth errors first
81+
if (error) {
82+
console.error('[AsgardeoClientProvider] OAuth error:', error, errorDescription);
83+
// Redirect to sign-in page with error
84+
router.push(`/signin?error=${encodeURIComponent(error)}&error_description=${encodeURIComponent(errorDescription || '')}`);
85+
return;
86+
}
87+
88+
// Handle OAuth callback if code and state are present
89+
if (code && state) {
90+
console.log('[AsgardeoClientProvider] Handling OAuth callback');
91+
setIsLoading(true);
92+
93+
const result = await handleOAuthCallback(code, state, sessionState || undefined);
94+
95+
if (result.success) {
96+
// Redirect to the success URL
97+
if (result.redirectUrl) {
98+
router.push(result.redirectUrl);
99+
} else {
100+
// Refresh the page to update authentication state
101+
window.location.reload();
102+
}
103+
} else {
104+
console.error('[AsgardeoClientProvider] OAuth callback failed:', result.error);
105+
router.push(`/signin?error=authentication_failed&error_description=${encodeURIComponent(result.error || 'Authentication failed')}`);
106+
}
107+
}
108+
} catch (error) {
109+
console.error('[AsgardeoClientProvider] Failed to handle OAuth callback:', error);
110+
router.push('/signin?error=authentication_failed');
111+
}
112+
};
113+
114+
processOAuthCallback();
115+
}, [searchParams, router, isSignedIn, handleOAuthCallback]);
116+
64117
useEffect(() => {
65118
if (!preferences?.theme?.mode || preferences.theme.mode === 'system') {
66119
setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches);

packages/nextjs/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export * from './client/contexts/Asgardeo/useAsgardeo';
2424

2525
export {default as isSignedIn} from './server/actions/isSignedIn';
2626

27+
export {default as handleOAuthCallback} from './server/actions/handleOAuthCallbackAction';
28+
2729
export {default as SignedIn} from './client/components/control/SignedIn/SignedIn';
2830
export {SignedInProps} from './client/components/control/SignedIn/SignedIn';
2931

packages/nextjs/src/server/AsgardeoProvider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import getUserAction from './actions/getUserAction';
2828
import getSessionId from './actions/getSessionId';
2929
import getUserProfileAction from './actions/getUserProfileAction';
3030
import signUpAction from './actions/signUpAction';
31+
import handleOAuthCallbackAction from './actions/handleOAuthCallbackAction';
3132

3233
/**
3334
* Props interface of {@link AsgardeoServerProvider}
@@ -100,6 +101,7 @@ const AsgardeoServerProvider: FC<PropsWithChildren<AsgardeoServerProviderProps>>
100101
signIn={signInAction}
101102
signOut={signOutAction}
102103
signUp={signUpAction}
104+
handleOAuthCallback={handleOAuthCallbackAction}
103105
signInUrl={config.signInUrl}
104106
signUpUrl={config.signUpUrl}
105107
preferences={config.preferences}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
'use server';
20+
21+
import { cookies } from 'next/headers';
22+
import { CookieConfig } from '@asgardeo/node';
23+
import AsgardeoNextClient from '../../AsgardeoNextClient';
24+
25+
/**
26+
* Server action to handle OAuth callback with authorization code.
27+
* This action processes the authorization code received from the OAuth provider
28+
* and exchanges it for tokens to complete the authentication flow.
29+
*
30+
* @param code - Authorization code from OAuth provider
31+
* @param state - State parameter from OAuth provider for CSRF protection
32+
* @param sessionState - Session state parameter from OAuth provider
33+
* @returns Promise that resolves with success status and optional error message
34+
*/
35+
const handleOAuthCallbackAction = async (
36+
code: string,
37+
state: string,
38+
sessionState?: string
39+
): Promise<{
40+
success: boolean;
41+
error?: string;
42+
redirectUrl?: string;
43+
}> => {
44+
try {
45+
if (!code || !state) {
46+
return {
47+
success: false,
48+
error: 'Missing required OAuth parameters: code and state are required'
49+
};
50+
}
51+
52+
// Get the Asgardeo client instance
53+
const asgardeoClient = AsgardeoNextClient.getInstance();
54+
55+
if (!asgardeoClient.isInitialized) {
56+
return {
57+
success: false,
58+
error: 'Asgardeo client is not initialized'
59+
};
60+
}
61+
62+
// Get the session ID from cookies
63+
const cookieStore = await cookies();
64+
const sessionId = cookieStore.get(CookieConfig.SESSION_COOKIE_NAME)?.value;
65+
66+
if (!sessionId) {
67+
return {
68+
success: false,
69+
error: 'No session found. Please start the authentication flow again.'
70+
};
71+
}
72+
73+
// Exchange the authorization code for tokens
74+
await asgardeoClient.signIn(
75+
{
76+
code,
77+
session_state: sessionState,
78+
state,
79+
} as any,
80+
{},
81+
sessionId
82+
);
83+
84+
// Get the after sign-in URL from configuration
85+
const config = await asgardeoClient.getConfiguration();
86+
const afterSignInUrl = config.afterSignInUrl || '/';
87+
88+
return {
89+
success: true,
90+
redirectUrl: afterSignInUrl
91+
};
92+
} catch (error) {
93+
console.error('[handleOAuthCallbackAction] OAuth callback error:', error);
94+
95+
return {
96+
success: false,
97+
error: error instanceof Error ? error.message : 'Authentication failed'
98+
};
99+
}
100+
};
101+
102+
export default handleOAuthCallbackAction;

packages/nextjs/src/server/actions/signInAction.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@ const signInAction = async (
7373

7474
return {success: true, data: {signInUrl: String(defaultSignInUrl)}};
7575
} else {
76-
console.log('[signInAction] Handling embedded sign-in flow with payload:', payload);
77-
console.log('[signInAction] Request config:', request);
78-
console.log('[signInAction] User ID:', userId);
7976
const response: any = await client.signIn(payload, request!, userId);
8077

8178
if (response.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) {

samples/teamspace-nextjs/app/actions/auth.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)