@clerk/expo-passkeys 1.0.0-canary.v52dd05fd75ae29510cd0f653c3224a3382675a5f
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
@@ -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 @@
|
|
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
|
+
}
|