dauth-context-react 6.1.0 → 6.3.0
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/dist/index.d.mts +24 -2
- package/dist/index.d.ts +24 -2
- package/dist/index.js +1249 -164
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1249 -164
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/DauthProfileModal.tsx +1285 -246
- package/src/api/dauth.api.ts +73 -0
- package/src/api/interfaces/dauth.api.responses.ts +36 -0
- package/src/index.tsx +33 -1
- package/src/initialDauthState.ts +3 -0
- package/src/interfaces.ts +28 -0
- package/src/reducer/dauth.actions.ts +91 -0
- package/src/webauthn.ts +111 -0
package/src/api/dauth.api.ts
CHANGED
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
ISessionResponse,
|
|
5
5
|
IUpdateUserResponse,
|
|
6
6
|
IDeleteAccountResponse,
|
|
7
|
+
IPasskeyCredentialsResponse,
|
|
8
|
+
IPasskeyRegistrationStartResponse,
|
|
9
|
+
IPasskeyRegistrationFinishResponse,
|
|
10
|
+
IDeletePasskeyResponse,
|
|
7
11
|
} from './interfaces/dauth.api.responses';
|
|
8
12
|
|
|
9
13
|
function getCsrfToken(): string {
|
|
@@ -85,3 +89,72 @@ export async function deleteAccountAPI(
|
|
|
85
89
|
const data = await response.json();
|
|
86
90
|
return { response, data };
|
|
87
91
|
}
|
|
92
|
+
|
|
93
|
+
export async function getPasskeyCredentialsAPI(
|
|
94
|
+
basePath: string
|
|
95
|
+
): Promise<IPasskeyCredentialsResponse> {
|
|
96
|
+
const response = await fetch(
|
|
97
|
+
`${basePath}/passkey/credentials`,
|
|
98
|
+
{
|
|
99
|
+
method: 'GET',
|
|
100
|
+
headers: { 'X-CSRF-Token': getCsrfToken() },
|
|
101
|
+
credentials: 'include',
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
return { response, data };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function startPasskeyRegistrationAPI(
|
|
109
|
+
basePath: string
|
|
110
|
+
): Promise<IPasskeyRegistrationStartResponse> {
|
|
111
|
+
const response = await fetch(
|
|
112
|
+
`${basePath}/passkey/register/start`,
|
|
113
|
+
{
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
'X-CSRF-Token': getCsrfToken(),
|
|
118
|
+
},
|
|
119
|
+
credentials: 'include',
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
return { response, data };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function finishPasskeyRegistrationAPI(
|
|
127
|
+
basePath: string,
|
|
128
|
+
body: { credential: unknown; name?: string }
|
|
129
|
+
): Promise<IPasskeyRegistrationFinishResponse> {
|
|
130
|
+
const response = await fetch(
|
|
131
|
+
`${basePath}/passkey/register/finish`,
|
|
132
|
+
{
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
'X-CSRF-Token': getCsrfToken(),
|
|
137
|
+
},
|
|
138
|
+
credentials: 'include',
|
|
139
|
+
body: JSON.stringify(body),
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
const data = await response.json();
|
|
143
|
+
return { response, data };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function deletePasskeyCredentialAPI(
|
|
147
|
+
basePath: string,
|
|
148
|
+
credentialId: string
|
|
149
|
+
): Promise<IDeletePasskeyResponse> {
|
|
150
|
+
const response = await fetch(
|
|
151
|
+
`${basePath}/passkey/credentials/${credentialId}`,
|
|
152
|
+
{
|
|
153
|
+
method: 'DELETE',
|
|
154
|
+
headers: { 'X-CSRF-Token': getCsrfToken() },
|
|
155
|
+
credentials: 'include',
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
const data = await response.json();
|
|
159
|
+
return { response, data };
|
|
160
|
+
}
|
|
@@ -33,3 +33,39 @@ export interface IDeleteAccountResponse {
|
|
|
33
33
|
message: string;
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
export interface IPasskeyCredential {
|
|
38
|
+
_id: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
deviceType: 'singleDevice' | 'multiDevice';
|
|
41
|
+
backedUp: boolean;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
lastUsedAt?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface IPasskeyCredentialsResponse {
|
|
47
|
+
response: Response;
|
|
48
|
+
data: {
|
|
49
|
+
credentials: IPasskeyCredential[];
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface IPasskeyRegistrationStartResponse {
|
|
54
|
+
response: Response;
|
|
55
|
+
data: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface IPasskeyRegistrationFinishResponse {
|
|
59
|
+
response: Response;
|
|
60
|
+
data: {
|
|
61
|
+
credential: IPasskeyCredential;
|
|
62
|
+
message: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface IDeletePasskeyResponse {
|
|
67
|
+
response: Response;
|
|
68
|
+
data: {
|
|
69
|
+
message: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -17,7 +17,9 @@ import type {
|
|
|
17
17
|
IDauthAuthMethods,
|
|
18
18
|
IDauthUser,
|
|
19
19
|
IFormField,
|
|
20
|
+
ICustomField,
|
|
20
21
|
IModalTheme,
|
|
22
|
+
IPasskeyCredential,
|
|
21
23
|
DauthProfileModalProps,
|
|
22
24
|
} from './interfaces';
|
|
23
25
|
|
|
@@ -26,7 +28,9 @@ export type {
|
|
|
26
28
|
IDauthProviderProps,
|
|
27
29
|
IDauthAuthMethods,
|
|
28
30
|
IFormField,
|
|
31
|
+
ICustomField,
|
|
29
32
|
IModalTheme,
|
|
33
|
+
IPasskeyCredential,
|
|
30
34
|
DauthProfileModalProps,
|
|
31
35
|
};
|
|
32
36
|
|
|
@@ -115,6 +119,22 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
|
|
|
115
119
|
[ctx]
|
|
116
120
|
);
|
|
117
121
|
|
|
122
|
+
const getPasskeyCredentials = useCallback(
|
|
123
|
+
() => action.getPasskeyCredentialsAction(ctx),
|
|
124
|
+
[ctx]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const registerPasskey = useCallback(
|
|
128
|
+
(name?: string) => action.registerPasskeyAction(ctx, name),
|
|
129
|
+
[ctx]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const deletePasskeyCredential = useCallback(
|
|
133
|
+
(credentialId: string) =>
|
|
134
|
+
action.deletePasskeyCredentialAction(ctx, credentialId),
|
|
135
|
+
[ctx]
|
|
136
|
+
);
|
|
137
|
+
|
|
118
138
|
const memoProvider = useMemo(
|
|
119
139
|
() => ({
|
|
120
140
|
...dauthState,
|
|
@@ -122,8 +142,20 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
|
|
|
122
142
|
logout,
|
|
123
143
|
updateUser,
|
|
124
144
|
deleteAccount,
|
|
145
|
+
getPasskeyCredentials,
|
|
146
|
+
registerPasskey,
|
|
147
|
+
deletePasskeyCredential,
|
|
125
148
|
}),
|
|
126
|
-
[
|
|
149
|
+
[
|
|
150
|
+
dauthState,
|
|
151
|
+
loginWithRedirect,
|
|
152
|
+
logout,
|
|
153
|
+
updateUser,
|
|
154
|
+
deleteAccount,
|
|
155
|
+
getPasskeyCredentials,
|
|
156
|
+
registerPasskey,
|
|
157
|
+
deletePasskeyCredential,
|
|
158
|
+
]
|
|
127
159
|
);
|
|
128
160
|
|
|
129
161
|
return (
|
package/src/initialDauthState.ts
CHANGED
|
@@ -14,6 +14,9 @@ const initialDauthState: IDauthState = {
|
|
|
14
14
|
logout: () => {},
|
|
15
15
|
updateUser: () => Promise.resolve(false),
|
|
16
16
|
deleteAccount: () => Promise.resolve(false),
|
|
17
|
+
getPasskeyCredentials: () => Promise.resolve([]),
|
|
18
|
+
registerPasskey: () => Promise.resolve(null),
|
|
19
|
+
deletePasskeyCredential: () => Promise.resolve(false),
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
export default initialDauthState;
|
package/src/interfaces.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface IDauthUser {
|
|
|
16
16
|
birthDate?: Date;
|
|
17
17
|
country?: string;
|
|
18
18
|
metadata?: Record<string, unknown>;
|
|
19
|
+
customFields?: Record<string, string>;
|
|
19
20
|
createdAt: Date;
|
|
20
21
|
updatedAt: Date;
|
|
21
22
|
lastLogin: Date;
|
|
@@ -36,6 +37,12 @@ export interface IFormField {
|
|
|
36
37
|
required: boolean;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export interface ICustomField {
|
|
41
|
+
key: string;
|
|
42
|
+
label: string;
|
|
43
|
+
required: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
export interface IModalTheme {
|
|
40
47
|
accent?: string;
|
|
41
48
|
accentHover?: string;
|
|
@@ -53,9 +60,19 @@ export interface IDauthDomainState {
|
|
|
53
60
|
allowedOrigins: string[];
|
|
54
61
|
authMethods?: IDauthAuthMethods;
|
|
55
62
|
formFields?: IFormField[];
|
|
63
|
+
customFields?: ICustomField[];
|
|
56
64
|
modalTheme?: IModalTheme;
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
export interface IPasskeyCredential {
|
|
68
|
+
_id: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
deviceType: 'singleDevice' | 'multiDevice';
|
|
71
|
+
backedUp: boolean;
|
|
72
|
+
createdAt: string;
|
|
73
|
+
lastUsedAt?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
export interface IDauthState {
|
|
60
77
|
user: IDauthUser;
|
|
61
78
|
domain: IDauthDomainState;
|
|
@@ -65,11 +82,22 @@ export interface IDauthState {
|
|
|
65
82
|
logout: () => void;
|
|
66
83
|
updateUser: (fields: Partial<IDauthUser>) => Promise<boolean>;
|
|
67
84
|
deleteAccount: () => Promise<boolean>;
|
|
85
|
+
getPasskeyCredentials: () => Promise<IPasskeyCredential[]>;
|
|
86
|
+
registerPasskey: (
|
|
87
|
+
name?: string
|
|
88
|
+
) => Promise<IPasskeyCredential | null>;
|
|
89
|
+
deletePasskeyCredential: (
|
|
90
|
+
credentialId: string
|
|
91
|
+
) => Promise<boolean>;
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
export interface DauthProfileModalProps {
|
|
71
95
|
open: boolean;
|
|
72
96
|
onClose: () => void;
|
|
97
|
+
/** Optional: provide a function to handle avatar upload.
|
|
98
|
+
* Receives a File, should return the URL string.
|
|
99
|
+
* If not provided, the avatar edit button is hidden. */
|
|
100
|
+
onAvatarUpload?: (file: File) => Promise<string>;
|
|
73
101
|
}
|
|
74
102
|
|
|
75
103
|
export interface IActionStatus {
|
|
@@ -4,7 +4,13 @@ import {
|
|
|
4
4
|
logoutAPI,
|
|
5
5
|
updateUserAPI,
|
|
6
6
|
deleteAccountAPI,
|
|
7
|
+
getPasskeyCredentialsAPI,
|
|
8
|
+
startPasskeyRegistrationAPI,
|
|
9
|
+
finishPasskeyRegistrationAPI,
|
|
10
|
+
deletePasskeyCredentialAPI,
|
|
7
11
|
} from '../api/dauth.api';
|
|
12
|
+
import type { IPasskeyCredential } from '../api/interfaces/dauth.api.responses';
|
|
13
|
+
import { createPasskeyCredential } from '../webauthn';
|
|
8
14
|
import { IDauthDomainState, IDauthUser } from '../interfaces';
|
|
9
15
|
import * as DauthTypes from './dauth.types';
|
|
10
16
|
|
|
@@ -148,6 +154,91 @@ export async function deleteAccountAction(
|
|
|
148
154
|
}
|
|
149
155
|
}
|
|
150
156
|
|
|
157
|
+
export async function getPasskeyCredentialsAction(
|
|
158
|
+
ctx: ActionContext
|
|
159
|
+
): Promise<IPasskeyCredential[]> {
|
|
160
|
+
const { authProxyPath, onError } = ctx;
|
|
161
|
+
try {
|
|
162
|
+
const result = await getPasskeyCredentialsAPI(authProxyPath);
|
|
163
|
+
if (result.response.status === 200) {
|
|
164
|
+
return result.data.credentials ?? [];
|
|
165
|
+
}
|
|
166
|
+
return [];
|
|
167
|
+
} catch (error) {
|
|
168
|
+
onError(
|
|
169
|
+
error instanceof Error
|
|
170
|
+
? error
|
|
171
|
+
: new Error('Get passkey credentials error')
|
|
172
|
+
);
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function registerPasskeyAction(
|
|
178
|
+
ctx: ActionContext,
|
|
179
|
+
name?: string
|
|
180
|
+
): Promise<IPasskeyCredential | null> {
|
|
181
|
+
const { authProxyPath, onError } = ctx;
|
|
182
|
+
try {
|
|
183
|
+
// Step 1: Get registration options from server
|
|
184
|
+
const startResult =
|
|
185
|
+
await startPasskeyRegistrationAPI(authProxyPath);
|
|
186
|
+
if (startResult.response.status !== 200) {
|
|
187
|
+
onError(new Error('Failed to start passkey registration'));
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Step 2: Execute WebAuthn ceremony in the browser
|
|
192
|
+
const credential = await createPasskeyCredential(
|
|
193
|
+
startResult.data
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Step 3: Send the credential back to the server
|
|
197
|
+
const finishResult = await finishPasskeyRegistrationAPI(
|
|
198
|
+
authProxyPath,
|
|
199
|
+
{ credential, name }
|
|
200
|
+
);
|
|
201
|
+
if (
|
|
202
|
+
finishResult.response.status === 200 ||
|
|
203
|
+
finishResult.response.status === 201
|
|
204
|
+
) {
|
|
205
|
+
return finishResult.data.credential;
|
|
206
|
+
}
|
|
207
|
+
onError(
|
|
208
|
+
new Error('Failed to finish passkey registration')
|
|
209
|
+
);
|
|
210
|
+
return null;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
onError(
|
|
213
|
+
error instanceof Error
|
|
214
|
+
? error
|
|
215
|
+
: new Error('Passkey registration error')
|
|
216
|
+
);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function deletePasskeyCredentialAction(
|
|
222
|
+
ctx: ActionContext,
|
|
223
|
+
credentialId: string
|
|
224
|
+
): Promise<boolean> {
|
|
225
|
+
const { authProxyPath, onError } = ctx;
|
|
226
|
+
try {
|
|
227
|
+
const result = await deletePasskeyCredentialAPI(
|
|
228
|
+
authProxyPath,
|
|
229
|
+
credentialId
|
|
230
|
+
);
|
|
231
|
+
return result.response.status === 200;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
onError(
|
|
234
|
+
error instanceof Error
|
|
235
|
+
? error
|
|
236
|
+
: new Error('Delete passkey error')
|
|
237
|
+
);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
151
242
|
export const resetUser = (dispatch: React.Dispatch<any>) => {
|
|
152
243
|
return dispatch({
|
|
153
244
|
type: DauthTypes.LOGIN,
|
package/src/webauthn.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal WebAuthn helpers for passkey registration.
|
|
3
|
+
* Uses the raw Web Credentials API — no external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function base64urlToBuffer(
|
|
7
|
+
base64url: string
|
|
8
|
+
): ArrayBuffer {
|
|
9
|
+
const base64 = base64url
|
|
10
|
+
.replace(/-/g, '+')
|
|
11
|
+
.replace(/_/g, '/');
|
|
12
|
+
const pad = base64.length % 4;
|
|
13
|
+
const padded = pad ? base64 + '='.repeat(4 - pad) : base64;
|
|
14
|
+
const binary = atob(padded);
|
|
15
|
+
const bytes = new Uint8Array(binary.length);
|
|
16
|
+
for (let i = 0; i < binary.length; i++) {
|
|
17
|
+
bytes[i] = binary.charCodeAt(i);
|
|
18
|
+
}
|
|
19
|
+
return bytes.buffer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function bufferToBase64url(
|
|
23
|
+
buffer: ArrayBuffer
|
|
24
|
+
): string {
|
|
25
|
+
const bytes = new Uint8Array(buffer);
|
|
26
|
+
let binary = '';
|
|
27
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
28
|
+
binary += String.fromCharCode(bytes[i]);
|
|
29
|
+
}
|
|
30
|
+
return btoa(binary)
|
|
31
|
+
.replace(/\+/g, '-')
|
|
32
|
+
.replace(/\//g, '_')
|
|
33
|
+
.replace(/=+$/, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Converts server-provided registration options (base64url strings)
|
|
38
|
+
* into the format expected by navigator.credentials.create(),
|
|
39
|
+
* executes the WebAuthn ceremony, and serializes the response
|
|
40
|
+
* back to JSON with base64url-encoded ArrayBuffers.
|
|
41
|
+
*/
|
|
42
|
+
export async function createPasskeyCredential(
|
|
43
|
+
options: Record<string, any>
|
|
44
|
+
): Promise<Record<string, any>> {
|
|
45
|
+
const publicKey = {
|
|
46
|
+
...options,
|
|
47
|
+
challenge: base64urlToBuffer(options.challenge),
|
|
48
|
+
user: {
|
|
49
|
+
...options.user,
|
|
50
|
+
id: base64urlToBuffer(options.user.id),
|
|
51
|
+
},
|
|
52
|
+
excludeCredentials: (options.excludeCredentials ?? []).map(
|
|
53
|
+
(c: any) => ({
|
|
54
|
+
...c,
|
|
55
|
+
id: base64urlToBuffer(c.id),
|
|
56
|
+
})
|
|
57
|
+
),
|
|
58
|
+
} as PublicKeyCredentialCreationOptions;
|
|
59
|
+
|
|
60
|
+
const credential = (await navigator.credentials.create({
|
|
61
|
+
publicKey,
|
|
62
|
+
})) as PublicKeyCredential;
|
|
63
|
+
|
|
64
|
+
if (!credential) {
|
|
65
|
+
throw new Error('Passkey registration was cancelled');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const attestation =
|
|
69
|
+
credential.response as AuthenticatorAttestationResponse;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: credential.id,
|
|
73
|
+
rawId: bufferToBase64url(credential.rawId),
|
|
74
|
+
type: credential.type,
|
|
75
|
+
response: {
|
|
76
|
+
clientDataJSON: bufferToBase64url(
|
|
77
|
+
attestation.clientDataJSON
|
|
78
|
+
),
|
|
79
|
+
attestationObject: bufferToBase64url(
|
|
80
|
+
attestation.attestationObject
|
|
81
|
+
),
|
|
82
|
+
...(attestation.getTransports
|
|
83
|
+
? { transports: attestation.getTransports() }
|
|
84
|
+
: {}),
|
|
85
|
+
...(attestation.getPublicKeyAlgorithm
|
|
86
|
+
? {
|
|
87
|
+
publicKeyAlgorithm:
|
|
88
|
+
attestation.getPublicKeyAlgorithm(),
|
|
89
|
+
}
|
|
90
|
+
: {}),
|
|
91
|
+
...(attestation.getPublicKey
|
|
92
|
+
? {
|
|
93
|
+
publicKey: bufferToBase64url(
|
|
94
|
+
attestation.getPublicKey()!
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
98
|
+
...(attestation.getAuthenticatorData
|
|
99
|
+
? {
|
|
100
|
+
authenticatorData: bufferToBase64url(
|
|
101
|
+
attestation.getAuthenticatorData()
|
|
102
|
+
),
|
|
103
|
+
}
|
|
104
|
+
: {}),
|
|
105
|
+
},
|
|
106
|
+
clientExtensionResults:
|
|
107
|
+
credential.getClientExtensionResults(),
|
|
108
|
+
authenticatorAttachment:
|
|
109
|
+
credential.authenticatorAttachment ?? undefined,
|
|
110
|
+
};
|
|
111
|
+
}
|