@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.
- package/.eslintrc.js +7 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/android/build.gradle +53 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/clerkexpopasskeys/ClerkExpoPasskeysExceptions.kt +140 -0
- package/android/src/main/java/expo/modules/clerkexpopasskeys/ClerkExpoPasskeysModule.kt +46 -0
- package/android/src/main/java/expo/modules/clerkexpopasskeys/CredentialManager.kt +36 -0
- package/app.json +10 -0
- package/build/ClerkExpoPasskeys.types.d.ts +61 -0
- package/build/ClerkExpoPasskeys.types.d.ts.map +1 -0
- package/build/ClerkExpoPasskeys.types.js +2 -0
- package/build/ClerkExpoPasskeys.types.js.map +1 -0
- package/build/ClerkExpoPasskeysModule.d.ts +3 -0
- package/build/ClerkExpoPasskeysModule.d.ts.map +1 -0
- package/build/ClerkExpoPasskeysModule.js +5 -0
- package/build/ClerkExpoPasskeysModule.js.map +1 -0
- package/build/ClerkExpoPasskeysModule.web.d.ts +3 -0
- package/build/ClerkExpoPasskeysModule.web.d.ts.map +1 -0
- package/build/ClerkExpoPasskeysModule.web.js +2 -0
- package/build/ClerkExpoPasskeysModule.web.js.map +1 -0
- package/build/index.d.ts +13 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +143 -0
- package/build/index.js.map +1 -0
- package/build/utils.d.ts +13 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +81 -0
- package/build/utils.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AccountManager.swift +182 -0
- package/ios/ClerkExpoPasskeys.podspec +27 -0
- package/ios/ClerkExpoPasskeysModule.swift +27 -0
- package/ios/Helpers.swift +56 -0
- package/package.json +47 -0
- package/src/ClerkExpoPasskeys.types.ts +92 -0
- package/src/ClerkExpoPasskeysModule.ts +5 -0
- package/src/ClerkExpoPasskeysModule.web.ts +1 -0
- package/src/index.ts +194 -0
- package/src/utils.ts +107 -0
- package/tsconfig.json +9 -0
package/build/index.js
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
import { Platform } from 'react-native';
|
2
|
+
import ClerkExpoPasskeys from './ClerkExpoPasskeysModule';
|
3
|
+
import { arrayBufferToBase64Url, base64urlToArrayBuffer, ClerkWebAuthnError, encodeBase64Url, mapNativeErrorToClerkWebAuthnErrorCode, toArrayBuffer, } from './utils';
|
4
|
+
const makeSerializedCreateResponse = (publicCredential) => ({
|
5
|
+
id: publicCredential.id,
|
6
|
+
rawId: base64urlToArrayBuffer(publicCredential.rawId),
|
7
|
+
response: {
|
8
|
+
getTransports: () => publicCredential?.response?.transports,
|
9
|
+
attestationObject: base64urlToArrayBuffer(publicCredential.response.attestationObject),
|
10
|
+
clientDataJSON: base64urlToArrayBuffer(publicCredential.response.clientDataJSON),
|
11
|
+
},
|
12
|
+
type: publicCredential.type,
|
13
|
+
authenticatorAttachment: publicCredential.authenticatorAttachment || null,
|
14
|
+
toJSON: () => publicCredential,
|
15
|
+
});
|
16
|
+
export async function create(publicKey) {
|
17
|
+
if (!publicKey || !publicKey.rp.id) {
|
18
|
+
throw new Error('Invalid public key or RpID');
|
19
|
+
}
|
20
|
+
const createOptions = {
|
21
|
+
rp: { id: publicKey.rp.id, name: publicKey.rp.name },
|
22
|
+
user: {
|
23
|
+
id: encodeBase64Url(toArrayBuffer(publicKey.user.id)),
|
24
|
+
displayName: publicKey.user.displayName,
|
25
|
+
name: publicKey.user.name,
|
26
|
+
},
|
27
|
+
pubKeyCredParams: publicKey.pubKeyCredParams,
|
28
|
+
challenge: encodeBase64Url(toArrayBuffer(publicKey.challenge)),
|
29
|
+
authenticatorSelection: {
|
30
|
+
authenticatorAttachment: 'platform',
|
31
|
+
requireResidentKey: true,
|
32
|
+
residentKey: 'required',
|
33
|
+
userVerification: 'required',
|
34
|
+
},
|
35
|
+
excludeCredentials: publicKey.excludeCredentials.map(c => ({
|
36
|
+
type: 'public-key',
|
37
|
+
id: encodeBase64Url(toArrayBuffer(c.id)),
|
38
|
+
})),
|
39
|
+
};
|
40
|
+
const createPasskeyModule = Platform.select({
|
41
|
+
android: async () => ClerkExpoPasskeys.create(JSON.stringify(createOptions)),
|
42
|
+
ios: async () => ClerkExpoPasskeys.create(createOptions.challenge, createOptions.rp.id, createOptions.user.id, createOptions.user.displayName),
|
43
|
+
default: null,
|
44
|
+
});
|
45
|
+
if (!createPasskeyModule) {
|
46
|
+
throw new Error('Platform not supported');
|
47
|
+
}
|
48
|
+
try {
|
49
|
+
const response = await createPasskeyModule();
|
50
|
+
return {
|
51
|
+
publicKeyCredential: makeSerializedCreateResponse(typeof response === 'string' ? JSON.parse(response) : response),
|
52
|
+
error: null,
|
53
|
+
};
|
54
|
+
}
|
55
|
+
catch (error) {
|
56
|
+
return {
|
57
|
+
publicKeyCredential: null,
|
58
|
+
error: mapNativeErrorToClerkWebAuthnErrorCode(error.code, error.message, 'create'),
|
59
|
+
};
|
60
|
+
}
|
61
|
+
}
|
62
|
+
const makeSerializedGetResponse = (publicKeyCredential) => {
|
63
|
+
return {
|
64
|
+
type: publicKeyCredential.type,
|
65
|
+
id: publicKeyCredential.id,
|
66
|
+
rawId: base64urlToArrayBuffer(publicKeyCredential.rawId),
|
67
|
+
authenticatorAttachment: publicKeyCredential?.authenticatorAttachment || null,
|
68
|
+
response: {
|
69
|
+
clientDataJSON: base64urlToArrayBuffer(publicKeyCredential.response.clientDataJSON),
|
70
|
+
authenticatorData: base64urlToArrayBuffer(publicKeyCredential.response.authenticatorData),
|
71
|
+
signature: base64urlToArrayBuffer(publicKeyCredential.response.signature),
|
72
|
+
userHandle: publicKeyCredential?.response.userHandle
|
73
|
+
? base64urlToArrayBuffer(publicKeyCredential?.response.userHandle)
|
74
|
+
: null,
|
75
|
+
},
|
76
|
+
toJSON: () => publicKeyCredential,
|
77
|
+
};
|
78
|
+
};
|
79
|
+
export async function get({ publicKeyOptions, }) {
|
80
|
+
if (!publicKeyOptions) {
|
81
|
+
throw new Error('publicKeyCredential has not been provided');
|
82
|
+
}
|
83
|
+
const serializedPublicCredential = {
|
84
|
+
...publicKeyOptions,
|
85
|
+
challenge: arrayBufferToBase64Url(publicKeyOptions.challenge),
|
86
|
+
};
|
87
|
+
const getPasskeyModule = Platform.select({
|
88
|
+
android: async () => ClerkExpoPasskeys.get(JSON.stringify(serializedPublicCredential)),
|
89
|
+
ios: async () => ClerkExpoPasskeys.get(serializedPublicCredential.challenge, serializedPublicCredential.rpId),
|
90
|
+
default: null,
|
91
|
+
});
|
92
|
+
if (!getPasskeyModule) {
|
93
|
+
return {
|
94
|
+
publicKeyCredential: null,
|
95
|
+
error: new ClerkWebAuthnError('Platform is not supported', { code: 'passkey_not_supported' }),
|
96
|
+
};
|
97
|
+
}
|
98
|
+
try {
|
99
|
+
const response = await getPasskeyModule();
|
100
|
+
return {
|
101
|
+
publicKeyCredential: makeSerializedGetResponse(typeof response === 'string' ? JSON.parse(response) : response),
|
102
|
+
error: null,
|
103
|
+
};
|
104
|
+
}
|
105
|
+
catch (error) {
|
106
|
+
return {
|
107
|
+
publicKeyCredential: null,
|
108
|
+
error: mapNativeErrorToClerkWebAuthnErrorCode(error.code, error.message, 'get'),
|
109
|
+
};
|
110
|
+
}
|
111
|
+
}
|
112
|
+
const ANDROID_9 = 28;
|
113
|
+
const IOS_15 = 15;
|
114
|
+
export function isSupported() {
|
115
|
+
if (Platform.OS === 'android') {
|
116
|
+
return Platform.Version >= ANDROID_9;
|
117
|
+
}
|
118
|
+
if (Platform.OS === 'ios') {
|
119
|
+
return parseInt(Platform.Version, 10) > IOS_15;
|
120
|
+
}
|
121
|
+
return false;
|
122
|
+
}
|
123
|
+
// FIX:The autofill function has been implemented for iOS only, but the pop-up is not showing up.
|
124
|
+
// This seems to be an issue with Expo that we haven't been able to resolve yet.
|
125
|
+
// Further investigation and possibly reaching out to Expo support may be necessary.
|
126
|
+
// async function autofill(): Promise<AuthenticationResponseJSON | null> {
|
127
|
+
// if (Platform.OS === 'android') {
|
128
|
+
// throw new Error('Not supported');
|
129
|
+
// } else if (Platform.OS === 'ios') {
|
130
|
+
// throw new Error('Not supported');
|
131
|
+
// } else {
|
132
|
+
// throw new Error('Not supported');
|
133
|
+
// }
|
134
|
+
// }
|
135
|
+
export const passkeys = {
|
136
|
+
create,
|
137
|
+
get,
|
138
|
+
isSupported,
|
139
|
+
isAutoFillSupported: () => {
|
140
|
+
throw new Error('Not supported');
|
141
|
+
},
|
142
|
+
};
|
143
|
+
//# sourceMappingURL=index.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { Platform } from 'react-native';\n\nimport type {\n AuthenticationResponseJSON,\n CredentialReturn,\n PublicKeyCredentialCreationOptionsWithoutExtensions,\n PublicKeyCredentialRequestOptionsWithoutExtensions,\n PublicKeyCredentialWithAuthenticatorAssertionResponse,\n PublicKeyCredentialWithAuthenticatorAttestationResponse,\n RegistrationResponseJSON,\n SerializedPublicKeyCredentialCreationOptions,\n SerializedPublicKeyCredentialRequestOptions,\n} from './ClerkExpoPasskeys.types';\nimport ClerkExpoPasskeys from './ClerkExpoPasskeysModule';\nimport {\n arrayBufferToBase64Url,\n base64urlToArrayBuffer,\n ClerkWebAuthnError,\n encodeBase64Url,\n mapNativeErrorToClerkWebAuthnErrorCode,\n toArrayBuffer,\n} from './utils';\n\nconst makeSerializedCreateResponse = (\n publicCredential: RegistrationResponseJSON,\n): PublicKeyCredentialWithAuthenticatorAttestationResponse => ({\n id: publicCredential.id,\n rawId: base64urlToArrayBuffer(publicCredential.rawId),\n response: {\n getTransports: () => publicCredential?.response?.transports as string[],\n attestationObject: base64urlToArrayBuffer(publicCredential.response.attestationObject),\n clientDataJSON: base64urlToArrayBuffer(publicCredential.response.clientDataJSON),\n },\n type: publicCredential.type,\n authenticatorAttachment: publicCredential.authenticatorAttachment || null,\n toJSON: () => publicCredential,\n});\n\nexport async function create(\n publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions,\n): Promise<CredentialReturn<PublicKeyCredentialWithAuthenticatorAttestationResponse>> {\n if (!publicKey || !publicKey.rp.id) {\n throw new Error('Invalid public key or RpID');\n }\n\n const createOptions: SerializedPublicKeyCredentialCreationOptions = {\n rp: { id: publicKey.rp.id, name: publicKey.rp.name },\n user: {\n id: encodeBase64Url(toArrayBuffer(publicKey.user.id)),\n displayName: publicKey.user.displayName,\n name: publicKey.user.name,\n },\n pubKeyCredParams: publicKey.pubKeyCredParams,\n challenge: encodeBase64Url(toArrayBuffer(publicKey.challenge)),\n authenticatorSelection: {\n authenticatorAttachment: 'platform',\n requireResidentKey: true,\n residentKey: 'required',\n userVerification: 'required',\n },\n excludeCredentials: publicKey.excludeCredentials.map(c => ({\n type: 'public-key',\n id: encodeBase64Url(toArrayBuffer(c.id)),\n })),\n };\n\n const createPasskeyModule = Platform.select({\n android: async () => ClerkExpoPasskeys.create(JSON.stringify(createOptions)),\n ios: async () =>\n ClerkExpoPasskeys.create(\n createOptions.challenge,\n createOptions.rp.id,\n createOptions.user.id,\n createOptions.user.displayName,\n ),\n default: null,\n });\n\n if (!createPasskeyModule) {\n throw new Error('Platform not supported');\n }\n\n try {\n const response = await createPasskeyModule();\n return {\n publicKeyCredential: makeSerializedCreateResponse(typeof response === 'string' ? JSON.parse(response) : response),\n error: null,\n };\n } catch (error) {\n return {\n publicKeyCredential: null,\n error: mapNativeErrorToClerkWebAuthnErrorCode(error.code, error.message, 'create'),\n };\n }\n}\n\nconst makeSerializedGetResponse = (\n publicKeyCredential: AuthenticationResponseJSON,\n): PublicKeyCredentialWithAuthenticatorAssertionResponse => {\n return {\n type: publicKeyCredential.type,\n id: publicKeyCredential.id,\n rawId: base64urlToArrayBuffer(publicKeyCredential.rawId),\n authenticatorAttachment: publicKeyCredential?.authenticatorAttachment || null,\n response: {\n clientDataJSON: base64urlToArrayBuffer(publicKeyCredential.response.clientDataJSON),\n authenticatorData: base64urlToArrayBuffer(publicKeyCredential.response.authenticatorData),\n signature: base64urlToArrayBuffer(publicKeyCredential.response.signature),\n userHandle: publicKeyCredential?.response.userHandle\n ? base64urlToArrayBuffer(publicKeyCredential?.response.userHandle)\n : null,\n },\n toJSON: () => publicKeyCredential,\n };\n};\n\nexport async function get({\n publicKeyOptions,\n}: {\n publicKeyOptions: PublicKeyCredentialRequestOptionsWithoutExtensions;\n}): Promise<CredentialReturn<PublicKeyCredentialWithAuthenticatorAssertionResponse>> {\n if (!publicKeyOptions) {\n throw new Error('publicKeyCredential has not been provided');\n }\n\n const serializedPublicCredential: SerializedPublicKeyCredentialRequestOptions = {\n ...publicKeyOptions,\n challenge: arrayBufferToBase64Url(publicKeyOptions.challenge),\n };\n\n const getPasskeyModule = Platform.select({\n android: async () => ClerkExpoPasskeys.get(JSON.stringify(serializedPublicCredential)),\n ios: async () => ClerkExpoPasskeys.get(serializedPublicCredential.challenge, serializedPublicCredential.rpId),\n default: null,\n });\n\n if (!getPasskeyModule) {\n return {\n publicKeyCredential: null,\n error: new ClerkWebAuthnError('Platform is not supported', { code: 'passkey_not_supported' }),\n };\n }\n\n try {\n const response = await getPasskeyModule();\n return {\n publicKeyCredential: makeSerializedGetResponse(typeof response === 'string' ? JSON.parse(response) : response),\n error: null,\n };\n } catch (error) {\n return {\n publicKeyCredential: null,\n error: mapNativeErrorToClerkWebAuthnErrorCode(error.code, error.message, 'get'),\n };\n }\n}\n\nconst ANDROID_9 = 28;\nconst IOS_15 = 15;\n\nexport function isSupported() {\n if (Platform.OS === 'android') {\n return Platform.Version >= ANDROID_9;\n }\n\n if (Platform.OS === 'ios') {\n return parseInt(Platform.Version, 10) > IOS_15;\n }\n\n return false;\n}\n\n// FIX:The autofill function has been implemented for iOS only, but the pop-up is not showing up.\n// This seems to be an issue with Expo that we haven't been able to resolve yet.\n// Further investigation and possibly reaching out to Expo support may be necessary.\n\n// async function autofill(): Promise<AuthenticationResponseJSON | null> {\n// if (Platform.OS === 'android') {\n// throw new Error('Not supported');\n// } else if (Platform.OS === 'ios') {\n// throw new Error('Not supported');\n// } else {\n// throw new Error('Not supported');\n// }\n// }\n\nexport const passkeys = {\n create,\n get,\n isSupported,\n isAutoFillSupported: () => {\n throw new Error('Not supported');\n },\n};\n"]}
|
package/build/utils.d.ts
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
import { ClerkWebAuthnError } from '@clerk/shared/error';
|
2
|
+
export { ClerkWebAuthnError };
|
3
|
+
export declare function encodeBase64(data: ArrayLike<number> | ArrayBufferLike): string;
|
4
|
+
export declare function encodeBase64Url(data: ArrayLike<number> | ArrayBufferLike): string;
|
5
|
+
export declare function decodeBase64Url(data: string): Uint8Array;
|
6
|
+
export declare function decodeToken(data: string): any;
|
7
|
+
export declare function decodeBase64(data: string): Uint8Array;
|
8
|
+
export declare function utf8Decode(buffer: BufferSource): string;
|
9
|
+
export declare function toArrayBuffer(bufferSource: BufferSource): ArrayBufferLike;
|
10
|
+
export declare function base64urlToArrayBuffer(base64url: string): ArrayBuffer;
|
11
|
+
export declare function arrayBufferToBase64Url(buffer: any): string;
|
12
|
+
export declare function mapNativeErrorToClerkWebAuthnErrorCode(code: string, message: string, action: 'get' | 'create'): ClerkWebAuthnError;
|
13
|
+
//# sourceMappingURL=utils.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAE9B,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,eAAe,UAErE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,eAAe,UAExE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,cAE3C;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,OASvC;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,cAExC;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,UAG9C;AAED,wBAAgB,aAAa,CAAC,YAAY,EAAE,YAAY,mBAQvD;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,eAcvD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,KAAA,UAa5C;AAED,wBAAgB,sCAAsC,CACpD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,KAAK,GAAG,QAAQ,GACvB,kBAAkB,CAyBpB"}
|
package/build/utils.js
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
import { ClerkWebAuthnError } from '@clerk/shared/error';
|
2
|
+
import { Buffer } from 'buffer';
|
3
|
+
export { ClerkWebAuthnError };
|
4
|
+
export function encodeBase64(data) {
|
5
|
+
return btoa(String.fromCharCode(...new Uint8Array(data)));
|
6
|
+
}
|
7
|
+
export function encodeBase64Url(data) {
|
8
|
+
return encodeBase64(data).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
|
9
|
+
}
|
10
|
+
export function decodeBase64Url(data) {
|
11
|
+
return decodeBase64(data.replaceAll('-', '+').replaceAll('_', '/'));
|
12
|
+
}
|
13
|
+
export function decodeToken(data) {
|
14
|
+
const base64 = data.replace(/-/g, '+').replace(/_/g, '/');
|
15
|
+
const jsonPayload = decodeURIComponent(atob(base64)
|
16
|
+
.split('')
|
17
|
+
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
18
|
+
.join(''));
|
19
|
+
return JSON.parse(jsonPayload);
|
20
|
+
}
|
21
|
+
export function decodeBase64(data) {
|
22
|
+
return Uint8Array.from(atob(data).split(''), x => x.charCodeAt(0));
|
23
|
+
}
|
24
|
+
export function utf8Decode(buffer) {
|
25
|
+
const textDecoder = new TextDecoder();
|
26
|
+
return textDecoder.decode(buffer);
|
27
|
+
}
|
28
|
+
export function toArrayBuffer(bufferSource) {
|
29
|
+
if (bufferSource instanceof ArrayBuffer) {
|
30
|
+
return bufferSource; // It's already an ArrayBuffer
|
31
|
+
}
|
32
|
+
else if (ArrayBuffer.isView(bufferSource)) {
|
33
|
+
return bufferSource.buffer; // Extract the ArrayBuffer from the typed array
|
34
|
+
}
|
35
|
+
else {
|
36
|
+
throw new TypeError('Expected a BufferSource, but received an incompatible type.');
|
37
|
+
}
|
38
|
+
}
|
39
|
+
export function base64urlToArrayBuffer(base64url) {
|
40
|
+
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
|
41
|
+
const binaryString = Buffer.from(base64, 'base64').toString('binary');
|
42
|
+
const len = binaryString.length;
|
43
|
+
const buffer = new ArrayBuffer(len);
|
44
|
+
const uintArray = new Uint8Array(buffer);
|
45
|
+
for (let i = 0; i < len; i++) {
|
46
|
+
uintArray[i] = binaryString.charCodeAt(i);
|
47
|
+
}
|
48
|
+
return buffer;
|
49
|
+
}
|
50
|
+
export function arrayBufferToBase64Url(buffer) {
|
51
|
+
const bytes = new Uint8Array(buffer);
|
52
|
+
let binary = '';
|
53
|
+
for (let i = 0; i < bytes.length; i++) {
|
54
|
+
binary += String.fromCharCode(bytes[i]);
|
55
|
+
}
|
56
|
+
const base64String = btoa(binary);
|
57
|
+
const base64Url = base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
58
|
+
return base64Url;
|
59
|
+
}
|
60
|
+
export function mapNativeErrorToClerkWebAuthnErrorCode(code, message, action) {
|
61
|
+
if (code === '1000' || code === '1004' || code === 'CreatePublicKeyCredentialDomException') {
|
62
|
+
return new ClerkWebAuthnError(message, {
|
63
|
+
code: action === 'create' ? 'passkey_registration_failed' : 'passkey_retrieval_failed',
|
64
|
+
});
|
65
|
+
}
|
66
|
+
if (code === '1001' ||
|
67
|
+
code === 'CreateCredentialCancellationException' ||
|
68
|
+
code === 'GetCredentialCancellationException') {
|
69
|
+
return new ClerkWebAuthnError(message, { code: 'passkey_registration_cancelled' });
|
70
|
+
}
|
71
|
+
if (code === '1002') {
|
72
|
+
return new ClerkWebAuthnError(message, { code: 'passkey_invalid_rpID_or_domain' });
|
73
|
+
}
|
74
|
+
if (code === '1003' || code === 'CreateCredentialInterruptedException') {
|
75
|
+
return new ClerkWebAuthnError(message, { code: 'passkey_operation_aborted' });
|
76
|
+
}
|
77
|
+
return new ClerkWebAuthnError(message, {
|
78
|
+
code: action === 'create' ? 'passkey_registration_failed' : 'passkey_retrieval_failed',
|
79
|
+
});
|
80
|
+
}
|
81
|
+
//# sourceMappingURL=utils.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { ClerkWebAuthnError } from '@clerk/shared/error';\nimport { Buffer } from 'buffer';\nexport { ClerkWebAuthnError };\n\nexport function encodeBase64(data: ArrayLike<number> | ArrayBufferLike) {\n return btoa(String.fromCharCode(...new Uint8Array(data)));\n}\n\nexport function encodeBase64Url(data: ArrayLike<number> | ArrayBufferLike) {\n return encodeBase64(data).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');\n}\n\nexport function decodeBase64Url(data: string) {\n return decodeBase64(data.replaceAll('-', '+').replaceAll('_', '/'));\n}\n\nexport function decodeToken(data: string) {\n const base64 = data.replace(/-/g, '+').replace(/_/g, '/');\n const jsonPayload = decodeURIComponent(\n atob(base64)\n .split('')\n .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))\n .join(''),\n );\n return JSON.parse(jsonPayload);\n}\n\nexport function decodeBase64(data: string) {\n return Uint8Array.from(atob(data).split(''), x => x.charCodeAt(0));\n}\n\nexport function utf8Decode(buffer: BufferSource) {\n const textDecoder = new TextDecoder();\n return textDecoder.decode(buffer);\n}\n\nexport function toArrayBuffer(bufferSource: BufferSource) {\n if (bufferSource instanceof ArrayBuffer) {\n return bufferSource; // It's already an ArrayBuffer\n } else if (ArrayBuffer.isView(bufferSource)) {\n return bufferSource.buffer; // Extract the ArrayBuffer from the typed array\n } else {\n throw new TypeError('Expected a BufferSource, but received an incompatible type.');\n }\n}\n\nexport function base64urlToArrayBuffer(base64url: string) {\n const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');\n\n const binaryString = Buffer.from(base64, 'base64').toString('binary');\n\n const len = binaryString.length;\n const buffer = new ArrayBuffer(len);\n const uintArray = new Uint8Array(buffer);\n\n for (let i = 0; i < len; i++) {\n uintArray[i] = binaryString.charCodeAt(i);\n }\n\n return buffer;\n}\n\nexport function arrayBufferToBase64Url(buffer) {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n\n const base64String = btoa(binary);\n\n const base64Url = base64String.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n\n return base64Url;\n}\n\nexport function mapNativeErrorToClerkWebAuthnErrorCode(\n code: string,\n message: string,\n action: 'get' | 'create',\n): ClerkWebAuthnError {\n if (code === '1000' || code === '1004' || code === 'CreatePublicKeyCredentialDomException') {\n return new ClerkWebAuthnError(message, {\n code: action === 'create' ? 'passkey_registration_failed' : 'passkey_retrieval_failed',\n });\n }\n if (\n code === '1001' ||\n code === 'CreateCredentialCancellationException' ||\n code === 'GetCredentialCancellationException'\n ) {\n return new ClerkWebAuthnError(message, { code: 'passkey_registration_cancelled' });\n }\n\n if (code === '1002') {\n return new ClerkWebAuthnError(message, { code: 'passkey_invalid_rpID_or_domain' });\n }\n\n if (code === '1003' || code === 'CreateCredentialInterruptedException') {\n return new ClerkWebAuthnError(message, { code: 'passkey_operation_aborted' });\n }\n\n return new ClerkWebAuthnError(message, {\n code: action === 'create' ? 'passkey_registration_failed' : 'passkey_retrieval_failed',\n });\n}\n"]}
|
@@ -0,0 +1,182 @@
|
|
1
|
+
import AuthenticationServices
|
2
|
+
import Foundation
|
3
|
+
import os
|
4
|
+
|
5
|
+
import ExpoModulesCore
|
6
|
+
|
7
|
+
@available(iOS 15.0, *)
|
8
|
+
struct AuthorizationResponse {
|
9
|
+
var registration: ASAuthorizationPlatformPublicKeyCredentialRegistration?
|
10
|
+
var assertion: ASAuthorizationPlatformPublicKeyCredentialAssertion?
|
11
|
+
}
|
12
|
+
|
13
|
+
@available(iOS 15.0, *)
|
14
|
+
class AccountManager: NSObject, ASAuthorizationControllerPresentationContextProviding, ASAuthorizationControllerDelegate {
|
15
|
+
private var authCallback: ((AuthorizationResponse?,ASAuthorizationError.Code?) -> Void)?
|
16
|
+
var authController: ASAuthorizationController?
|
17
|
+
|
18
|
+
func createPasskey(challengeBase64: String, rpId: String, displayName: String, userIdBase64: String, promise: Promise) {
|
19
|
+
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId)
|
20
|
+
|
21
|
+
let challenge = dataFromBase64URL(base64urlString: challengeBase64)!
|
22
|
+
let userId = dataFromBase64URL(base64urlString: userIdBase64)!
|
23
|
+
|
24
|
+
let registrationRequest = publicKeyCredentialProvider.createCredentialRegistrationRequest(challenge: challenge,
|
25
|
+
name: displayName, userID: userId)
|
26
|
+
|
27
|
+
let authController = ASAuthorizationController(authorizationRequests: [ registrationRequest ] )
|
28
|
+
authController.delegate = self
|
29
|
+
authController.presentationContextProvider = self
|
30
|
+
authController.performRequests()
|
31
|
+
|
32
|
+
self.authCallback = { result,error in
|
33
|
+
if (error != nil) {
|
34
|
+
promise.reject(String(error!.rawValue), handleAuthorizationError(error!).rawValue)
|
35
|
+
return
|
36
|
+
}
|
37
|
+
|
38
|
+
if let response = result?.registration {
|
39
|
+
let authResult: NSDictionary = [
|
40
|
+
"id": base64URLFromBase64(base64String: response.credentialID.base64EncodedString()),
|
41
|
+
"rawId": base64URLFromBase64(base64String: response.credentialID.base64EncodedString()),
|
42
|
+
"type": "public-key",
|
43
|
+
"response": [
|
44
|
+
"attestationObject": base64URLFromBase64(base64String: response.rawAttestationObject!.base64EncodedString()),
|
45
|
+
"clientDataJSON": base64URLFromBase64(base64String: response.rawClientDataJSON.base64EncodedString())
|
46
|
+
]
|
47
|
+
]
|
48
|
+
promise.resolve(authResult);
|
49
|
+
} else {
|
50
|
+
promise.reject(Errors.notHandled.rawValue, "Not valid registration result");
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
|
56
|
+
func getPasskey(challengeBase64URL: String, rpId: String, promise: Promise) {
|
57
|
+
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId)
|
58
|
+
|
59
|
+
let challenge = dataFromBase64URL(base64urlString: challengeBase64URL)!
|
60
|
+
let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge)
|
61
|
+
|
62
|
+
|
63
|
+
let authController = ASAuthorizationController(authorizationRequests: [ assertionRequest ] )
|
64
|
+
authController.delegate = self
|
65
|
+
authController.presentationContextProvider = self
|
66
|
+
authController.performRequests()
|
67
|
+
|
68
|
+
self.authCallback = { result,error in
|
69
|
+
if (error != nil) {
|
70
|
+
promise.reject(String(error!.rawValue), handleAuthorizationError(error!).rawValue);
|
71
|
+
return
|
72
|
+
}
|
73
|
+
|
74
|
+
if let response = result?.assertion {
|
75
|
+
let authResult: NSDictionary = [
|
76
|
+
"id": base64URLFromBase64(base64String: response.credentialID.base64EncodedString()),
|
77
|
+
"rawId": base64URLFromBase64(base64String: response.credentialID.base64EncodedString()),
|
78
|
+
"type":"public-key",
|
79
|
+
"response": [
|
80
|
+
"authenticatorData":base64URLFromBase64(base64String: response.rawAuthenticatorData.base64EncodedString()),
|
81
|
+
"clientDataJSON": base64URLFromBase64(base64String: response.rawClientDataJSON.base64EncodedString()),
|
82
|
+
"signature": base64URLFromBase64(base64String: response.signature.base64EncodedString()),
|
83
|
+
"userHandle": base64URLFromBase64(base64String: response.userID.base64EncodedString()),
|
84
|
+
] ]
|
85
|
+
promise.resolve(authResult);
|
86
|
+
} else {
|
87
|
+
promise.reject("Response is invalid", "Could not retrieve passkey");
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
}
|
92
|
+
|
93
|
+
@available(iOS 16.0, *)
|
94
|
+
func beginAutoFillAssistedPasskeySignIn(challengeBase64URL: String, rpId: String, promise: Promise) {
|
95
|
+
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: rpId)
|
96
|
+
|
97
|
+
let challenge = dataFromBase64URL(base64urlString: challengeBase64URL)!
|
98
|
+
let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge)
|
99
|
+
|
100
|
+
let authController = ASAuthorizationController(authorizationRequests: [ assertionRequest ] )
|
101
|
+
|
102
|
+
authController.delegate = self
|
103
|
+
authController.presentationContextProvider = self
|
104
|
+
authController.performAutoFillAssistedRequests()
|
105
|
+
|
106
|
+
self.authCallback = { result,error in
|
107
|
+
if (error != nil) {
|
108
|
+
promise.reject(String(error!.rawValue), handleAuthorizationError(error!).rawValue);
|
109
|
+
return
|
110
|
+
}
|
111
|
+
|
112
|
+
if let response = result?.assertion {
|
113
|
+
let authResult: NSDictionary = [
|
114
|
+
"id": base64URLFromBase64(base64String: response.credentialID.base64EncodedString()),
|
115
|
+
"rawId": base64URLFromBase64(base64String: response.credentialID.base64EncodedString()),
|
116
|
+
"type":"public-key",
|
117
|
+
"response": [
|
118
|
+
"authenticatorData":base64URLFromBase64(base64String: response.rawAuthenticatorData.base64EncodedString()),
|
119
|
+
"clientDataJSON": base64URLFromBase64(base64String: response.rawClientDataJSON.base64EncodedString()),
|
120
|
+
"signature": base64URLFromBase64(base64String: response.signature.base64EncodedString()),
|
121
|
+
"userHandle": base64URLFromBase64(base64String: response.userID.base64EncodedString()),
|
122
|
+
] ]
|
123
|
+
promise.resolve(authResult);
|
124
|
+
} else {
|
125
|
+
promise.reject("Response is invalid", "Could not retrieve passkey");
|
126
|
+
}
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
|
131
|
+
@available(iOS 16.0, *)
|
132
|
+
func cancelAutoFillAssistedPasskeySignIn() {
|
133
|
+
if authController != nil {
|
134
|
+
self.authController?.cancel()
|
135
|
+
self.authController = nil
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
|
142
|
+
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
|
143
|
+
let logger = Logger()
|
144
|
+
switch authorization.credential {
|
145
|
+
case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration:
|
146
|
+
self.authCallback!(AuthorizationResponse(registration: credentialRegistration),nil)
|
147
|
+
logger.log("A new passkey was registered: \(credentialRegistration)")
|
148
|
+
case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
|
149
|
+
self.authCallback!(AuthorizationResponse(assertion: credentialAssertion),nil)
|
150
|
+
logger.log("A passkey was used to sign in: \(credentialAssertion)")
|
151
|
+
default:
|
152
|
+
self.authCallback!(AuthorizationResponse(registration: nil), ASAuthorizationError.unknown)
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
|
157
|
+
let logger = Logger()
|
158
|
+
guard let authorizationError = error as? ASAuthorizationError else {
|
159
|
+
self.authCallback!(AuthorizationResponse(registration: nil), ASAuthorizationError.unknown)
|
160
|
+
logger.error("Unexpected authorization error: \(error.localizedDescription)")
|
161
|
+
return
|
162
|
+
}
|
163
|
+
|
164
|
+
if authorizationError.code == .canceled {
|
165
|
+
// Either the system doesn't find any credentials and the request ends silently, or the user cancels the request.
|
166
|
+
// This is a good time to show a traditional login form, or ask the user to create an account.
|
167
|
+
self.authCallback!(AuthorizationResponse(registration: nil),ASAuthorizationError.canceled)
|
168
|
+
logger.log("Request canceled.")
|
169
|
+
|
170
|
+
} else {
|
171
|
+
// Another ASAuthorization error.
|
172
|
+
// Note: The userInfo dictionary contains useful information.
|
173
|
+
self.authCallback!(AuthorizationResponse(registration: nil),ASAuthorizationError.unknown)
|
174
|
+
logger.error("Error: \((error as NSError).userInfo)")
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|
178
|
+
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
179
|
+
return UIApplication.shared.keyWindow!;
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
4
|
+
|
5
|
+
Pod::Spec.new do |s|
|
6
|
+
s.name = 'ClerkExpoPasskeys'
|
7
|
+
s.version = package['version']
|
8
|
+
s.summary = package['description']
|
9
|
+
s.description = package['description']
|
10
|
+
s.license = package['license']
|
11
|
+
s.author = package['author']
|
12
|
+
s.homepage = package['homepage']
|
13
|
+
s.platforms = { :ios => '13.4', :tvos => '13.4' }
|
14
|
+
s.swift_version = '5.4'
|
15
|
+
s.source = { git: 'https://github.com/clerk/javascript/tree/main/packages/expo-passkeys' }
|
16
|
+
s.static_framework = true
|
17
|
+
|
18
|
+
s.dependency 'ExpoModulesCore'
|
19
|
+
|
20
|
+
# Swift/Objective-C compatibility
|
21
|
+
s.pod_target_xcconfig = {
|
22
|
+
'DEFINES_MODULE' => 'YES',
|
23
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
24
|
+
}
|
25
|
+
|
26
|
+
s.source_files = "**/*.{h,m,swift}"
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import ExpoModulesCore
|
2
|
+
|
3
|
+
@available(iOS 15.0, *)
|
4
|
+
public class ClerkExpoPasskeysModule: Module {
|
5
|
+
let credentialManager = AccountManager()
|
6
|
+
|
7
|
+
public func definition() -> ModuleDefinition {
|
8
|
+
Name("ClerkExpoPasskeys")
|
9
|
+
|
10
|
+
AsyncFunction("create") { (challenge: String, rpId:String, userId: String, displayName: String, promise: Promise) in
|
11
|
+
self.credentialManager.createPasskey(challengeBase64: challenge, rpId:rpId, displayName:displayName, userIdBase64: userId, promise: promise)
|
12
|
+
}
|
13
|
+
|
14
|
+
AsyncFunction("get") { (challenge: String, rpId:String, promise: Promise) in
|
15
|
+
self.credentialManager.getPasskey(challengeBase64URL: challenge, rpId: rpId, promise: promise)
|
16
|
+
}
|
17
|
+
|
18
|
+
AsyncFunction("autofill") { (challenge: String, rpId:String, promise: Promise) in
|
19
|
+
if #available(iOS 16.0, *) {
|
20
|
+
self.credentialManager.beginAutoFillAssistedPasskeySignIn(challengeBase64URL: challenge, rpId: rpId, promise: promise)
|
21
|
+
} else {
|
22
|
+
// Fallback on earlier versions
|
23
|
+
}
|
24
|
+
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
@@ -0,0 +1,56 @@
|
|
1
|
+
import Foundation
|
2
|
+
import AuthenticationServices
|
3
|
+
|
4
|
+
|
5
|
+
enum Errors: String, Error {
|
6
|
+
case unknown = "An unknown error occurred."
|
7
|
+
case canceled = "Authorization was canceled by the user."
|
8
|
+
case invalidResponse = "The authorization request received an invalid response."
|
9
|
+
case notHandled = "The authorization request wasn't handled."
|
10
|
+
case failed = "The authorization request failed."
|
11
|
+
case unknownAuthorizationError = "An unknown authorization error occurred."
|
12
|
+
}
|
13
|
+
|
14
|
+
func handleAuthorizationError(_ error: ASAuthorizationError.Code) -> Errors {
|
15
|
+
switch error {
|
16
|
+
case .unknown:
|
17
|
+
return Errors.unknown
|
18
|
+
case .canceled:
|
19
|
+
return Errors.canceled
|
20
|
+
case .invalidResponse:
|
21
|
+
return Errors.invalidResponse
|
22
|
+
case .notHandled:
|
23
|
+
return Errors.notHandled
|
24
|
+
case .failed:
|
25
|
+
return Errors.failed
|
26
|
+
@unknown default:
|
27
|
+
return Errors.unknownAuthorizationError
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
func dataFromBase64URL(base64urlString: String) -> Data? {
|
32
|
+
var base64 = base64urlString
|
33
|
+
|
34
|
+
base64 = base64.replacingOccurrences(of: "-", with: "+")
|
35
|
+
base64 = base64.replacingOccurrences(of: "_", with: "/")
|
36
|
+
|
37
|
+
let paddingLength = 4 - (base64.count % 4)
|
38
|
+
if paddingLength < 4 {
|
39
|
+
base64 = base64.padding(toLength: base64.count + paddingLength, withPad: "=", startingAt: 0)
|
40
|
+
}
|
41
|
+
|
42
|
+
return Data(base64Encoded: base64)
|
43
|
+
}
|
44
|
+
|
45
|
+
|
46
|
+
func base64URLFromBase64(base64String: String) -> String {
|
47
|
+
var base64url = base64String
|
48
|
+
|
49
|
+
base64url = base64url.replacingOccurrences(of: "+", with: "-")
|
50
|
+
|
51
|
+
base64url = base64url.replacingOccurrences(of: "/", with: "_")
|
52
|
+
|
53
|
+
base64url = base64url.replacingOccurrences(of: "=", with: "")
|
54
|
+
|
55
|
+
return base64url
|
56
|
+
}
|
package/package.json
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
{
|
2
|
+
"name": "@clerk/expo-passkeys",
|
3
|
+
"version": "1.0.0-canary.v5ad1f6c829a30e8efd2d4e7da310a9116c8005db",
|
4
|
+
"description": "Passkeys library to be used with Clerk for expo",
|
5
|
+
"keywords": [
|
6
|
+
"react-native",
|
7
|
+
"expo",
|
8
|
+
"expo-passkey",
|
9
|
+
"ClerkExpoPasskeys",
|
10
|
+
"clerk",
|
11
|
+
"passkeys"
|
12
|
+
],
|
13
|
+
"homepage": "https://clerk.com/",
|
14
|
+
"bugs": {
|
15
|
+
"url": "https://github.com/clerk/javascript/issues"
|
16
|
+
},
|
17
|
+
"repository": "git+https://github.com/clerk/javascript.git",
|
18
|
+
"license": "MIT",
|
19
|
+
"author": "Clerk",
|
20
|
+
"main": "build/index.js",
|
21
|
+
"types": "build/index.d.ts",
|
22
|
+
"scripts": {
|
23
|
+
"build": "EXPO_NONINTERACTIVE=1 expo-module build",
|
24
|
+
"build:watch": "expo-module build",
|
25
|
+
"clean": "expo-module clean",
|
26
|
+
"expo-module": "expo-module",
|
27
|
+
"lint": "expo-module lint",
|
28
|
+
"open:android": "open -a \"Android Studio\" example/android",
|
29
|
+
"open:ios": "xed example/ios",
|
30
|
+
"prepare": "expo-module prepare",
|
31
|
+
"prepublishOnly": "expo-module prepublishOnly"
|
32
|
+
},
|
33
|
+
"dependencies": {
|
34
|
+
"@clerk/shared": "2.10.1-snapshot.va96a299",
|
35
|
+
"@clerk/types": "4.28.0-snapshot.va96a299"
|
36
|
+
},
|
37
|
+
"devDependencies": {
|
38
|
+
"expo-module-scripts": "^3.5.2",
|
39
|
+
"expo-modules-core": "^1.12.19"
|
40
|
+
},
|
41
|
+
"peerDependencies": {
|
42
|
+
"expo": "*",
|
43
|
+
"react": "*",
|
44
|
+
"react-native": "*"
|
45
|
+
},
|
46
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
47
|
+
}
|