Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6797eab
move CredentialReturn type to passskeys type
AlexNti Oct 17, 2024
cf8fe36
feat(expo): Allow clerk provider to receive passkey functions as props
AlexNti Oct 17, 2024
62c0e45
feat(expo): Add _unstable functions to override the default passkeys …
AlexNti Oct 17, 2024
5092931
consume CredentialReturn from types
AlexNti Oct 17, 2024
9472dc5
feat: Overide webAuthnCreateCredential webAuthnGetCredential isWebAut…
AlexNti Oct 17, 2024
e0e0683
chore(expo): Add optional operator to passkeysFunc
AlexNti Oct 17, 2024
5f52fb8
Add changeset
anagstef Oct 18, 2024
4eb00be
chore: Rename passkeysFunc => passkeys
AlexNti Oct 23, 2024
fa93e18
chore: Simplify the code by removing the if statements for injected f…
AlexNti Oct 23, 2024
62b5327
chore: Export ClerkWebAuthnError from shared error package
AlexNti Oct 23, 2024
d7690e9
chore: change type of passkeys.get function
AlexNti Oct 23, 2024
cbf33db
chore: Fix wrong type at create clerk instance
AlexNti Oct 23, 2024
0ea90e3
chore: Fix missing props at __unstable__getPublicCredentials
AlexNti Oct 23, 2024
8f749f9
chore: Add changeset
AlexNti Oct 24, 2024
537e0b5
chore: Update changesets
AlexNti Oct 24, 2024
172d7ce
chore: Update expo-passkeys to use current clerk versions
AlexNti Nov 1, 2024
476c659
chore: Update package-lock
AlexNti Nov 5, 2024
723fb44
chore(clerk-expo): Update clerk expo to expose passkeys path
AlexNti Nov 1, 2024
8b9eaf2
chore: Add patch version of @clerk/expo-passkeys at changesets
AlexNti Nov 1, 2024
46a6f19
chore: Rename __unstable__ => __internal__
AlexNti Nov 1, 2024
3527ac5
Update .changeset/late-camels-talk.md
AlexNti Nov 1, 2024
3bf07f2
chore: Change text on changeset
AlexNti Nov 1, 2024
7a46f80
chore: Address pr comments regarding naming of isWebAuthnSupported
AlexNti Nov 1, 2024
22f0396
chore: Rename passkeys => __experimental__passkeys on clerk provider
AlexNti Nov 1, 2024
5aaf1b3
chore: Address PR comments about removing prefix _ from web auth
AlexNti Nov 1, 2024
ace9836
chore: Fix type error
AlexNti Nov 1, 2024
c429666
chore: Fix failing lint
AlexNti Nov 1, 2024
3503756
chore: Rename experimental__ to use one underscore
AlexNti Nov 4, 2024
295daa7
chore: Address pr comment
AlexNti Nov 4, 2024
24d58bf
chore: Update expo passkeys deps
AlexNti Nov 4, 2024
ed3e6c1
chore: Attepmt to fix build error
AlexNti Nov 4, 2024
bc8536f
chore: Update scripts in expo-passkeys
AlexNti Nov 4, 2024
2f3f982
attempt to fix build
Nov 4, 2024
2e0e211
chore: Add descriptive commend for __experimental_passkeys
AlexNti Nov 4, 2024
f79ba91
chore: Fix build error
AlexNti Nov 4, 2024
dbc115e
chore: Update expo-passkeys deps
AlexNti Nov 5, 2024
29e9dcc
chore: Increase version of clerk/shared
AlexNti Nov 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .changeset/late-camels-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
"@clerk/clerk-js": minor
"@clerk/shared": minor
"@clerk/types": minor
"@clerk/clerk-expo": minor
"@clerk/expo-passkeys": patch
---

Introduce experimental support for passkeys in Expo (iOS, Android, and Web).

To use passkeys in Expo projects, pass the `__experimental_passkeys` object, which can be imported from `@clerk/clerk-expo/passkeys`, to the `ClerkProvider` component:

```tsx

import { ClerkProvider } from '@clerk/clerk-expo';
import { passkeys } from '@clerk/clerk-expo/passkeys';

<ClerkProvider __experimental_passkeys={passkeys}>
{/* Your app here */}
</ClerkProvider>
```

The API for using passkeys in Expo projects is the same as the one used in web apps:

