@portal-hq/passkey-storage 3.0.5

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/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @portal-hq/passkey-storage
2
+
3
+ Portal's Passkey storage adapter
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install @portal-hq/passkey-storage
9
+ ```
10
+
11
+ ```sh
12
+ yarn add @portal-hq/passkey-storage
13
+ ```
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.PasskeyStatus = exports.PasskeyStorage = void 0;
13
+ const utils_1 = require("@portal-hq/utils");
14
+ const react_native_passkey_1 = require("react-native-passkey");
15
+ class PasskeyStorage {
16
+ constructor(opts) {
17
+ this.relyingParty = 'portalhq.io';
18
+ this.webAuthnHost = 'https://backup.web.portalhq.io';
19
+ this.relyingPartyOrigins = [
20
+ 'https://portalhq.io',
21
+ 'https://backup.web.portalhq.io',
22
+ ];
23
+ this.relyingParty = opts.relyingParty || this.relyingParty;
24
+ this.relyingPartyOrigins =
25
+ opts.relyingPartyOrigins || this.relyingPartyOrigins;
26
+ this.webAuthnHost = opts.webAuthnHost || this.webAuthnHost;
27
+ this.passkeyApi = new utils_1.HttpRequester({
28
+ baseUrl: this.webAuthnHost,
29
+ });
30
+ }
31
+ delete() {
32
+ throw new Error('Method is not supported.');
33
+ }
34
+ // eslint-disable-next-line @typescript-eslint/require-await
35
+ read() {
36
+ var _a;
37
+ return __awaiter(this, void 0, void 0, function* () {
38
+ try {
39
+ if (!((_a = this.api) === null || _a === void 0 ? void 0 : _a.apiKey)) {
40
+ throw new Error('API key is not set');
41
+ }
42
+ const apiKey = this.api.apiKey;
43
+ // Construct the headers object
44
+ const headers = {
45
+ Accept: 'application/json',
46
+ 'Content-Type': 'application/json',
47
+ Authorization: `Bearer ${apiKey}`,
48
+ };
49
+ const assertion = yield this.getAssertionAndSetSessionId(headers);
50
+ // finish login
51
+ const encryptionKey = yield this.getEncryptionKey(assertion);
52
+ return encryptionKey;
53
+ }
54
+ catch (error) {
55
+ if (error instanceof Error) {
56
+ throw new Error(`Failed to complete read operation: ${error.message}`);
57
+ }
58
+ else {
59
+ throw new Error('Failed to complete read operation due to an unexpected error');
60
+ }
61
+ }
62
+ });
63
+ }
64
+ write(privateKey) {
65
+ var _a;
66
+ return __awaiter(this, void 0, void 0, function* () {
67
+ try {
68
+ if (!((_a = this.api) === null || _a === void 0 ? void 0 : _a.apiKey)) {
69
+ throw new Error('API key is not set');
70
+ }
71
+ const apiKey = this.api.apiKey;
72
+ // Construct the headers object
73
+ const headers = {
74
+ Accept: 'application/json',
75
+ 'Content-Type': 'application/json',
76
+ Authorization: `Bearer ${apiKey}`,
77
+ };
78
+ // Get the status of the passkey
79
+ const passkeyStatus = yield this.passkeyApi.get('/passkeys/status', {
80
+ headers,
81
+ });
82
+ // If the passkey is registered with a credential, we login
83
+ if (passkeyStatus.status === PasskeyStatus.RegisteredWithCredential) {
84
+ // Login
85
+ const assertion = yield this.getAssertionAndSetSessionId(headers);
86
+ yield this.passkeyApi.post('/passkeys/finish-login/write', {
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ Authorization: `Bearer ${apiKey}`,
90
+ },
91
+ body: {
92
+ assertion: JSON.stringify(assertion),
93
+ sessionId: this.sessionId,
94
+ encryptionKey: privateKey,
95
+ relyingParty: this.relyingParty,
96
+ relyingPartyOrigins: this.relyingPartyOrigins,
97
+ },
98
+ });
99
+ }
100
+ else {
101
+ // Register
102
+ const attestation = yield this.getAttestationAndSetSessionId(headers);
103
+ yield this.passkeyApi.post('/passkeys/finish-registration', {
104
+ headers: {
105
+ 'Content-Type': 'application/json',
106
+ Authorization: `Bearer ${apiKey}`,
107
+ },
108
+ body: {
109
+ attestation: JSON.stringify(attestation),
110
+ sessionId: this.sessionId,
111
+ encryptionKey: privateKey,
112
+ relyingParty: this.relyingParty,
113
+ relyingPartyOrigins: this.relyingPartyOrigins,
114
+ },
115
+ });
116
+ }
117
+ return 'done';
118
+ }
119
+ catch (error) {
120
+ if (error instanceof Error) {
121
+ throw new Error(`Failed to complete write operation: ${error.message}`);
122
+ }
123
+ else {
124
+ throw new Error('Failed to complete write operation due to an unexpected error');
125
+ }
126
+ }
127
+ });
128
+ }
129
+ // eslint-disable-next-line @typescript-eslint/require-await
130
+ validateOperations() {
131
+ return __awaiter(this, void 0, void 0, function* () {
132
+ const isSupported = react_native_passkey_1.Passkey.isSupported();
133
+ return isSupported;
134
+ });
135
+ }
136
+ // Helper Methods
137
+ getAttestationAndSetSessionId(headers) {
138
+ return __awaiter(this, void 0, void 0, function* () {
139
+ const beginResponse = yield this.passkeyApi.post('/passkeys/begin-registration', {
140
+ headers,
141
+ body: {
142
+ relyingParty: this.relyingParty,
143
+ relyingPartyOrigins: this.relyingPartyOrigins,
144
+ },
145
+ });
146
+ // check if challenge exists
147
+ if (!beginResponse.options.publicKey.challenge) {
148
+ throw new Error('Challenge is missing in passkey options. Please try again and check your web authn host url.');
149
+ }
150
+ beginResponse.options.publicKey.challenge = convertBase64UrlToBase64(beginResponse.options.publicKey.challenge);
151
+ const result = yield react_native_passkey_1.Passkey.register(beginResponse.options.publicKey);
152
+ const attestation = {
153
+ rawId: convertBase64ToBase64Url(result.rawId),
154
+ id: convertBase64ToBase64Url(result.id),
155
+ type: 'public-key',
156
+ response: {
157
+ clientDataJSON: convertBase64ToBase64Url(result.response.clientDataJSON),
158
+ attestationObject: convertBase64ToBase64Url(result.response.attestationObject),
159
+ },
160
+ };
161
+ // check if sessionId exists
162
+ if (!beginResponse.sessionId) {
163
+ throw new Error('Session ID is missing in passkey options. Please try again and check your web authn host url.');
164
+ }
165
+ this.sessionId = beginResponse.sessionId;
166
+ return attestation;
167
+ });
168
+ }
169
+ getAssertionAndSetSessionId(headers) {
170
+ return __awaiter(this, void 0, void 0, function* () {
171
+ const beginResponse = yield this.passkeyApi.post('/passkeys/begin-login', {
172
+ headers,
173
+ body: {
174
+ relyingParty: this.relyingParty,
175
+ relyingPartyOrigins: this.relyingPartyOrigins,
176
+ },
177
+ });
178
+ // check if challenge exists
179
+ if (!beginResponse.options.publicKey.challenge) {
180
+ throw new Error('Challenge is missing in passkey options. Please try again and check your web authn host url.');
181
+ }
182
+ beginResponse.options.publicKey.challenge = convertBase64UrlToBase64(beginResponse.options.publicKey.challenge);
183
+ const result = yield react_native_passkey_1.Passkey.authenticate(beginResponse.options.publicKey);
184
+ const assertion = {
185
+ rawId: convertBase64ToBase64Url(result.rawId),
186
+ id: convertBase64ToBase64Url(result.id),
187
+ type: 'public-key',
188
+ response: {
189
+ clientDataJSON: convertBase64ToBase64Url(result.response.clientDataJSON),
190
+ authenticatorData: convertBase64ToBase64Url(result.response.authenticatorData),
191
+ signature: convertBase64ToBase64Url(result.response.signature),
192
+ userHandle: convertBase64ToBase64Url(result.response.userHandle),
193
+ },
194
+ };
195
+ this.sessionId = beginResponse.sessionId;
196
+ return assertion;
197
+ });
198
+ }
199
+ getEncryptionKey(assertion) {
200
+ var _a;
201
+ return __awaiter(this, void 0, void 0, function* () {
202
+ if (((_a = this.api) === null || _a === void 0 ? void 0 : _a.apiKey) === undefined) {
203
+ throw new Error('API key is not set');
204
+ }
205
+ const finishResponse = yield this.passkeyApi.post('/passkeys/finish-login/read', {
206
+ headers: {
207
+ 'Content-Type': 'application/json',
208
+ Authorization: `Bearer ${this.api.apiKey}`,
209
+ },
210
+ body: {
211
+ assertion: JSON.stringify(assertion),
212
+ sessionId: this.sessionId,
213
+ relyingParty: this.relyingParty,
214
+ relyingPartyOrigins: this.relyingPartyOrigins,
215
+ },
216
+ });
217
+ // check if encryptionKey exists
218
+ if (!finishResponse.encryptionKey) {
219
+ throw new Error('No encryption key found. Please try again.');
220
+ }
221
+ return finishResponse.encryptionKey;
222
+ });
223
+ }
224
+ }
225
+ exports.PasskeyStorage = PasskeyStorage;
226
+ // Assuming 'challenge' is the base64 URL-encoded string received from the server
227
+ function convertBase64UrlToBase64(challenge) {
228
+ let base64 = challenge.replace(/-/g, '+').replace(/_/g, '/');
229
+ while (base64.length % 4 !== 0) {
230
+ base64 += '=';
231
+ }
232
+ return base64;
233
+ }
234
+ function convertBase64ToBase64Url(base64) {
235
+ // Replace '+' with '-' and '/' with '_'
236
+ let base64Url = base64.replace(/\+/g, '-').replace(/\//g, '_');
237
+ // Remove any '=' padding
238
+ base64Url = base64Url.replace(/=+$/, '');
239
+ return base64Url;
240
+ }
241
+ var PasskeyStatus;
242
+ (function (PasskeyStatus) {
243
+ PasskeyStatus["NotRegistered"] = "not registered";
244
+ PasskeyStatus["Registered"] = "registered";
245
+ PasskeyStatus["RegisteredWithCredential"] = "registered with credential";
246
+ })(PasskeyStatus = exports.PasskeyStatus || (exports.PasskeyStatus = {}));
247
+ exports.default = PasskeyStorage;
@@ -0,0 +1,243 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { HttpRequester } from '@portal-hq/utils';
11
+ import { Passkey, } from 'react-native-passkey';
12
+ export class PasskeyStorage {
13
+ constructor(opts) {
14
+ this.relyingParty = 'portalhq.io';
15
+ this.webAuthnHost = 'https://backup.web.portalhq.io';
16
+ this.relyingPartyOrigins = [
17
+ 'https://portalhq.io',
18
+ 'https://backup.web.portalhq.io',
19
+ ];
20
+ this.relyingParty = opts.relyingParty || this.relyingParty;
21
+ this.relyingPartyOrigins =
22
+ opts.relyingPartyOrigins || this.relyingPartyOrigins;
23
+ this.webAuthnHost = opts.webAuthnHost || this.webAuthnHost;
24
+ this.passkeyApi = new HttpRequester({
25
+ baseUrl: this.webAuthnHost,
26
+ });
27
+ }
28
+ delete() {
29
+ throw new Error('Method is not supported.');
30
+ }
31
+ // eslint-disable-next-line @typescript-eslint/require-await
32
+ read() {
33
+ var _a;
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ try {
36
+ if (!((_a = this.api) === null || _a === void 0 ? void 0 : _a.apiKey)) {
37
+ throw new Error('API key is not set');
38
+ }
39
+ const apiKey = this.api.apiKey;
40
+ // Construct the headers object
41
+ const headers = {
42
+ Accept: 'application/json',
43
+ 'Content-Type': 'application/json',
44
+ Authorization: `Bearer ${apiKey}`,
45
+ };
46
+ const assertion = yield this.getAssertionAndSetSessionId(headers);
47
+ // finish login
48
+ const encryptionKey = yield this.getEncryptionKey(assertion);
49
+ return encryptionKey;
50
+ }
51
+ catch (error) {
52
+ if (error instanceof Error) {
53
+ throw new Error(`Failed to complete read operation: ${error.message}`);
54
+ }
55
+ else {
56
+ throw new Error('Failed to complete read operation due to an unexpected error');
57
+ }
58
+ }
59
+ });
60
+ }
61
+ write(privateKey) {
62
+ var _a;
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ try {
65
+ if (!((_a = this.api) === null || _a === void 0 ? void 0 : _a.apiKey)) {
66
+ throw new Error('API key is not set');
67
+ }
68
+ const apiKey = this.api.apiKey;
69
+ // Construct the headers object
70
+ const headers = {
71
+ Accept: 'application/json',
72
+ 'Content-Type': 'application/json',
73
+ Authorization: `Bearer ${apiKey}`,
74
+ };
75
+ // Get the status of the passkey
76
+ const passkeyStatus = yield this.passkeyApi.get('/passkeys/status', {
77
+ headers,
78
+ });
79
+ // If the passkey is registered with a credential, we login
80
+ if (passkeyStatus.status === PasskeyStatus.RegisteredWithCredential) {
81
+ // Login
82
+ const assertion = yield this.getAssertionAndSetSessionId(headers);
83
+ yield this.passkeyApi.post('/passkeys/finish-login/write', {
84
+ headers: {
85
+ 'Content-Type': 'application/json',
86
+ Authorization: `Bearer ${apiKey}`,
87
+ },
88
+ body: {
89
+ assertion: JSON.stringify(assertion),
90
+ sessionId: this.sessionId,
91
+ encryptionKey: privateKey,
92
+ relyingParty: this.relyingParty,
93
+ relyingPartyOrigins: this.relyingPartyOrigins,
94
+ },
95
+ });
96
+ }
97
+ else {
98
+ // Register
99
+ const attestation = yield this.getAttestationAndSetSessionId(headers);
100
+ yield this.passkeyApi.post('/passkeys/finish-registration', {
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ Authorization: `Bearer ${apiKey}`,
104
+ },
105
+ body: {
106
+ attestation: JSON.stringify(attestation),
107
+ sessionId: this.sessionId,
108
+ encryptionKey: privateKey,
109
+ relyingParty: this.relyingParty,
110
+ relyingPartyOrigins: this.relyingPartyOrigins,
111
+ },
112
+ });
113
+ }
114
+ return 'done';
115
+ }
116
+ catch (error) {
117
+ if (error instanceof Error) {
118
+ throw new Error(`Failed to complete write operation: ${error.message}`);
119
+ }
120
+ else {
121
+ throw new Error('Failed to complete write operation due to an unexpected error');
122
+ }
123
+ }
124
+ });
125
+ }
126
+ // eslint-disable-next-line @typescript-eslint/require-await
127
+ validateOperations() {
128
+ return __awaiter(this, void 0, void 0, function* () {
129
+ const isSupported = Passkey.isSupported();
130
+ return isSupported;
131
+ });
132
+ }
133
+ // Helper Methods
134
+ getAttestationAndSetSessionId(headers) {
135
+ return __awaiter(this, void 0, void 0, function* () {
136
+ const beginResponse = yield this.passkeyApi.post('/passkeys/begin-registration', {
137
+ headers,
138
+ body: {
139
+ relyingParty: this.relyingParty,
140
+ relyingPartyOrigins: this.relyingPartyOrigins,
141
+ },
142
+ });
143
+ // check if challenge exists
144
+ if (!beginResponse.options.publicKey.challenge) {
145
+ throw new Error('Challenge is missing in passkey options. Please try again and check your web authn host url.');
146
+ }
147
+ beginResponse.options.publicKey.challenge = convertBase64UrlToBase64(beginResponse.options.publicKey.challenge);
148
+ const result = yield Passkey.register(beginResponse.options.publicKey);
149
+ const attestation = {
150
+ rawId: convertBase64ToBase64Url(result.rawId),
151
+ id: convertBase64ToBase64Url(result.id),
152
+ type: 'public-key',
153
+ response: {
154
+ clientDataJSON: convertBase64ToBase64Url(result.response.clientDataJSON),
155
+ attestationObject: convertBase64ToBase64Url(result.response.attestationObject),
156
+ },
157
+ };
158
+ // check if sessionId exists
159
+ if (!beginResponse.sessionId) {
160
+ throw new Error('Session ID is missing in passkey options. Please try again and check your web authn host url.');
161
+ }
162
+ this.sessionId = beginResponse.sessionId;
163
+ return attestation;
164
+ });
165
+ }
166
+ getAssertionAndSetSessionId(headers) {
167
+ return __awaiter(this, void 0, void 0, function* () {
168
+ const beginResponse = yield this.passkeyApi.post('/passkeys/begin-login', {
169
+ headers,
170
+ body: {
171
+ relyingParty: this.relyingParty,
172
+ relyingPartyOrigins: this.relyingPartyOrigins,
173
+ },
174
+ });
175
+ // check if challenge exists
176
+ if (!beginResponse.options.publicKey.challenge) {
177
+ throw new Error('Challenge is missing in passkey options. Please try again and check your web authn host url.');
178
+ }
179
+ beginResponse.options.publicKey.challenge = convertBase64UrlToBase64(beginResponse.options.publicKey.challenge);
180
+ const result = yield Passkey.authenticate(beginResponse.options.publicKey);
181
+ const assertion = {
182
+ rawId: convertBase64ToBase64Url(result.rawId),
183
+ id: convertBase64ToBase64Url(result.id),
184
+ type: 'public-key',
185
+ response: {
186
+ clientDataJSON: convertBase64ToBase64Url(result.response.clientDataJSON),
187
+ authenticatorData: convertBase64ToBase64Url(result.response.authenticatorData),
188
+ signature: convertBase64ToBase64Url(result.response.signature),
189
+ userHandle: convertBase64ToBase64Url(result.response.userHandle),
190
+ },
191
+ };
192
+ this.sessionId = beginResponse.sessionId;
193
+ return assertion;
194
+ });
195
+ }
196
+ getEncryptionKey(assertion) {
197
+ var _a;
198
+ return __awaiter(this, void 0, void 0, function* () {
199
+ if (((_a = this.api) === null || _a === void 0 ? void 0 : _a.apiKey) === undefined) {
200
+ throw new Error('API key is not set');
201
+ }
202
+ const finishResponse = yield this.passkeyApi.post('/passkeys/finish-login/read', {
203
+ headers: {
204
+ 'Content-Type': 'application/json',
205
+ Authorization: `Bearer ${this.api.apiKey}`,
206
+ },
207
+ body: {
208
+ assertion: JSON.stringify(assertion),
209
+ sessionId: this.sessionId,
210
+ relyingParty: this.relyingParty,
211
+ relyingPartyOrigins: this.relyingPartyOrigins,
212
+ },
213
+ });
214
+ // check if encryptionKey exists
215
+ if (!finishResponse.encryptionKey) {
216
+ throw new Error('No encryption key found. Please try again.');
217
+ }
218
+ return finishResponse.encryptionKey;
219
+ });
220
+ }
221
+ }
222
+ // Assuming 'challenge' is the base64 URL-encoded string received from the server
223
+ function convertBase64UrlToBase64(challenge) {
224
+ let base64 = challenge.replace(/-/g, '+').replace(/_/g, '/');
225
+ while (base64.length % 4 !== 0) {
226
+ base64 += '=';
227
+ }
228
+ return base64;
229
+ }
230
+ function convertBase64ToBase64Url(base64) {
231
+ // Replace '+' with '-' and '/' with '_'
232
+ let base64Url = base64.replace(/\+/g, '-').replace(/\//g, '_');
233
+ // Remove any '=' padding
234
+ base64Url = base64Url.replace(/=+$/, '');
235
+ return base64Url;
236
+ }
237
+ export var PasskeyStatus;
238
+ (function (PasskeyStatus) {
239
+ PasskeyStatus["NotRegistered"] = "not registered";
240
+ PasskeyStatus["Registered"] = "registered";
241
+ PasskeyStatus["RegisteredWithCredential"] = "registered with credential";
242
+ })(PasskeyStatus || (PasskeyStatus = {}));
243
+ export default PasskeyStorage;
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@portal-hq/passkey-storage",
3
+ "version": "3.0.5",
4
+ "description": "Portal's Passkey storage adapter",
5
+ "license": "MIT",
6
+ "author": "Portal Labs, Inc.",
7
+ "homepage": "https://portalhq.io",
8
+ "main": "lib/commonjs/index",
9
+ "module": "lib/esm/index",
10
+ "source": "src/index",
11
+ "types": "src/index",
12
+ "files": [
13
+ "lib",
14
+ "src",
15
+ "types.d.ts"
16
+ ],
17
+ "scripts": {
18
+ "coverage": "jest --collect-coverage",
19
+ "prepare": "yarn prepare:cjs && yarn prepare:esm",
20
+ "prepare:cjs": "tsc --outDir lib/commonjs --module commonjs",
21
+ "prepare:esm": "tsc --outDir lib/esm --module es2015 --target es2015",
22
+ "test": "jest --pass-with-no-tests"
23
+ },
24
+ "dependencies": {
25
+ "@portal-hq/core": "^3.0.5",
26
+ "@portal-hq/utils": "^3.0.5",
27
+ "react-native-passkey": "^2.1.1"
28
+ },
29
+ "devDependencies": {
30
+ "@babel/preset-typescript": "^7.18.6",
31
+ "@types/jest": "^29.2.0",
32
+ "jest": "^29.2.1",
33
+ "jest-environment-jsdom": "^29.2.2",
34
+ "ts-jest": "^29.0.3",
35
+ "typescript": "^4.8.4"
36
+ },
37
+ "peerDependencies": {
38
+ "react-native-passkey": "^2.1.1"
39
+ },
40
+ "gitHead": "58faddeac81625fcb230357540e7f4663f8c810c"
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,304 @@
1
+ import { HttpRequester, IPortalApi, type Storage } from '@portal-hq/utils'
2
+ import {
3
+ Passkey,
4
+ PasskeyAuthenticationResult,
5
+ PasskeyRegistrationResult,
6
+ } from 'react-native-passkey'
7
+
8
+ import type {
9
+ AuthenticationResponse,
10
+ LoginReadResponse,
11
+ PasskeyOptions,
12
+ PasskeyStatusResponse,
13
+ RegistrationResponse,
14
+ } from '../types'
15
+
16
+ export class PasskeyStorage implements Storage {
17
+ public api?: IPortalApi
18
+ private passkeyApi: HttpRequester
19
+ private relyingParty: string = 'portalhq.io'
20
+ private webAuthnHost: string = 'https://backup.web.portalhq.io'
21
+ private relyingPartyOrigins: string[] = [
22
+ 'https://portalhq.io',
23
+ 'https://backup.web.portalhq.io',
24
+ ]
25
+ private sessionId?: string
26
+
27
+ constructor(opts: PasskeyOptions) {
28
+ this.relyingParty = opts.relyingParty || this.relyingParty
29
+ this.relyingPartyOrigins =
30
+ opts.relyingPartyOrigins || this.relyingPartyOrigins
31
+ this.webAuthnHost = opts.webAuthnHost || this.webAuthnHost
32
+ this.passkeyApi = new HttpRequester({
33
+ baseUrl: this.webAuthnHost,
34
+ })
35
+ }
36
+
37
+ delete(): Promise<boolean> {
38
+ throw new Error('Method is not supported.')
39
+ }
40
+
41
+ // eslint-disable-next-line @typescript-eslint/require-await
42
+ public async read(): Promise<string> {
43
+ try {
44
+ if (!this.api?.apiKey) {
45
+ throw new Error('API key is not set')
46
+ }
47
+
48
+ const apiKey = this.api.apiKey
49
+
50
+ // Construct the headers object
51
+ const headers = {
52
+ Accept: 'application/json',
53
+ 'Content-Type': 'application/json',
54
+ Authorization: `Bearer ${apiKey}`,
55
+ }
56
+ const assertion = await this.getAssertionAndSetSessionId(headers)
57
+
58
+ // finish login
59
+ const encryptionKey = await this.getEncryptionKey(assertion)
60
+
61
+ return encryptionKey
62
+ } catch (error) {
63
+ if (error instanceof Error) {
64
+ throw new Error(`Failed to complete read operation: ${error.message}`)
65
+ } else {
66
+ throw new Error(
67
+ 'Failed to complete read operation due to an unexpected error',
68
+ )
69
+ }
70
+ }
71
+ }
72
+
73
+ public async write(privateKey: string): Promise<string> {
74
+ try {
75
+ if (!this.api?.apiKey) {
76
+ throw new Error('API key is not set')
77
+ }
78
+
79
+ const apiKey = this.api.apiKey
80
+
81
+ // Construct the headers object
82
+ const headers = {
83
+ Accept: 'application/json',
84
+ 'Content-Type': 'application/json',
85
+ Authorization: `Bearer ${apiKey}`,
86
+ }
87
+
88
+ // Get the status of the passkey
89
+ const passkeyStatus = await this.passkeyApi.get<PasskeyStatusResponse>(
90
+ '/passkeys/status',
91
+ {
92
+ headers,
93
+ },
94
+ )
95
+
96
+ // If the passkey is registered with a credential, we login
97
+ if (passkeyStatus.status === PasskeyStatus.RegisteredWithCredential) {
98
+ // Login
99
+ const assertion = await this.getAssertionAndSetSessionId(headers)
100
+
101
+ await this.passkeyApi.post<string>('/passkeys/finish-login/write', {
102
+ headers: {
103
+ 'Content-Type': 'application/json',
104
+ Authorization: `Bearer ${apiKey}`,
105
+ },
106
+ body: {
107
+ assertion: JSON.stringify(assertion),
108
+ sessionId: this.sessionId,
109
+ encryptionKey: privateKey,
110
+ relyingParty: this.relyingParty,
111
+ relyingPartyOrigins: this.relyingPartyOrigins,
112
+ },
113
+ })
114
+ } else {
115
+ // Register
116
+ const attestation = await this.getAttestationAndSetSessionId(headers)
117
+
118
+ await this.passkeyApi.post<string>('/passkeys/finish-registration', {
119
+ headers: {
120
+ 'Content-Type': 'application/json',
121
+ Authorization: `Bearer ${apiKey}`,
122
+ },
123
+ body: {
124
+ attestation: JSON.stringify(attestation),
125
+ sessionId: this.sessionId,
126
+ encryptionKey: privateKey,
127
+ relyingParty: this.relyingParty,
128
+ relyingPartyOrigins: this.relyingPartyOrigins,
129
+ },
130
+ })
131
+ }
132
+
133
+ return 'done'
134
+ } catch (error) {
135
+ if (error instanceof Error) {
136
+ throw new Error(`Failed to complete write operation: ${error.message}`)
137
+ } else {
138
+ throw new Error(
139
+ 'Failed to complete write operation due to an unexpected error',
140
+ )
141
+ }
142
+ }
143
+ }
144
+
145
+ // eslint-disable-next-line @typescript-eslint/require-await
146
+ public async validateOperations(): Promise<boolean> {
147
+ const isSupported: boolean = Passkey.isSupported()
148
+ return isSupported
149
+ }
150
+
151
+ // Helper Methods
152
+ private async getAttestationAndSetSessionId(
153
+ headers: Record<string, any>,
154
+ ): Promise<PasskeyRegistrationResult> {
155
+ const beginResponse = await this.passkeyApi.post<RegistrationResponse>(
156
+ '/passkeys/begin-registration',
157
+ {
158
+ headers,
159
+ body: {
160
+ relyingParty: this.relyingParty,
161
+ relyingPartyOrigins: this.relyingPartyOrigins,
162
+ },
163
+ },
164
+ )
165
+
166
+ // check if challenge exists
167
+ if (!beginResponse.options.publicKey.challenge) {
168
+ throw new Error(
169
+ 'Challenge is missing in passkey options. Please try again and check your web authn host url.',
170
+ )
171
+ }
172
+ beginResponse.options.publicKey.challenge = convertBase64UrlToBase64(
173
+ beginResponse.options.publicKey.challenge,
174
+ )
175
+
176
+ const result: PasskeyRegistrationResult = await Passkey.register(
177
+ beginResponse.options.publicKey,
178
+ )
179
+ const attestation = {
180
+ rawId: convertBase64ToBase64Url(result.rawId),
181
+ id: convertBase64ToBase64Url(result.id),
182
+ type: 'public-key',
183
+ response: {
184
+ clientDataJSON: convertBase64ToBase64Url(
185
+ result.response.clientDataJSON,
186
+ ),
187
+ attestationObject: convertBase64ToBase64Url(
188
+ result.response.attestationObject,
189
+ ),
190
+ },
191
+ }
192
+
193
+ // check if sessionId exists
194
+ if (!beginResponse.sessionId) {
195
+ throw new Error(
196
+ 'Session ID is missing in passkey options. Please try again and check your web authn host url.',
197
+ )
198
+ }
199
+ this.sessionId = beginResponse.sessionId
200
+
201
+ return attestation
202
+ }
203
+
204
+ private async getAssertionAndSetSessionId(
205
+ headers: Record<string, any>,
206
+ ): Promise<PasskeyAuthenticationResult> {
207
+ const beginResponse = await this.passkeyApi.post<AuthenticationResponse>(
208
+ '/passkeys/begin-login',
209
+ {
210
+ headers,
211
+ body: {
212
+ relyingParty: this.relyingParty,
213
+ relyingPartyOrigins: this.relyingPartyOrigins,
214
+ },
215
+ },
216
+ )
217
+
218
+ // check if challenge exists
219
+ if (!beginResponse.options.publicKey.challenge) {
220
+ throw new Error(
221
+ 'Challenge is missing in passkey options. Please try again and check your web authn host url.',
222
+ )
223
+ }
224
+ beginResponse.options.publicKey.challenge = convertBase64UrlToBase64(
225
+ beginResponse.options.publicKey.challenge,
226
+ )
227
+
228
+ const result: PasskeyAuthenticationResult = await Passkey.authenticate(
229
+ beginResponse.options.publicKey,
230
+ )
231
+
232
+ const assertion = {
233
+ rawId: convertBase64ToBase64Url(result.rawId),
234
+ id: convertBase64ToBase64Url(result.id),
235
+ type: 'public-key',
236
+ response: {
237
+ clientDataJSON: convertBase64ToBase64Url(
238
+ result.response.clientDataJSON,
239
+ ),
240
+ authenticatorData: convertBase64ToBase64Url(
241
+ result.response.authenticatorData,
242
+ ),
243
+ signature: convertBase64ToBase64Url(result.response.signature),
244
+ userHandle: convertBase64ToBase64Url(result.response.userHandle),
245
+ },
246
+ }
247
+
248
+ this.sessionId = beginResponse.sessionId
249
+ return assertion
250
+ }
251
+
252
+ private async getEncryptionKey(
253
+ assertion: PasskeyAuthenticationResult,
254
+ ): Promise<string> {
255
+ if (this.api?.apiKey === undefined) {
256
+ throw new Error('API key is not set')
257
+ }
258
+
259
+ const finishResponse = await this.passkeyApi.post<LoginReadResponse>(
260
+ '/passkeys/finish-login/read',
261
+ {
262
+ headers: {
263
+ 'Content-Type': 'application/json',
264
+ Authorization: `Bearer ${this.api.apiKey}`,
265
+ },
266
+ body: {
267
+ assertion: JSON.stringify(assertion),
268
+ sessionId: this.sessionId,
269
+ relyingParty: this.relyingParty,
270
+ relyingPartyOrigins: this.relyingPartyOrigins,
271
+ },
272
+ },
273
+ )
274
+ // check if encryptionKey exists
275
+ if (!finishResponse.encryptionKey) {
276
+ throw new Error('No encryption key found. Please try again.')
277
+ }
278
+ return finishResponse.encryptionKey
279
+ }
280
+ }
281
+ // Assuming 'challenge' is the base64 URL-encoded string received from the server
282
+ function convertBase64UrlToBase64(challenge: string): string {
283
+ let base64 = challenge.replace(/-/g, '+').replace(/_/g, '/')
284
+ while (base64.length % 4 !== 0) {
285
+ base64 += '='
286
+ }
287
+ return base64
288
+ }
289
+
290
+ function convertBase64ToBase64Url(base64: string) {
291
+ // Replace '+' with '-' and '/' with '_'
292
+ let base64Url = base64.replace(/\+/g, '-').replace(/\//g, '_')
293
+ // Remove any '=' padding
294
+ base64Url = base64Url.replace(/=+$/, '')
295
+ return base64Url
296
+ }
297
+
298
+ export enum PasskeyStatus {
299
+ NotRegistered = 'not registered',
300
+ Registered = 'registered',
301
+ RegisteredWithCredential = 'registered with credential',
302
+ }
303
+
304
+ export default PasskeyStorage
package/types.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ import {
2
+ PasskeyRegistrationRequest,
3
+ PasskeyAuthenticationRequest,
4
+ } from 'react-native-passkey/lib/typescript/Passkey'
5
+ import { PasskeyStatus } from './src'
6
+
7
+ export declare class PasskeyStorage {
8
+ public accessToken?: string
9
+ public api: HttpRequester
10
+
11
+ constructor(options: PasskeyOptions)
12
+
13
+ public async read(): Promise<string | null>
14
+
15
+ public async write(content: string): Promise<string | null>
16
+
17
+ public async validateOperations(): Promise<boolean>
18
+ }
19
+
20
+ export interface PasskeyOptions {
21
+ relyingParty?: string
22
+ webAuthnHost?: string
23
+ relyingPartyOrigins?: string[]
24
+ }
25
+
26
+ export interface PasskeyStatusResponse {
27
+ status: PasskeyStatus
28
+ }
29
+
30
+ export interface RegistrationResponse {
31
+ options: RegistrationOptions
32
+ sessionId: string
33
+ }
34
+
35
+ export interface RegistrationOptions {
36
+ publicKey: PasskeyRegistrationRequest
37
+ }
38
+
39
+ export interface AuthenticationResponse {
40
+ options: AuthenticationOptions
41
+ sessionId: string
42
+ }
43
+
44
+ export interface AuthenticationOptions {
45
+ publicKey: PasskeyAuthenticationRequest
46
+ }
47
+
48
+ export interface LoginReadResponse {
49
+ encryptionKey: string
50
+ }