@clerk/expo-passkeys 1.0.0-canary.v5ad1f6c829a30e8efd2d4e7da310a9116c8005db

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.eslintrc.js +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +100 -0
  4. package/android/build.gradle +53 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/expo/modules/clerkexpopasskeys/ClerkExpoPasskeysExceptions.kt +140 -0
  7. package/android/src/main/java/expo/modules/clerkexpopasskeys/ClerkExpoPasskeysModule.kt +46 -0
  8. package/android/src/main/java/expo/modules/clerkexpopasskeys/CredentialManager.kt +36 -0
  9. package/app.json +10 -0
  10. package/build/ClerkExpoPasskeys.types.d.ts +61 -0
  11. package/build/ClerkExpoPasskeys.types.d.ts.map +1 -0
  12. package/build/ClerkExpoPasskeys.types.js +2 -0
  13. package/build/ClerkExpoPasskeys.types.js.map +1 -0
  14. package/build/ClerkExpoPasskeysModule.d.ts +3 -0
  15. package/build/ClerkExpoPasskeysModule.d.ts.map +1 -0
  16. package/build/ClerkExpoPasskeysModule.js +5 -0
  17. package/build/ClerkExpoPasskeysModule.js.map +1 -0
  18. package/build/ClerkExpoPasskeysModule.web.d.ts +3 -0
  19. package/build/ClerkExpoPasskeysModule.web.d.ts.map +1 -0
  20. package/build/ClerkExpoPasskeysModule.web.js +2 -0
  21. package/build/ClerkExpoPasskeysModule.web.js.map +1 -0
  22. package/build/index.d.ts +13 -0
  23. package/build/index.d.ts.map +1 -0
  24. package/build/index.js +143 -0
  25. package/build/index.js.map +1 -0
  26. package/build/utils.d.ts +13 -0
  27. package/build/utils.d.ts.map +1 -0
  28. package/build/utils.js +81 -0
  29. package/build/utils.js.map +1 -0
  30. package/expo-module.config.json +9 -0
  31. package/ios/AccountManager.swift +182 -0
  32. package/ios/ClerkExpoPasskeys.podspec +27 -0
  33. package/ios/ClerkExpoPasskeysModule.swift +27 -0
  34. package/ios/Helpers.swift +56 -0
  35. package/package.json +47 -0
  36. package/src/ClerkExpoPasskeys.types.ts +92 -0
  37. package/src/ClerkExpoPasskeysModule.ts +5 -0
  38. package/src/ClerkExpoPasskeysModule.web.ts +1 -0
  39. package/src/index.ts +194 -0
  40. package/src/utils.ts +107 -0
  41. package/tsconfig.json +9 -0
