Skip to content

Commit e199037

Browse files
AlexNtianagstef
andauthoredNov 6, 2024··
feat(clerk-expo): Support expo passkeys (#4352)
Co-authored-by: Stefanos Anagnostou <anagstef@users.noreply.github.com> Co-authored-by: Stefanos Anagnostou <stefanos@clerk.dev>
1 parent b064f52 commit e199037

File tree

19 files changed

+1667
-5843
lines changed

19 files changed

+1667
-5843
lines changed
 

‎.changeset/late-camels-talk.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@clerk/clerk-js": minor
3+
"@clerk/shared": minor
4+
"@clerk/types": minor
5+
"@clerk/clerk-expo": minor
6+
"@clerk/expo-passkeys": patch
7+
---
8+
9+
Introduce experimental support for passkeys in Expo (iOS, Android, and Web).
10+
11+
To use passkeys in Expo projects, pass the `__experimental_passkeys` object, which can be imported from `@clerk/clerk-expo/passkeys`, to the `ClerkProvider` component:
12+
13+
```tsx
14+
15+
import { ClerkProvider } from '@clerk/clerk-expo';
16+
import { passkeys } from '@clerk/clerk-expo/passkeys';
17+
18+
<ClerkProvider __experimental_passkeys={passkeys}>
19+
{/* Your app here */}
20+
</ClerkProvider>
21+
```
22+
23+
The API for using passkeys in Expo projects is the same as the one used in web apps:
24+
25+
```tsx
26+
// passkey creation
27+
const { user } = useUser();
28+
29+
const handleCreatePasskey = async () => {
30+
if (!user) return;
31+
try {
32+
return await user.createPasskey();
33+
} catch (e: any) {
34+
// handle error
35+
}
36+
};
37+
38+
39+
// passkey authentication
40+
const { signIn, setActive } = useSignIn();
41+
42+
const handlePasskeySignIn = async () => {
43+
try {
44+
const signInResponse = await signIn.authenticateWithPasskey();
45+
await setActive({ session: signInResponse.createdSessionId });
46+
} catch (err: any) {
47+
//handle error
48+
}
49+
};
50+
```

‎package-lock.json

+3-43
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/clerk-js/src/core/clerk.ts

+23
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
ClientResource,
2222
CreateOrganizationParams,
2323
CreateOrganizationProps,
24+
CredentialReturn,
2425
DomainOrProxyUrl,
2526
EnvironmentJSON,
2627
EnvironmentResource,
@@ -36,6 +37,10 @@ import type {
3637
OrganizationProfileProps,
3738
OrganizationResource,
3839
OrganizationSwitcherProps,
40+
PublicKeyCredentialCreationOptionsWithoutExtensions,
41+
PublicKeyCredentialRequestOptionsWithoutExtensions,
42+
PublicKeyCredentialWithAuthenticatorAssertionResponse,
43+
PublicKeyCredentialWithAuthenticatorAttestationResponse,
3944
RedirectOptions,
4045
Resources,
4146
SDKMetadata,
@@ -185,6 +190,24 @@ export class Clerk implements ClerkInterface {
185190
#pageLifecycle: ReturnType<typeof createPageLifecycle> | null = null;
186191
#touchThrottledUntil = 0;
187192

193+
public __internal_createPublicCredentials:
194+
| ((
195+
publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions,
196+
) => Promise<CredentialReturn<PublicKeyCredentialWithAuthenticatorAttestationResponse>>)
197+
| undefined;
198+
199+
public __internal_getPublicCredentials:
200+
| (({
201+
publicKeyOptions,
202+
}: {
203+
publicKeyOptions: PublicKeyCredentialRequestOptionsWithoutExtensions;
204+
}) => Promise<CredentialReturn<PublicKeyCredentialWithAuthenticatorAssertionResponse>>)
205+
| undefined;
206+
207+
public __internal_isWebAuthnSupported: (() => boolean) | undefined;
208+
public __internal_isWebAuthnAutofillSupported: (() => Promise<boolean>) | undefined;
209+
public __internal_isWebAuthnPlatformAuthenticatorSupported: (() => Promise<boolean>) | undefined;
210+
188211
get publishableKey(): string {
189212
return this.#publishableKey;
190213
}

‎packages/clerk-js/src/core/resources/Passkey.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { isWebAuthnPlatformAuthenticatorSupported, isWebAuthnSupported } from '@clerk/shared/webauthn';
1+
import { ClerkWebAuthnError } from '@clerk/shared/error';
2+
import {
3+
isWebAuthnPlatformAuthenticatorSupported as isWebAuthnPlatformAuthenticatorSupportedOnWindow,
4+
isWebAuthnSupported as isWebAuthnSupportedOnWindow,
5+
} from '@clerk/shared/webauthn';
26
import type {
37
DeletedObjectJSON,
48
DeletedObjectResource,
@@ -10,7 +14,10 @@ import type {
1014
} from '@clerk/types';
1115

1216
import { unixEpochToDate } from '../../utils/date';
13-
import { ClerkWebAuthnError, serializePublicKeyCredential, webAuthnCreateCredential } from '../../utils/passkeys';
17+
import {
18+
serializePublicKeyCredential,
19+
webAuthnCreateCredential as webAuthnCreateCredentialOnWindow,
20+
} from '../../utils/passkeys';
1421
import { clerkMissingWebAuthnPublicKeyOptions } from '../errors';
1522
import { BaseResource, DeletedObject, PasskeyVerification } from './internal';
1623

@@ -55,6 +62,13 @@ export class Passkey extends BaseResource implements PasskeyResource {
5562
* The UI should always prevent from this method being called if WebAuthn is not supported.
5663
* As a precaution we need to check if WebAuthn is supported.
5764
*/
65+
const isWebAuthnSupported = Passkey.clerk.__internal_isWebAuthnSupported || isWebAuthnSupportedOnWindow;
66+
const webAuthnCreateCredential =
67+
Passkey.clerk.__internal_createPublicCredentials || webAuthnCreateCredentialOnWindow;
68+
const isWebAuthnPlatformAuthenticatorSupported =
69+
Passkey.clerk.__internal_isWebAuthnPlatformAuthenticatorSupported ||
70+
isWebAuthnPlatformAuthenticatorSupportedOnWindow;
71+
5872
if (!isWebAuthnSupported()) {
5973
throw new ClerkWebAuthnError('Passkeys are not supported on this device.', {
6074
code: 'passkey_not_supported',
@@ -89,7 +103,6 @@ export class Passkey extends BaseResource implements PasskeyResource {
89103
if (!publicKeyCredential) {
90104
throw error;
91105
}
92-
93106
return this.attemptVerification(passkey.id, publicKeyCredential);
94107
}
95108

‎packages/clerk-js/src/core/resources/SignIn.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { ClerkWebAuthnError } from '@clerk/shared/error';
12
import { Poller } from '@clerk/shared/poller';
23
import { deepSnakeToCamel } from '@clerk/shared/underscore';
3-
import { isWebAuthnAutofillSupported, isWebAuthnSupported } from '@clerk/shared/webauthn';
4+
import {
5+
isWebAuthnAutofillSupported as isWebAuthnAutofillSupportedOnWindow,
6+
isWebAuthnSupported as isWebAuthnSupportedOnWindow,
7+
} from '@clerk/shared/webauthn';
48
import type {
59
AttemptFirstFactorParams,
610
AttemptSecondFactorParams,
@@ -41,10 +45,9 @@ import {
4145
windowNavigate,
4246
} from '../../utils';
4347
import {
44-
ClerkWebAuthnError,
4548
convertJSONToPublicKeyRequestOptions,
4649
serializePublicKeyCredentialAssertion,
47-
webAuthnGetCredential,
50+
webAuthnGetCredential as webAuthnGetCredentialOnWindow,
4851
} from '../../utils/passkeys';
4952
import { createValidatePassword } from '../../utils/passwords/password';
5053
import {
@@ -304,6 +307,12 @@ export class SignIn extends BaseResource implements SignInResource {
304307
* The UI should always prevent from this method being called if WebAuthn is not supported.
305308
* As a precaution we need to check if WebAuthn is supported.
306309
*/
310+
311+
const isWebAuthnSupported = SignIn.clerk.__internal_isWebAuthnSupported || isWebAuthnSupportedOnWindow;
312+
const webAuthnGetCredential = SignIn.clerk.__internal_getPublicCredentials || webAuthnGetCredentialOnWindow;
313+
const isWebAuthnAutofillSupported =
314+
SignIn.clerk.__internal_isWebAuthnAutofillSupported || isWebAuthnAutofillSupportedOnWindow;
315+
307316
if (!isWebAuthnSupported()) {
308317
throw new ClerkWebAuthnError('Passkeys are not supported', {
309318
code: 'passkey_not_supported',

‎packages/clerk-js/src/utils/passkeys.ts

+3-37
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { ClerkRuntimeError } from '@clerk/shared/error';
1+
import type { ClerkRuntimeError } from '@clerk/shared/error';
2+
import { ClerkWebAuthnError } from '@clerk/shared/error';
23
import type {
4+
CredentialReturn,
35
PublicKeyCredentialCreationOptionsJSON,
46
PublicKeyCredentialCreationOptionsWithoutExtensions,
57
PublicKeyCredentialRequestOptionsJSON,
@@ -8,33 +10,9 @@ import type {
810
PublicKeyCredentialWithAuthenticatorAttestationResponse,
911
} from '@clerk/types';
1012

11-
type CredentialReturn<T> =
12-
| {
13-
publicKeyCredential: T;
14-
error: null;
15-
}
16-
| {
17-
publicKeyCredential: null;
18-
error: ClerkWebAuthnError | Error;
19-
};
20-
2113
type WebAuthnCreateCredentialReturn = CredentialReturn<PublicKeyCredentialWithAuthenticatorAttestationResponse>;
2214
type WebAuthnGetCredentialReturn = CredentialReturn<PublicKeyCredentialWithAuthenticatorAssertionResponse>;
2315

24-
type ClerkWebAuthnErrorCode =
25-
// Generic
26-
| 'passkey_not_supported'
27-
| 'passkey_pa_not_supported'
28-
| 'passkey_invalid_rpID_or_domain'
29-
| 'passkey_already_exists'
30-
| 'passkey_operation_aborted'
31-
// Retrieval
32-
| 'passkey_retrieval_cancelled'
33-
| 'passkey_retrieval_failed'
34-
// Registration
35-
| 'passkey_registration_cancelled'
36-
| 'passkey_registration_failed';
37-
3816
class Base64Converter {
3917
static encode(buffer: ArrayBuffer): string {
4018
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
@@ -243,18 +221,6 @@ function serializePublicKeyCredentialAssertion(pkc: PublicKeyCredentialWithAuthe
243221
const bufferToBase64Url = Base64Converter.encode.bind(Base64Converter);
244222
const base64UrlToBuffer = Base64Converter.decode.bind(Base64Converter);
245223

246-
export class ClerkWebAuthnError extends ClerkRuntimeError {
247-
/**
248-
* A unique code identifying the error, can be used for localization.
249-
*/
250-
code: ClerkWebAuthnErrorCode;
251-
252-
constructor(message: string, { code }: { code: ClerkWebAuthnErrorCode }) {
253-
super(message, { code });
254-
this.code = code;
255-
}
256-
}
257-
258224
export {
259225
base64UrlToBuffer,
260226
bufferToBase64Url,

‎packages/expo-passkeys/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
import { ClerkProvider } from '@clerk/clerk-expo';
4242
import { passkeys } from '@clerk/clerk-expo/passkeys';
4343

44-
<ClerkProvider passkeys={passkeys}>{/* Your app here */}</ClerkProvider>;
44+
<ClerkProvider __experimental_passkeys={passkeys}>{/* Your app here */}</ClerkProvider>;
4545
```
4646

4747
### 🔑 Creating a Passkey

‎packages/expo-passkeys/example/App.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { ClerkProvider, SignedIn, SignedOut, useAuth, useSignIn, useUser } from '@clerk/clerk-expo';
2+
import { passkeys } from '@clerk/clerk-expo/passkeys';
23
import * as SecureStore from 'expo-secure-store';
34
import React from 'react';
45
import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
56

6-
import { passkeys } from '../src';
7-
87
const tokenCache = {
98
async getToken(key: string) {
109
try {
@@ -143,7 +142,7 @@ export default function App() {
143142
<ClerkProvider
144143
publishableKey={publishableKey}
145144
tokenCache={tokenCache}
146-
passkeys={passkeys}
145+
__experimental_passkeys={passkeys}
147146
>
148147
<View style={styles.container}>
149148
<SignedIn>

0 commit comments

Comments
 (0)
Please sign in to comment.