```tsx
// passkey creation
const { user } = useUser();

const handleCreatePasskey = async () => {
if (!user) return;
try {
return await user.createPasskey();
} catch (e: any) {
// handle error
}
};


// passkey authentication
const { signIn, setActive } = useSignIn();

const handlePasskeySignIn = async () => {
try {
const signInResponse = await signIn.authenticateWithPasskey();
await setActive({ session: signInResponse.createdSessionId });
} catch (err: any) {
//handle error
}
};
```
46 changes: 3 additions & 43 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
ClientResource,
CreateOrganizationParams,
CreateOrganizationProps,
CredentialReturn,
DomainOrProxyUrl,
EnvironmentJSON,
EnvironmentResource,
Expand All @@ -36,6 +37,10 @@ import type {
OrganizationProfileProps,
OrganizationResource,
OrganizationSwitcherProps,
PublicKeyCredentialCreationOptionsWithoutExtensions,
PublicKeyCredentialRequestOptionsWithoutExtensions,
PublicKeyCredentialWithAuthenticatorAssertionResponse,
PublicKeyCredentialWithAuthenticatorAttestationResponse,
RedirectOptions,
Resources,
SDKMetadata,
Expand Down Expand Up @@ -185,6 +190,24 @@ export class Clerk implements ClerkInterface {
#pageLifecycle: ReturnType<typeof createPageLifecycle> | null = null;
#touchThrottledUntil = 0;

public __internal_createPublicCredentials:
| ((
publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions,
) => Promise<CredentialReturn<PublicKeyCredentialWithAuthenticatorAttestationResponse>>)
| undefined;

public __internal_getPublicCredentials:
| (({
publicKeyOptions,
}: {
publicKeyOptions: PublicKeyCredentialRequestOptionsWithoutExtensions;
}) => Promise<CredentialReturn<PublicKeyCredentialWithAuthenticatorAssertionResponse>>)
| undefined;

public __internal_isWebAuthnSupported: (() => boolean) | undefined;
public __internal_isWebAuthnAutofillSupported: (() => Promise<boolean>) | undefined;
public __internal_isWebAuthnPlatformAuthenticatorSupported: (() => Promise<boolean>) | undefined;

get publishableKey(): string {
return this.#publishableKey;
}
Expand Down
19 changes: 16 additions & 3 deletions packages/clerk-js/src/core/resources/Passkey.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { isWebAuthnPlatformAuthenticatorSupported, isWebAuthnSupported } from '@clerk/shared/webauthn';
import { ClerkWebAuthnError } from '@clerk/shared/error';
import {
isWebAuthnPlatformAuthenticatorSupported as isWebAuthnPlatformAuthenticatorSupportedOnWindow,
isWebAuthnSupported as isWebAuthnSupportedOnWindow,
} from '@clerk/shared/webauthn';
import type {
DeletedObjectJSON,
DeletedObjectResource,
Expand All @@ -10,7 +14,10 @@ import type {
} from '@clerk/types';

import { unixEpochToDate } from '../../utils/date';
import { ClerkWebAuthnError, serializePublicKeyCredential, webAuthnCreateCredential } from '../../utils/passkeys';
import {
serializePublicKeyCredential,
webAuthnCreateCredential as webAuthnCreateCredentialOnWindow,
} from '../../utils/passkeys';
import { clerkMissingWebAuthnPublicKeyOptions } from '../errors';
import { BaseResource, DeletedObject, PasskeyVerification } from './internal';

Expand Down Expand Up @@ -55,6 +62,13 @@ export class Passkey extends BaseResource implements PasskeyResource {
* The UI should always prevent from this method being called if WebAuthn is not supported.
* As a precaution we need to check if WebAuthn is supported.
*/
const isWebAuthnSupported = Passkey.clerk.__internal_isWebAuthnSupported || isWebAuthnSupportedOnWindow;
const webAuthnCreateCredential =
Passkey.clerk.__internal_createPublicCredentials || webAuthnCreateCredentialOnWindow;
const isWebAuthnPlatformAuthenticatorSupported =
Passkey.clerk.__internal_isWebAuthnPlatformAuthenticatorSupported ||
isWebAuthnPlatformAuthenticatorSupportedOnWindow;

if (!isWebAuthnSupported()) {
throw new ClerkWebAuthnError('Passkeys are not supported on this device.', {
code: 'passkey_not_supported',
Expand Down Expand Up @@ -89,7 +103,6 @@ export class Passkey extends BaseResource implements PasskeyResource {
if (!publicKeyCredential) {
throw error;
}

return this.attemptVerification(passkey.id, publicKeyCredential);
}

Expand Down
15 changes: 12 additions & 3 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ClerkWebAuthnError } from '@clerk/shared/error';
import { Poller } from '@clerk/shared/poller';
import { deepSnakeToCamel } from '@clerk/shared/underscore';
import { isWebAuthnAutofillSupported, isWebAuthnSupported } from '@clerk/shared/webauthn';
import {
isWebAuthnAutofillSupported as isWebAuthnAutofillSupportedOnWindow,
isWebAuthnSupported as isWebAuthnSupportedOnWindow,
} from '@clerk/shared/webauthn';
import type {
AttemptFirstFactorParams,
AttemptSecondFactorParams,
Expand Down Expand Up @@ -41,10 +45,9 @@ import {
windowNavigate,
} from '../../utils';
import {
ClerkWebAuthnError,
convertJSONToPublicKeyRequestOptions,
serializePublicKeyCredentialAssertion,
webAuthnGetCredential,
webAuthnGetCredential as webAuthnGetCredentialOnWindow,
} from '../../utils/passkeys';
import { createValidatePassword } from '../../utils/passwords/password';
import {
Expand Down Expand Up @@ -304,6 +307,12 @@ export class SignIn extends BaseResource implements SignInResource {
* The UI should always prevent from this method being called if WebAuthn is not supported.
* As a precaution we need to check if WebAuthn is supported.
*/

const isWebAuthnSupported = SignIn.clerk.__internal_isWebAuthnSupported || isWebAuthnSupportedOnWindow;
const webAuthnGetCredential = SignIn.clerk.__internal_getPublicCredentials || webAuthnGetCredentialOnWindow;
const isWebAuthnAutofillSupported =
SignIn.clerk.__internal_isWebAuthnAutofillSupported || isWebAuthnAutofillSupportedOnWindow;

if (!isWebAuthnSupported()) {
throw new ClerkWebAuthnError('Passkeys are not supported', {
code: 'passkey_not_supported',
Expand Down
40 changes: 3 additions & 37 deletions packages/clerk-js/src/utils/passkeys.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ClerkRuntimeError } from '@clerk/shared/error';
import type { ClerkRuntimeError } from '@clerk/shared/error';
import { ClerkWebAuthnError } from '@clerk/shared/error';
import type {
CredentialReturn,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialCreationOptionsWithoutExtensions,
PublicKeyCredentialRequestOptionsJSON,
Expand All @@ -8,33 +10,9 @@ import type {
PublicKeyCredentialWithAuthenticatorAttestationResponse,
} from '@clerk/types';

type CredentialReturn<T> =
| {
publicKeyCredential: T;
error: null;
}
| {
publicKeyCredential: null;
error: ClerkWebAuthnError | Error;
};

type WebAuthnCreateCredentialReturn = CredentialReturn<PublicKeyCredentialWithAuthenticatorAttestationResponse>;
type WebAuthnGetCredentialReturn = CredentialReturn<PublicKeyCredentialWithAuthenticatorAssertionResponse>;

type ClerkWebAuthnErrorCode =
// Generic
| 'passkey_not_supported'
| 'passkey_pa_not_supported'
| 'passkey_invalid_rpID_or_domain'
| 'passkey_already_exists'
| 'passkey_operation_aborted'
// Retrieval
| 'passkey_retrieval_cancelled'
| 'passkey_retrieval_failed'
// Registration
| 'passkey_registration_cancelled'
| 'passkey_registration_failed';

class Base64Converter {
static encode(buffer: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
Expand Down Expand Up @@ -243,18 +221,6 @@ function serializePublicKeyCredentialAssertion(pkc: PublicKeyCredentialWithAuthe
const bufferToBase64Url = Base64Converter.encode.bind(Base64Converter);
const base64UrlToBuffer = Base64Converter.decode.bind(Base64Converter);

export class ClerkWebAuthnError extends ClerkRuntimeError {
/**
* A unique code identifying the error, can be used for localization.
*/
code: ClerkWebAuthnErrorCode;

constructor(message: string, { code }: { code: ClerkWebAuthnErrorCode }) {
super(message, { code });
this.code = code;
}
}

export {
base64UrlToBuffer,
bufferToBase64Url,
Expand Down
2 changes: 1 addition & 1 deletion packages/expo-passkeys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
import { ClerkProvider } from '@clerk/clerk-expo';
import { passkeys } from '@clerk/clerk-expo/passkeys';

<ClerkProvider passkeys={passkeys}>{/* Your app here */}</ClerkProvider>;
<ClerkProvider __experimental_passkeys={passkeys}>{/* Your app here */}</ClerkProvider>;
```

### 🔑 Creating a Passkey
Expand Down
5 changes: 2 additions & 3 deletions packages/expo-passkeys/example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { ClerkProvider, SignedIn, SignedOut, useAuth, useSignIn, useUser } from '@clerk/clerk-expo';
import { passkeys } from '@clerk/clerk-expo/passkeys';
import * as SecureStore from 'expo-secure-store';
import React from 'react';
import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';

import { passkeys } from '../src';

const tokenCache = {
async getToken(key: string) {
try {
Expand Down Expand Up @@ -143,7 +142,7 @@ export default function App() {
<ClerkProvider
publishableKey={publishableKey}
tokenCache={tokenCache}
passkeys={passkeys}
__experimental_passkeys={passkeys}
>
<View style={styles.container}>
<SignedIn>
Expand Down
Loading