@@ -0,0 +1,92 @@
1
+ import type {
2
+ CredentialReturn,
3
+ PublicKeyCredentialCreationOptionsWithoutExtensions,
4
+ PublicKeyCredentialRequestOptionsWithoutExtensions,
5
+ PublicKeyCredentialWithAuthenticatorAssertionResponse as ClerkPublicKeyCredentialWithAuthenticatorAssertionResponse,
6
+ PublicKeyCredentialWithAuthenticatorAttestationResponse as ClerkPublicKeyCredentialWithAuthenticatorAttestationResponse,
7
+ } from '@clerk/types';
8
+
9
+ export type {
10
+ PublicKeyCredentialRequestOptionsWithoutExtensions,
11
+ PublicKeyCredentialCreationOptionsWithoutExtensions,
12
+ CredentialReturn,
13
+ };
14
+
15
+ type AuthenticatorTransportFuture = 'ble' | 'cable' | 'hybrid' | 'internal' | 'nfc' | 'smart-card' | 'usb';
16
+
17
+ interface PublicKeyCredentialDescriptorJSON {
18
+ id: string;
19
+ type: PublicKeyCredentialType;
20
+ transports?: AuthenticatorTransportFuture[];
21
+ }
22
+
23
+ // The serialized JSON to send to "create" native module
24
+ export type SerializedPublicKeyCredentialCreationOptions = Pick<
25
+ PublicKeyCredentialCreationOptionsWithoutExtensions,
26
+ 'authenticatorSelection' | 'pubKeyCredParams'
27
+ > & {
28
+ rp: { id: string; name: string };
29
+ user: {
30
+ id: string;
31
+ displayName: string;
32
+ name: string;
33
+ };
34
+ challenge: string;
35
+ excludeCredentials?: PublicKeyCredentialDescriptorJSON[];
36
+ };
37
+
38
+ // The serialized JSON to send to "get" native module
39
+ export type SerializedPublicKeyCredentialRequestOptions = Omit<
40
+ PublicKeyCredentialRequestOptionsWithoutExtensions,
41
+ 'challenge'
42
+ > & {
43
+ challenge: string;
44
+ };
45
+
46
+ // The return type from the "get" native module.
47
+ export interface AuthenticationResponseJSON {
48
+ id: string;
49
+ rawId: string;
50
+ response: AuthenticatorAssertionResponseJSON;
51
+ authenticatorAttachment?: AuthenticatorAttachment;
52
+ clientExtensionResults: AuthenticationExtensionsClientOutputs;
53
+ type: PublicKeyCredentialType;
54
+ }
55
+
56
+ // The serialized response of the native module "create" response to be send back to clerk
57
+ export type PublicKeyCredentialWithAuthenticatorAttestationResponse =
58
+ ClerkPublicKeyCredentialWithAuthenticatorAttestationResponse & {
59
+ toJSON: () => any;
60
+ };
61
+
62
+ interface AuthenticatorAssertionResponseJSON {
63
+ clientDataJSON: string;
64
+ authenticatorData: string;
65
+ signature: string;
66
+ userHandle?: string;
67
+ }
68
+
69
+ // The serialized response of the native module "get" response to be send back to clerk
70
+ export type PublicKeyCredentialWithAuthenticatorAssertionResponse =
71
+ ClerkPublicKeyCredentialWithAuthenticatorAssertionResponse & {
72
+ toJSON: () => any;
73
+ };
74
+
75
+ interface AuthenticatorAttestationResponseJSON {
76
+ clientDataJSON: string;
77
+ attestationObject: string;
78
+ authenticatorData?: string;
79
+ transports?: AuthenticatorTransportFuture[];
80
+ publicKeyAlgorithm?: COSEAlgorithmIdentifier;
81
+ publicKey?: string;
82
+ }
83
+
84
+ // The type is returned from from native module "create" response
85
+ export interface RegistrationResponseJSON {
86
+ id: string;
87
+ rawId: string;
88
+ response: AuthenticatorAttestationResponseJSON;
89
+ authenticatorAttachment?: AuthenticatorAttachment;
90
+ clientExtensionResults: AuthenticationExtensionsClientOutputs;
91
+ type: PublicKeyCredentialType;
92
+ }
@@ -0,0 +1,5 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+
3
+ // It loads the native module object from the JSI or falls back to
4
+ // the bridge module (from NativeModulesProxy) if the remote debugger is on.
5
+ export default requireNativeModule('ClerkExpoPasskeys');
@@ -0,0 +1 @@
1
+ export default {};
package/src/index.ts ADDED
@@ -0,0 +1,194 @@
1
+ import { Platform } from 'react-native';
2
+
3
+ import type {
4
+ AuthenticationResponseJSON,
5
+ CredentialReturn,
6
+ PublicKeyCredentialCreationOptionsWithoutExtensions,
7
+ PublicKeyCredentialRequestOptionsWithoutExtensions,
8
+ PublicKeyCredentialWithAuthenticatorAssertionResponse,
9
+ PublicKeyCredentialWithAuthenticatorAttestationResponse,
10
+ RegistrationResponseJSON,
11
+ SerializedPublicKeyCredentialCreationOptions,
12
+ SerializedPublicKeyCredentialRequestOptions,
13
+ } from './ClerkExpoPasskeys.types';
14
+ import ClerkExpoPasskeys from './ClerkExpoPasskeysModule';
15
+ import {
16
+ arrayBufferToBase64Url,
17
+ base64urlToArrayBuffer,
18
+ ClerkWebAuthnError,
19
+ encodeBase64Url,
20
+ mapNativeErrorToClerkWebAuthnErrorCode,
21
+ toArrayBuffer,
22
+ } from './utils';
23
+
24
+ const makeSerializedCreateResponse = (
25
+ publicCredential: RegistrationResponseJSON,
26
+ ): PublicKeyCredentialWithAuthenticatorAttestationResponse => ({
27
+ id: publicCredential.id,
28
+ rawId: base64urlToArrayBuffer(publicCredential.rawId),
29
+ response: {
30
+ getTransports: () => publicCredential?.response?.transports as string[],
31
+ attestationObject: base64urlToArrayBuffer(publicCredential.response.attestationObject),
32
+ clientDataJSON: base64urlToArrayBuffer(publicCredential.response.clientDataJSON),
33
+ },
34
+ type: publicCredential.type,
35
+ authenticatorAttachment: publicCredential.authenticatorAttachment || null,
36
+ toJSON: () => publicCredential,
37
+ });
38
+
39
+ export async function create(
40
+ publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions,
41
+ ): Promise<CredentialReturn<PublicKeyCredentialWithAuthenticatorAttestationResponse>> {
42
+ if (!publicKey || !publicKey.rp.id) {
43
+ throw new Error('Invalid public key or RpID');
44
+ }
45
+
46
+ const createOptions: SerializedPublicKeyCredentialCreationOptions = {
47
+ rp: { id: publicKey.rp.id, name: publicKey.rp.name },
48
+ user: {
49
+ id: encodeBase64Url(toArrayBuffer(publicKey.user.id)),
50
+ displayName: publicKey.user.displayName,
51
+ name: publicKey.user.name,
52
+ },
53
+ pubKeyCredParams: publicKey.pubKeyCredParams,
54
+ challenge: encodeBase64Url(toArrayBuffer(publicKey.challenge)),
55
+ authenticatorSelection: {
56
+ authenticatorAttachment: 'platform',
57
+ requireResidentKey: true,
58
+ residentKey: 'required',
59
+ userVerification: 'required',
60
+ },
61
+ excludeCredentials: publicKey.excludeCredentials.map(c => ({
62
+ type: 'public-key',
63
+ id: encodeBase64Url(toArrayBuffer(c.id)),
64
+ })),
65
+ };
66
+
67
+ const createPasskeyModule = Platform.select({
68
+ android: async () => ClerkExpoPasskeys.create(JSON.stringify(createOptions)),
69
+ ios: async () =>
70
+ ClerkExpoPasskeys.create(
71
+ createOptions.challenge,
72
+ createOptions.rp.id,
73
+ createOptions.user.id,
74
+ createOptions.user.displayName,
75
+ ),
76
+ default: null,
77
+ });
78
+
79
+ if (!createPasskeyModule) {
80
+ throw new Error('Platform not supported');
81
+ }
82
+
83
+ try {
84
+ const response = await createPasskeyModule();
85
+ return {
86
+ publicKeyCredential: makeSerializedCreateResponse(typeof response === 'string' ? JSON.parse(response) : response),
87
+ error: null,
88
+ };
89
+ } catch (error) {
90
+ return {
91
+ publicKeyCredential: null,
92
+ error: mapNativeErrorToClerkWebAuthnErrorCode(error.code, error.message, 'create'),
93
+ };
94
+ }
95
+ }
96
+
97
+ const makeSerializedGetResponse = (
98
+ publicKeyCredential: AuthenticationResponseJSON,
99
+ ): PublicKeyCredentialWithAuthenticatorAssertionResponse => {
100
+ return {
101
+ type: publicKeyCredential.type,
102
+ id: publicKeyCredential.id,
103
+ rawId: base64urlToArrayBuffer(publicKeyCredential.rawId),
104
+ authenticatorAttachment: publicKeyCredential?.authenticatorAttachment || null,
105
+ response: {
106
+ clientDataJSON: base64urlToArrayBuffer(publicKeyCredential.response.clientDataJSON),
107
+ authenticatorData: base64urlToArrayBuffer(publicKeyCredential.response.authenticatorData),
108
+ signature: base64urlToArrayBuffer(publicKeyCredential.response.signature),
109
+ userHandle: publicKeyCredential?.response.userHandle
110
+ ? base64urlToArrayBuffer(publicKeyCredential?.response.userHandle)
111
+ : null,
112
+ },
113
+ toJSON: () => publicKeyCredential,
114
+ };
115
+ };
116
+
117
+ export async function get({
118
+ publicKeyOptions,
119
+ }: {
120
+ publicKeyOptions: PublicKeyCredentialRequestOptionsWithoutExtensions;
121
+ }): Promise<CredentialReturn<PublicKeyCredentialWithAuthenticatorAssertionResponse>> {
122
+ if (!publicKeyOptions) {
123
+ throw new Error('publicKeyCredential has not been provided');
124
+ }
125
+
126
+ const serializedPublicCredential: SerializedPublicKeyCredentialRequestOptions = {
127
+ ...publicKeyOptions,
128
+ challenge: arrayBufferToBase64Url(publicKeyOptions.challenge),
129
+ };
130
+
131
+ const getPasskeyModule = Platform.select({
132
+ android: async () => ClerkExpoPasskeys.get(JSON.stringify(serializedPublicCredential)),
133
+ ios: async () => ClerkExpoPasskeys.get(serializedPublicCredential.challenge, serializedPublicCredential.rpId),
134
+ default: null,
135
+ });
136
+
137
+ if (!getPasskeyModule) {
138
+ return {
139
+ publicKeyCredential: null,
140
+ error: new ClerkWebAuthnError('Platform is not supported', { code: 'passkey_not_supported' }),
141
+ };
142
+ }
143
+
144
+ try {
145
+ const response = await getPasskeyModule();
146
+ return {
147
+ publicKeyCredential: makeSerializedGetResponse(typeof response === 'string' ? JSON.parse(response) : response),
148
+ error: null,
149
+ };
150
+ } catch (error) {
151
+ return {
152
+ publicKeyCredential: null,
153
+ error: mapNativeErrorToClerkWebAuthnErrorCode(error.code, error.message, 'get'),
154
+ };
155
+ }
156
+ }
157
+
158
+ const ANDROID_9 = 28;
159
+ const IOS_15 = 15;
160
+
161
+ export function isSupported() {
162
+ if (Platform.OS === 'android') {
163
+ return Platform.Version >= ANDROID_9;
164
+ }
165
+
166
+ if (Platform.OS === 'ios') {
167
+ return parseInt(Platform.Version, 10) > IOS_15;
168
+ }
169
+
170
+ return false;
171
+ }
172
+
173
+ // FIX:The autofill function has been implemented for iOS only, but the pop-up is not showing up.
174
+ // This seems to be an issue with Expo that we haven't been able to resolve yet.
175
+ // Further investigation and possibly reaching out to Expo support may be necessary.
176
+
177
+ // async function autofill(): Promise<AuthenticationResponseJSON | null> {
178
+ // if (Platform.OS === 'android') {
179
+ // throw new Error('Not supported');
180
+ // } else if (Platform.OS === 'ios') {
181
+ // throw new Error('Not supported');
182
+ // } else {
183
+ // throw new Error('Not supported');
184
+ // }
185
+ // }
186
+
187
+ export const passkeys = {
188
+ create,
189
+ get,
190
+ isSupported,
191
+ isAutoFillSupported: () => {
192
+ throw new Error('Not supported');
193
+ },
194
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { ClerkWebAuthnError } from '@clerk/shared/error';
2
+ import { Buffer } from 'buffer';
3
+ export { ClerkWebAuthnError };
4
+
5
+ export function encodeBase64(data: ArrayLike<number> | ArrayBufferLike) {
6
+ return btoa(String.fromCharCode(...new Uint8Array(data)));
7
+ }
8
+
9
+ export function encodeBase64Url(data: ArrayLike<number> | ArrayBufferLike) {
10
+ return encodeBase64(data).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
11
+ }
12
+
13
+ export function decodeBase64Url(data: string) {
14
+ return decodeBase64(data.replaceAll('-', '+').replaceAll('_', '/'));
15
+ }
16
+
17
+ export function decodeToken(data: string) {
18
+ const base64 = data.replace(/-/g, '+').replace(/_/g, '/');
19
+ const jsonPayload = decodeURIComponent(
20
+ atob(base64)
21
+ .split('')
22
+ .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
23
+ .join(''),
24
+ );
25
+ return JSON.parse(jsonPayload);
26
+ }
27
+
28
+ export function decodeBase64(data: string) {
29
+ return Uint8Array.from(atob(data).split(''), x => x.charCodeAt(0));
30
+ }
31
+
32
+ export function utf8Decode(buffer: BufferSource) {
33
+ const textDecoder = new TextDecoder();
34
+ return textDecoder.decode(buffer);
35
+ }
36
+
37
+ export function toArrayBuffer(bufferSource: BufferSource) {
38
+ if (bufferSource instanceof ArrayBuffer) {
39
+ return bufferSource; // It's already an ArrayBuffer
40
+ } else if (ArrayBuffer.isView(bufferSource)) {
41
+ return bufferSource.buffer; // Extract the ArrayBuffer from the typed array
42
+ } else {
43
+ throw new TypeError('Expected a BufferSource, but received an incompatible type.');
44
+ }
45
+ }
46
+
47
+ export function base64urlToArrayBuffer(base64url: string) {
48
+ const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
49
+
50
+ const binaryString = Buffer.from(base64, 'base64').toString('binary');
51
+
52
+ const len = binaryString.length;
53
+ const buffer = new ArrayBuffer(len);
54
+ const uintArray = new Uint8Array(buffer);
55
+
56
+ for (let i = 0; i < len; i++) {
57
+ uintArray[i] = binaryString.charCodeAt(i);
58
+ }
59
+
60
+ return buffer;
61
+ }
62
+
63
+ export function arrayBufferToBase64Url(buffer) {
64
+ const bytes = new Uint8Array(buffer);
65
+ let binary = '';
66
+
67
+ for (let i = 0; i < bytes.length; i++) {
68
+ binary += String.fromCharCode(bytes[i]);
69
+ }
70
+
71
+ const base64String = btoa(binary);
72
+
73
+ const base64Url = base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
74
+
75
+ return base64Url;
76
+ }
77
+
78
+ export function mapNativeErrorToClerkWebAuthnErrorCode(
79
+ code: string,
80
+ message: string,
81
+ action: 'get' | 'create',
82
+ ): ClerkWebAuthnError {
83
+ if (code === '1000' || code === '1004' || code === 'CreatePublicKeyCredentialDomException') {
84
+ return new ClerkWebAuthnError(message, {
85
+ code: action === 'create' ? 'passkey_registration_failed' : 'passkey_retrieval_failed',
86
+ });
87
+ }
88
+ if (
89
+ code === '1001' ||
90
+ code === 'CreateCredentialCancellationException' ||
91
+ code === 'GetCredentialCancellationException'
92
+ ) {
93
+ return new ClerkWebAuthnError(message, { code: 'passkey_registration_cancelled' });
94
+ }
95
+
96
+ if (code === '1002') {
97
+ return new ClerkWebAuthnError(message, { code: 'passkey_invalid_rpID_or_domain' });
98
+ }
99
+
100
+ if (code === '1003' || code === 'CreateCredentialInterruptedException') {
101
+ return new ClerkWebAuthnError(message, { code: 'passkey_operation_aborted' });
102
+ }
103
+
104
+ return new ClerkWebAuthnError(message, {
105
+ code: action === 'create' ? 'passkey_registration_failed' : 'passkey_retrieval_failed',
106
+ });
107
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ // @generated by expo-module-scripts
2
+ {
3
+ "extends": "expo-module-scripts/tsconfig.base",
4
+ "compilerOptions": {
5
+ "outDir": "./build"
6
+ },
7
+ "include": ["./src"],
8
+ "exclude": ["**/__mocks__/*", "**/__tests__/*"]
9
+ }