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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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