@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 +13 -0
- package/lib/commonjs/index.js +247 -0
- package/lib/esm/index.js +243 -0
- package/package.json +41 -0
- package/src/index.ts +304 -0
- package/types.d.ts +50 -0
package/README.md
ADDED
|
@@ -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;
|
package/lib/esm/index.js
ADDED
|
@@ -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
|
+
}
|