-
Notifications
You must be signed in to change notification settings - Fork 446
Expand file tree
/
Copy pathuseSSO.ts
More file actions
144 lines (125 loc) · 4.96 KB
/
useSSO.ts
File metadata and controls
144 lines (125 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import { useClerk, useSignIn } from '@clerk/react';
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { OAuthStrategy, EnterpriseSSOStrategy } from '@clerk/shared/types';
import * as SecureStore from 'expo-secure-store';
import type * as WebBrowser from 'expo-web-browser';
import { CLERK_CLIENT_JWT_KEY } from '../constants';
import { errorThrower } from '../utils/errors';
export type StartSSOFlowParams = {
redirectUrl?: string;
unsafeMetadata?: SignUpUnsafeMetadata;
authSessionOptions?: Pick<WebBrowser.AuthSessionOpenOptions, 'showInRecents'>;
} & (
| {
strategy: OAuthStrategy;
}
| {
strategy: EnterpriseSSOStrategy;
identifier: string;
}
);
export type StartSSOFlowReturnType = {
createdSessionId: string | null;
authSessionResult: WebBrowser.WebBrowserAuthSessionResult | null;
};
export function useSSO() {
const clerk = useClerk();
const { signIn } = useSignIn();
async function startSSOFlow(startSSOFlowParams: StartSSOFlowParams): Promise<StartSSOFlowReturnType> {
if (!signIn || !clerk.client) {
return {
createdSessionId: null,
authSessionResult: null,
};
}
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- dynamic import of optional dependency
let AuthSession: typeof import('expo-auth-session');
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- dynamic import of optional dependency
let WebBrowserModule: typeof import('expo-web-browser');
try {
[AuthSession, WebBrowserModule] = await Promise.all([import('expo-auth-session'), import('expo-web-browser')]);
} catch {
return errorThrower.throw(
'expo-auth-session and expo-web-browser are required for SSO. ' +
'Install them: npx expo install expo-auth-session expo-web-browser',
);
}
const { strategy, authSessionOptions } = startSSOFlowParams ?? {};
const redirectUrl =
startSSOFlowParams.redirectUrl ??
AuthSession.makeRedirectUri({
path: 'sso-callback',
});
const createParams = {
strategy,
redirectUrl,
...(startSSOFlowParams.strategy === 'enterprise_sso' ? { identifier: startSSOFlowParams.identifier } : {}),
};
// Create the sign-in attempt. If a stale session exists (e.g. JWT persisted
// in SecureStore after an incomplete sign-out), clear the token and retry.
// The error can surface as either a thrown exception (client-side "already signed in"
// guard) or a returned { error } (FAPI "session_exists" response).
try {
const createResult = await signIn.create(createParams);
if (createResult.error) {
throw createResult.error;
}
} catch (err) {
const isSessionExists = isClerkAPIResponseError(err) && err.errors.some(e => e.code === 'session_exists');
const isAlreadySignedIn = err instanceof Error && err.message?.includes('already signed in');
if (isSessionExists || isAlreadySignedIn) {
await SecureStore.deleteItemAsync(CLERK_CLIENT_JWT_KEY);
const retryResult = await signIn.create(createParams);
if (retryResult.error) {
throw retryResult.error;
}
} else {
throw err;
}
}
const { externalVerificationRedirectURL } = signIn.firstFactorVerification;
if (!externalVerificationRedirectURL) {
return errorThrower.throw('Missing external verification redirect URL for SSO flow');
}
// Open the in-app browser for the OAuth/SSO provider.
let authSessionResult: WebBrowser.WebBrowserAuthSessionResult;
try {
authSessionResult = await WebBrowserModule.openAuthSessionAsync(
externalVerificationRedirectURL.toString(),
redirectUrl,
authSessionOptions,
);
} finally {
// Dismiss the browser to prevent it from lingering in the background,
// which can cause subsequent SSO attempts to fail or appear frozen.
try {
await WebBrowserModule.dismissBrowser();
} catch {
// Already dismissed (e.g. iOS ASWebAuthenticationSession auto-dismisses on success)
}
}
if (authSessionResult.type !== 'success' || !authSessionResult.url) {
return {
createdSessionId: null,
authSessionResult,
};
}
const callbackParams = new URL(authSessionResult.url).searchParams;
const createdSessionId = callbackParams.get('created_session_id');
const rotatingTokenNonce = callbackParams.get('rotating_token_nonce') ?? '';
// Pass the nonce to FAPI to verify the OAuth callback and update the client
// with the newly created session. The FAPI response populates the client's
// session list as a side effect, which is required for setActive to work.
await clerk.client.signIn.reload({ rotatingTokenNonce });
if (createdSessionId) {
await clerk.setActive({ session: createdSessionId });
}
return {
createdSessionId,
authSessionResult,
};
}
return {
startSSOFlow,
};
}