@aurabase/auth 0.6.2 → 0.7.1
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 +44 -8
- package/dist/index.cjs +338 -55
- package/dist/index.d.cts +186 -15
- package/dist/index.d.ts +186 -15
- package/dist/index.js +334 -57
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseService, isTokenExpired, clientScope,
|
|
1
|
+
import { BaseService, AuraError, isTokenExpired, clientScope, refreshOnce, DEFAULT_REQUEST_TIMEOUT_MS, emitAuraEvent } from '@aurabase/core';
|
|
2
2
|
|
|
3
3
|
// src/AuthAdminService.ts
|
|
4
4
|
var AuthAdminService = class extends BaseService {
|
|
@@ -50,9 +50,81 @@ var AuthAdminService = class extends BaseService {
|
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* Génère un lien à usage unique pour un utilisateur (`'magic'`, `'recovery'` ou `'invite'`) et renvoie `{ link, token }`.
|
|
53
|
+
* Accepte soit `generateLink(userId, type)` soit `generateLink({ userId, type })`.
|
|
53
54
|
*/
|
|
54
|
-
async generateLink(
|
|
55
|
-
|
|
55
|
+
async generateLink(userIdOrParams, type) {
|
|
56
|
+
let resolvedUserId;
|
|
57
|
+
let resolvedType;
|
|
58
|
+
if (typeof userIdOrParams === "object" && userIdOrParams !== null) {
|
|
59
|
+
resolvedUserId = userIdOrParams.userId ?? userIdOrParams.user_id;
|
|
60
|
+
resolvedType = userIdOrParams.type ?? type;
|
|
61
|
+
} else if (typeof userIdOrParams === "string") {
|
|
62
|
+
resolvedUserId = userIdOrParams;
|
|
63
|
+
resolvedType = type;
|
|
64
|
+
}
|
|
65
|
+
if (!resolvedUserId || typeof resolvedUserId !== "string" || resolvedUserId.trim() === "" || resolvedUserId.includes("[object")) {
|
|
66
|
+
return {
|
|
67
|
+
data: null,
|
|
68
|
+
error: new AuraError(400, {
|
|
69
|
+
error: {
|
|
70
|
+
code: "INVALID_ARGUMENT",
|
|
71
|
+
message: "userId must be a non-empty string (UUID). Usage: generateLink(userId, type) or generateLink({ userId, type })."
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (!resolvedType) {
|
|
77
|
+
return {
|
|
78
|
+
data: null,
|
|
79
|
+
error: new AuraError(400, {
|
|
80
|
+
error: {
|
|
81
|
+
code: "INVALID_ARGUMENT",
|
|
82
|
+
message: 'type is required for generateLink ("magic" | "recovery" | "invite").'
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return this.request("POST", `${this.prefix}/users/${encodeURIComponent(resolvedUserId)}/generate-link`, { type: resolvedType });
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Impersonation (AUTH-037) : jeton COURT (10 minutes au plus) pour agir en tant que l'utilisateur,
|
|
91
|
+
* avec le claim `act: { sub }`. Pas de refresh token, journalisé (`admin_impersonation`).
|
|
92
|
+
* Un compte `owner`/`admin` exige `allowPrivileged: true` ET une `reason`.
|
|
93
|
+
*/
|
|
94
|
+
async impersonate(userId, options) {
|
|
95
|
+
return this.request("POST", `${this.prefix}/users/${userId}/impersonate`, {
|
|
96
|
+
reason: options?.reason,
|
|
97
|
+
ttl_seconds: options?.ttlSeconds,
|
|
98
|
+
allow_privileged: options?.allowPrivileged ?? false
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Import en masse (AUTH-037) : jusqu'a 1000 utilisateurs par appel, avec hash bcrypt ou argon2id.
|
|
103
|
+
* Rapport ligne par ligne ; une ligne en erreur n'arrete pas les autres.
|
|
104
|
+
*/
|
|
105
|
+
async importUsers(users) {
|
|
106
|
+
return this.request("POST", `${this.prefix}/users/import`, {
|
|
107
|
+
users: users.map((u) => ({
|
|
108
|
+
email: u.email,
|
|
109
|
+
email_verified: u.emailVerified ?? false,
|
|
110
|
+
password_hash: u.passwordHash,
|
|
111
|
+
name: u.name,
|
|
112
|
+
user_metadata: u.userMetadata,
|
|
113
|
+
app_metadata: u.appMetadata
|
|
114
|
+
}))
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Cles de signature du projet (active + previous), URL du JWKS et emetteur (`iss`). */
|
|
118
|
+
async listSigningKeys() {
|
|
119
|
+
return this.request("GET", `${this.prefix}/signing-keys`);
|
|
120
|
+
}
|
|
121
|
+
/** Rotation : une nouvelle cle signe les nouveaux jetons, l'ancienne reste publiee 24 h. */
|
|
122
|
+
async rotateSigningKey() {
|
|
123
|
+
return this.request("POST", `${this.prefix}/signing-keys/rotate`, {});
|
|
124
|
+
}
|
|
125
|
+
/** Revoque une ancienne cle compromise (retiree du JWKS, jetons refuses). */
|
|
126
|
+
async revokeSigningKey(kid) {
|
|
127
|
+
return this.request("DELETE", `${this.prefix}/signing-keys/${encodeURIComponent(kid)}`);
|
|
56
128
|
}
|
|
57
129
|
/**
|
|
58
130
|
* Statistiques des utilisateurs : total, vérifiés, bannis, avec MFA et nouveaux sur 7 jours.
|
|
@@ -351,6 +423,77 @@ async function codeChallengeS256(verifier) {
|
|
|
351
423
|
return b64url(new Uint8Array(digest));
|
|
352
424
|
}
|
|
353
425
|
|
|
426
|
+
// src/webauthn.ts
|
|
427
|
+
function base64urlToBytes(valeur) {
|
|
428
|
+
const b64 = valeur.replace(/-/g, "+").replace(/_/g, "/");
|
|
429
|
+
const bin = atob(b64 + "=".repeat((4 - b64.length % 4) % 4));
|
|
430
|
+
const octets = new Uint8Array(new ArrayBuffer(bin.length));
|
|
431
|
+
for (let i = 0; i < bin.length; i++) octets[i] = bin.charCodeAt(i);
|
|
432
|
+
return octets;
|
|
433
|
+
}
|
|
434
|
+
function bytesToBase64url(octets) {
|
|
435
|
+
const vue = octets instanceof ArrayBuffer ? new Uint8Array(octets) : new Uint8Array(octets.buffer, octets.byteOffset, octets.byteLength);
|
|
436
|
+
let bin = "";
|
|
437
|
+
for (const o of vue) bin += String.fromCharCode(o);
|
|
438
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
439
|
+
}
|
|
440
|
+
function creationOptionsFromJson(json) {
|
|
441
|
+
const pk = json.publicKey;
|
|
442
|
+
return {
|
|
443
|
+
publicKey: {
|
|
444
|
+
...pk,
|
|
445
|
+
challenge: base64urlToBytes(pk.challenge),
|
|
446
|
+
user: { ...pk.user, id: base64urlToBytes(pk.user.id) },
|
|
447
|
+
excludeCredentials: (pk.excludeCredentials ?? []).map((c) => ({
|
|
448
|
+
...c,
|
|
449
|
+
id: base64urlToBytes(c.id)
|
|
450
|
+
}))
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function requestOptionsFromJson(json) {
|
|
455
|
+
const pk = json.publicKey;
|
|
456
|
+
return {
|
|
457
|
+
publicKey: {
|
|
458
|
+
...pk,
|
|
459
|
+
challenge: base64urlToBytes(pk.challenge),
|
|
460
|
+
allowCredentials: (pk.allowCredentials ?? []).map((c) => ({
|
|
461
|
+
...c,
|
|
462
|
+
id: base64urlToBytes(c.id)
|
|
463
|
+
}))
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function attestationToJson(cred) {
|
|
468
|
+
const r = cred.response;
|
|
469
|
+
return {
|
|
470
|
+
id: cred.id,
|
|
471
|
+
rawId: bytesToBase64url(cred.rawId),
|
|
472
|
+
type: cred.type,
|
|
473
|
+
response: {
|
|
474
|
+
attestationObject: bytesToBase64url(r.attestationObject),
|
|
475
|
+
clientDataJSON: bytesToBase64url(r.clientDataJSON),
|
|
476
|
+
transports: typeof r.getTransports === "function" ? r.getTransports() : void 0
|
|
477
|
+
},
|
|
478
|
+
extensions: cred.getClientExtensionResults?.() ?? {}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function assertionToJson(cred) {
|
|
482
|
+
const r = cred.response;
|
|
483
|
+
return {
|
|
484
|
+
id: cred.id,
|
|
485
|
+
rawId: bytesToBase64url(cred.rawId),
|
|
486
|
+
type: cred.type,
|
|
487
|
+
response: {
|
|
488
|
+
authenticatorData: bytesToBase64url(r.authenticatorData),
|
|
489
|
+
clientDataJSON: bytesToBase64url(r.clientDataJSON),
|
|
490
|
+
signature: bytesToBase64url(r.signature),
|
|
491
|
+
userHandle: r.userHandle ? bytesToBase64url(r.userHandle) : null
|
|
492
|
+
},
|
|
493
|
+
extensions: cred.getClientExtensionResults?.() ?? {}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
354
497
|
// src/SessionManager.ts
|
|
355
498
|
var SessionManager = class {
|
|
356
499
|
opts;
|
|
@@ -423,6 +566,15 @@ var SessionManager = class {
|
|
|
423
566
|
};
|
|
424
567
|
|
|
425
568
|
// src/AuthService.ts
|
|
569
|
+
function extractSessionId(token) {
|
|
570
|
+
if (!token) return void 0;
|
|
571
|
+
try {
|
|
572
|
+
const payload = decodeJwt(token).payload;
|
|
573
|
+
return payload?.session_id ?? payload?.sid ?? void 0;
|
|
574
|
+
} catch {
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
426
578
|
function isMfaChallenge(v) {
|
|
427
579
|
return typeof v === "object" && v !== null && v.mfa_required === true;
|
|
428
580
|
}
|
|
@@ -452,10 +604,16 @@ var AuthService = class extends BaseService {
|
|
|
452
604
|
_stateListeners = /* @__PURE__ */ new Set();
|
|
453
605
|
_sessionManager;
|
|
454
606
|
_applyingRemote = false;
|
|
607
|
+
/** AUTH-011 — séquence des effacements de session et dernier effacement annoncé par SIGNED_OUT. */
|
|
608
|
+
_clearSeq = 0;
|
|
609
|
+
_signedOutSeq = 0;
|
|
455
610
|
_unsubStore = null;
|
|
456
611
|
_disposed = false;
|
|
612
|
+
/** AUTH-015 — flux OAuth par défaut du client. */
|
|
613
|
+
_defaultFlowType;
|
|
457
614
|
constructor(client, options) {
|
|
458
615
|
super(client);
|
|
616
|
+
this._defaultFlowType = options?.flowType;
|
|
459
617
|
const browser = typeof window !== "undefined";
|
|
460
618
|
const enabled = !!options?.forceEnabled || (options?.autoRefresh ?? true) && browser && (options?.persistSession ?? true);
|
|
461
619
|
this._sessionManager = new SessionManager({
|
|
@@ -471,8 +629,18 @@ var AuthService = class extends BaseService {
|
|
|
471
629
|
onRemoteMessage: (data) => this._applyRemoteState(data)
|
|
472
630
|
});
|
|
473
631
|
this._sessionManager.start();
|
|
632
|
+
let hadToken = !!this.client.authStore.token;
|
|
474
633
|
this._unsubStore = this.client.authStore.onChange((token) => {
|
|
475
634
|
this._sessionManager.setActive(!!token);
|
|
635
|
+
if (!token && hadToken && !this._applyingRemote) {
|
|
636
|
+
const seq = ++this._clearSeq;
|
|
637
|
+
queueMicrotask(() => {
|
|
638
|
+
if (this._signedOutSeq < seq && !this.client.authStore.token) {
|
|
639
|
+
this._emitEvent("SIGNED_OUT", null, null);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
hadToken = !!token;
|
|
476
644
|
if (!this._applyingRemote) {
|
|
477
645
|
const state = this._broadcastState();
|
|
478
646
|
if (!token || state !== null) {
|
|
@@ -685,10 +853,9 @@ var AuthService = class extends BaseService {
|
|
|
685
853
|
queueMicrotask(() => {
|
|
686
854
|
if (this._stateListeners.has(callback)) {
|
|
687
855
|
const session = store.token && store.user ? {
|
|
856
|
+
id: extractSessionId(store.token),
|
|
688
857
|
access_token: store.token,
|
|
689
858
|
refresh_token: store.refreshToken ?? "",
|
|
690
|
-
accessToken: store.token,
|
|
691
|
-
refreshToken: store.refreshToken ?? "",
|
|
692
859
|
token_type: "Bearer",
|
|
693
860
|
expires_in: 900,
|
|
694
861
|
user: store.user
|
|
@@ -699,11 +866,11 @@ var AuthService = class extends BaseService {
|
|
|
699
866
|
return result;
|
|
700
867
|
}
|
|
701
868
|
_emitEvent(event, user, accessToken, extra) {
|
|
869
|
+
if (event === "SIGNED_OUT") this._signedOutSeq = this._clearSeq;
|
|
702
870
|
const session = accessToken && user ? {
|
|
871
|
+
id: extractSessionId(accessToken),
|
|
703
872
|
access_token: accessToken,
|
|
704
873
|
refresh_token: this.client.authStore.refreshToken ?? "",
|
|
705
|
-
accessToken,
|
|
706
|
-
refreshToken: this.client.authStore.refreshToken ?? "",
|
|
707
874
|
token_type: "Bearer",
|
|
708
875
|
expires_in: 900,
|
|
709
876
|
user
|
|
@@ -733,16 +900,18 @@ var AuthService = class extends BaseService {
|
|
|
733
900
|
}
|
|
734
901
|
if (req.options?.data) payload.data = req.options.data;
|
|
735
902
|
if (req.options?.emailRedirectTo) payload.email_redirect_to = req.options.emailRedirectTo;
|
|
903
|
+
if (req.options?.preventEmailEnumeration) payload.prevent_email_enumeration = true;
|
|
736
904
|
const res = await this.request("POST", `${this.prefix}/register`, payload);
|
|
905
|
+
if (res.data && !isMfaChallenge(res.data) && !res.data.access_token) {
|
|
906
|
+
res.data.session = null;
|
|
907
|
+
return res;
|
|
908
|
+
}
|
|
737
909
|
if (res.data && !isMfaChallenge(res.data)) {
|
|
738
910
|
const tp = res.data;
|
|
739
|
-
tp.accessToken = tp.access_token;
|
|
740
|
-
tp.refreshToken = tp.refresh_token;
|
|
741
911
|
tp.session = {
|
|
912
|
+
id: extractSessionId(tp.access_token),
|
|
742
913
|
access_token: tp.access_token,
|
|
743
914
|
refresh_token: tp.refresh_token,
|
|
744
|
-
accessToken: tp.access_token,
|
|
745
|
-
refreshToken: tp.refresh_token,
|
|
746
915
|
token_type: tp.token_type,
|
|
747
916
|
expires_in: tp.expires_in,
|
|
748
917
|
user: tp.user,
|
|
@@ -754,11 +923,6 @@ var AuthService = class extends BaseService {
|
|
|
754
923
|
}
|
|
755
924
|
if (res.data && isMfaChallenge(res.data)) {
|
|
756
925
|
const mfa = res.data;
|
|
757
|
-
mfa.mfaChallenge = {
|
|
758
|
-
mfaRequired: mfa.mfa_required,
|
|
759
|
-
mfaToken: mfa.mfa_token,
|
|
760
|
-
mfaSetupRequired: mfa.mfa_setup_required
|
|
761
|
-
};
|
|
762
926
|
this._emitEvent("MFA_CHALLENGE", null, null, {
|
|
763
927
|
mfaToken: mfa.mfa_token,
|
|
764
928
|
mfaSetupRequired: mfa.mfa_setup_required
|
|
@@ -780,13 +944,10 @@ var AuthService = class extends BaseService {
|
|
|
780
944
|
const res = await this.request("POST", `${this.prefix}/login`, payload);
|
|
781
945
|
if (res.data && !isMfaChallenge(res.data)) {
|
|
782
946
|
const tp = res.data;
|
|
783
|
-
tp.accessToken = tp.access_token;
|
|
784
|
-
tp.refreshToken = tp.refresh_token;
|
|
785
947
|
tp.session = {
|
|
948
|
+
id: extractSessionId(tp.access_token),
|
|
786
949
|
access_token: tp.access_token,
|
|
787
950
|
refresh_token: tp.refresh_token,
|
|
788
|
-
accessToken: tp.access_token,
|
|
789
|
-
refreshToken: tp.refresh_token,
|
|
790
951
|
token_type: tp.token_type,
|
|
791
952
|
expires_in: tp.expires_in,
|
|
792
953
|
user: tp.user,
|
|
@@ -798,11 +959,6 @@ var AuthService = class extends BaseService {
|
|
|
798
959
|
}
|
|
799
960
|
if (res.data && isMfaChallenge(res.data)) {
|
|
800
961
|
const mfa = res.data;
|
|
801
|
-
mfa.mfaChallenge = {
|
|
802
|
-
mfaRequired: mfa.mfa_required,
|
|
803
|
-
mfaToken: mfa.mfa_token,
|
|
804
|
-
mfaSetupRequired: mfa.mfa_setup_required
|
|
805
|
-
};
|
|
806
962
|
this._emitEvent("MFA_CHALLENGE", null, null, {
|
|
807
963
|
mfaToken: mfa.mfa_token,
|
|
808
964
|
mfaSetupRequired: mfa.mfa_setup_required
|
|
@@ -831,7 +987,7 @@ var AuthService = class extends BaseService {
|
|
|
831
987
|
error: new AuraError(0, {
|
|
832
988
|
error: {
|
|
833
989
|
code: "invalid_provider",
|
|
834
|
-
message: "signInWithOAuth:
|
|
990
|
+
message: "signInWithOAuth: the provider must be a non-empty string, e.g. signInWithOAuth('google', { redirectTo }) or signInWithOAuth({ provider: 'google', redirectTo })"
|
|
835
991
|
}
|
|
836
992
|
})
|
|
837
993
|
};
|
|
@@ -845,22 +1001,22 @@ var AuthService = class extends BaseService {
|
|
|
845
1001
|
params.set(k, v);
|
|
846
1002
|
continue;
|
|
847
1003
|
}
|
|
848
|
-
const raison = OAUTH_SDK_OWNED_QUERY_PARAMS.has(k) ? `
|
|
1004
|
+
const raison = OAUTH_SDK_OWNED_QUERY_PARAMS.has(k) ? `it is built by signInWithOAuth (use the dedicated option: ${k === "redirect_uri" || k === "redirect_to" ? "redirectTo" : k === "scopes" ? "scopes" : k === "project_id" ? "the project URL passed to createClient()" : "flowType:'pkce'"})` : `it is not forwarded to the provider. Only these sign-in experience parameters are: ${[...OAUTH_FORWARDABLE_QUERY_PARAMS].join(", ")}`;
|
|
849
1005
|
return {
|
|
850
1006
|
data: null,
|
|
851
1007
|
error: new AuraError(400, {
|
|
852
1008
|
error: {
|
|
853
1009
|
code: "invalid_oauth_query_param",
|
|
854
|
-
message: `signInWithOAuth: queryParams.${k}
|
|
1010
|
+
message: `signInWithOAuth: queryParams.${k} rejected \u2014 ${raison}.`
|
|
855
1011
|
}
|
|
856
1012
|
})
|
|
857
1013
|
};
|
|
858
1014
|
}
|
|
859
1015
|
}
|
|
860
|
-
if (options?.flowType === "pkce") {
|
|
1016
|
+
if ((options?.flowType ?? this._defaultFlowType) === "pkce") {
|
|
861
1017
|
if (typeof window !== "undefined" && !this.client.authStore.persistent) {
|
|
862
1018
|
console.warn(
|
|
863
|
-
|
|
1019
|
+
'[aurabase] PKCE (flowType:"pkce") requires a persistent store (LocalAuthStore) so that the code_verifier survives the OAuth redirect; the current store is in-memory, so the exchange will fail on return.'
|
|
864
1020
|
);
|
|
865
1021
|
}
|
|
866
1022
|
try {
|
|
@@ -875,7 +1031,7 @@ var AuthService = class extends BaseService {
|
|
|
875
1031
|
error: new AuraError(0, {
|
|
876
1032
|
error: {
|
|
877
1033
|
code: "pkce_generation_failed",
|
|
878
|
-
message: "PKCE:
|
|
1034
|
+
message: "PKCE: cannot generate the code_verifier/challenge (WebCrypto unavailable)"
|
|
879
1035
|
}
|
|
880
1036
|
})
|
|
881
1037
|
};
|
|
@@ -883,7 +1039,7 @@ var AuthService = class extends BaseService {
|
|
|
883
1039
|
} else {
|
|
884
1040
|
if (typeof window !== "undefined" && !this.client.authStore.persistent) {
|
|
885
1041
|
console.warn(
|
|
886
|
-
'[aurabase]
|
|
1042
|
+
'[aurabase] SECURITY: signInWithOAuth in implicit flow with a NON-persistent store (in-memory): the anti session-fixation marker will not survive the OAuth redirect and the session will not be adopted on return. Use a LocalAuthStore, and prefer flowType:"pkce" (recommended, immune to session fixation).'
|
|
887
1043
|
);
|
|
888
1044
|
}
|
|
889
1045
|
try {
|
|
@@ -892,7 +1048,7 @@ var AuthService = class extends BaseService {
|
|
|
892
1048
|
}
|
|
893
1049
|
}
|
|
894
1050
|
const url = `${this.client.baseUrl}/v1/auth/oauth/${encodeURIComponent(provider)}/start?${params.toString()}`;
|
|
895
|
-
if (typeof window !== "undefined") {
|
|
1051
|
+
if (typeof window !== "undefined" && window.location) {
|
|
896
1052
|
window.location.href = url;
|
|
897
1053
|
}
|
|
898
1054
|
return { data: { url }, error: null };
|
|
@@ -949,31 +1105,22 @@ var AuthService = class extends BaseService {
|
|
|
949
1105
|
return res;
|
|
950
1106
|
}
|
|
951
1107
|
/**
|
|
952
|
-
* Session courante lue depuis le store (
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
* - Permet `const { data: { session }, error } = await aura.auth.getSession()` (forme canonique)
|
|
956
|
-
* - Permet `const { accessToken, user } = await aura.auth.getSession()` (forme plate historique)
|
|
1108
|
+
* Session courante lue depuis le store (pas d'appel réseau). Forme unique (AUTH-019) :
|
|
1109
|
+
* `const { data: { session } } = await aura.auth.getSession()`.
|
|
1110
|
+
* Côté serveur, cette session n'est PAS vérifiée : autorisez avec `getUser()` (AUTH-004).
|
|
957
1111
|
*/
|
|
958
1112
|
getSession() {
|
|
959
1113
|
const token = this.client.authStore.token;
|
|
960
1114
|
const user = this.client.authStore.user;
|
|
961
1115
|
const session = token && user ? {
|
|
1116
|
+
id: extractSessionId(token),
|
|
962
1117
|
access_token: token,
|
|
963
1118
|
refresh_token: this.client.authStore.refreshToken ?? "",
|
|
964
|
-
accessToken: token,
|
|
965
|
-
refreshToken: this.client.authStore.refreshToken ?? "",
|
|
966
1119
|
token_type: "Bearer",
|
|
967
1120
|
expires_in: 900,
|
|
968
1121
|
user
|
|
969
1122
|
} : null;
|
|
970
|
-
return Promise.resolve({
|
|
971
|
-
data: { session },
|
|
972
|
-
error: null,
|
|
973
|
-
accessToken: token,
|
|
974
|
-
access_token: token,
|
|
975
|
-
user
|
|
976
|
-
});
|
|
1123
|
+
return Promise.resolve({ data: { session }, error: null });
|
|
977
1124
|
}
|
|
978
1125
|
/**
|
|
979
1126
|
* Installe une session à partir d'un jeton d'accès et d'un jeton de rafraîchissement existants (par exemple reçus d'un serveur).
|
|
@@ -1241,7 +1388,7 @@ var AuthService = class extends BaseService {
|
|
|
1241
1388
|
error: new AuraError(0, {
|
|
1242
1389
|
error: {
|
|
1243
1390
|
code: "allow_expired_unsupported_on_fallback",
|
|
1244
|
-
message: "getClaims: allowExpired
|
|
1391
|
+
message: "getClaims: allowExpired cannot be honored on the server validation path (HS256/JWKS fallback)"
|
|
1245
1392
|
}
|
|
1246
1393
|
})
|
|
1247
1394
|
};
|
|
@@ -1269,7 +1416,7 @@ var AuthService = class extends BaseService {
|
|
|
1269
1416
|
error: new AuraError(0, {
|
|
1270
1417
|
error: {
|
|
1271
1418
|
code: "invalid_response",
|
|
1272
|
-
message: "getClaims:
|
|
1419
|
+
message: "getClaims: the server token validation (getUser) answered with no content, so the claims cannot be determined"
|
|
1273
1420
|
}
|
|
1274
1421
|
})
|
|
1275
1422
|
};
|
|
@@ -1285,6 +1432,17 @@ var AuthService = class extends BaseService {
|
|
|
1285
1432
|
* Supprime le compte de l'utilisateur connecté après confirmation par son mot de passe, puis vide la session locale.
|
|
1286
1433
|
*/
|
|
1287
1434
|
async deleteUser(password) {
|
|
1435
|
+
if (!password || typeof password !== "string" || password.trim() === "") {
|
|
1436
|
+
return {
|
|
1437
|
+
data: null,
|
|
1438
|
+
error: new AuraError(400, {
|
|
1439
|
+
error: {
|
|
1440
|
+
code: "PASSWORD_REQUIRED",
|
|
1441
|
+
message: "Password is required to delete account"
|
|
1442
|
+
}
|
|
1443
|
+
})
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1288
1446
|
const res = await this.request("DELETE", `${this.prefix}/user`, { password });
|
|
1289
1447
|
if (!res.error) {
|
|
1290
1448
|
this.client.authStore.clear();
|
|
@@ -1346,9 +1504,107 @@ var AuthService = class extends BaseService {
|
|
|
1346
1504
|
regenerateRecoveryCodes: (codeOrParams) => {
|
|
1347
1505
|
const code = typeof codeOrParams === "string" ? codeOrParams : codeOrParams.code;
|
|
1348
1506
|
return this.regenerateRecoveryCodes(code);
|
|
1507
|
+
},
|
|
1508
|
+
factors: (mfaToken) => this.request("POST", `${this.prefix}/mfa/factors`, { mfa_token: mfaToken }),
|
|
1509
|
+
sms: {
|
|
1510
|
+
setup: (phone) => this.request("POST", `${this.prefix}/mfa/sms/setup`, { phone }),
|
|
1511
|
+
verifySetup: (code) => this.request("POST", `${this.prefix}/mfa/sms/setup/verify`, { code }),
|
|
1512
|
+
send: (mfaToken) => this.request("POST", `${this.prefix}/mfa/sms/send`, {
|
|
1513
|
+
mfa_token: mfaToken
|
|
1514
|
+
}),
|
|
1515
|
+
remove: (password) => this.request("DELETE", `${this.prefix}/mfa/sms`, { password })
|
|
1516
|
+
},
|
|
1517
|
+
webauthn: {
|
|
1518
|
+
register: (params) => this.webauthnRegister(params),
|
|
1519
|
+
verify: (params) => this.webauthnVerify(params),
|
|
1520
|
+
list: () => this.request("GET", `${this.prefix}/mfa/webauthn`),
|
|
1521
|
+
remove: (id, password) => this.request(
|
|
1522
|
+
"DELETE",
|
|
1523
|
+
`${this.prefix}/mfa/webauthn/${encodeURIComponent(id)}`,
|
|
1524
|
+
{ password }
|
|
1525
|
+
)
|
|
1349
1526
|
}
|
|
1350
1527
|
};
|
|
1351
1528
|
}
|
|
1529
|
+
webauthnIndisponible() {
|
|
1530
|
+
return {
|
|
1531
|
+
data: null,
|
|
1532
|
+
error: new AuraError(0, {
|
|
1533
|
+
error: {
|
|
1534
|
+
code: "webauthn_unavailable",
|
|
1535
|
+
message: "WebAuthn is not available here: navigator.credentials is missing. Use a browser in a secure context (https or localhost), or pass `credentials` explicitly."
|
|
1536
|
+
}
|
|
1537
|
+
})
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Enrôle une passkey WebAuthn pour l'utilisateur connecté (cérémonie complète : options du serveur,
|
|
1542
|
+
* `navigator.credentials.create`, confirmation). Le premier facteur du compte active le MFA et
|
|
1543
|
+
* renvoie les codes de récupération (`recovery_codes`) — à afficher une seule fois.
|
|
1544
|
+
*/
|
|
1545
|
+
async webauthnRegister(params) {
|
|
1546
|
+
const conteneur = params?.credentials ?? (typeof navigator !== "undefined" ? navigator.credentials : void 0);
|
|
1547
|
+
if (!conteneur) return this.webauthnIndisponible();
|
|
1548
|
+
const debut = await this.request(
|
|
1549
|
+
"POST",
|
|
1550
|
+
`${this.prefix}/mfa/webauthn/register/start`,
|
|
1551
|
+
{ friendly_name: params?.friendlyName }
|
|
1552
|
+
);
|
|
1553
|
+
if (!debut.data) return debut;
|
|
1554
|
+
let cred;
|
|
1555
|
+
try {
|
|
1556
|
+
cred = await conteneur.create(creationOptionsFromJson(debut.data.options));
|
|
1557
|
+
} catch (e) {
|
|
1558
|
+
return this.webauthnAnnule(e);
|
|
1559
|
+
}
|
|
1560
|
+
if (!cred) return this.webauthnAnnule(new Error("no credential returned"));
|
|
1561
|
+
return this.request("POST", `${this.prefix}/mfa/webauthn/register/finish`, {
|
|
1562
|
+
credential: attestationToJson(cred),
|
|
1563
|
+
friendly_name: params?.friendlyName
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Relève un défi MFA (`mfaToken`, reçu d'un `signIn`) avec une passkey : options du serveur,
|
|
1568
|
+
* `navigator.credentials.get`, vérification. Enregistre la session et émet `SIGNED_IN`.
|
|
1569
|
+
*/
|
|
1570
|
+
async webauthnVerify(params) {
|
|
1571
|
+
const conteneur = params.credentials ?? (typeof navigator !== "undefined" ? navigator.credentials : void 0);
|
|
1572
|
+
if (!conteneur) return this.webauthnIndisponible();
|
|
1573
|
+
const debut = await this.request(
|
|
1574
|
+
"POST",
|
|
1575
|
+
`${this.prefix}/mfa/webauthn/verify/start`,
|
|
1576
|
+
{ mfa_token: params.mfaToken }
|
|
1577
|
+
);
|
|
1578
|
+
if (!debut.data) return debut;
|
|
1579
|
+
let cred;
|
|
1580
|
+
try {
|
|
1581
|
+
cred = await conteneur.get(requestOptionsFromJson(debut.data.options));
|
|
1582
|
+
} catch (e) {
|
|
1583
|
+
return this.webauthnAnnule(e);
|
|
1584
|
+
}
|
|
1585
|
+
if (!cred) return this.webauthnAnnule(new Error("no credential returned"));
|
|
1586
|
+
const res = await this.request("POST", `${this.prefix}/mfa/webauthn/verify/finish`, {
|
|
1587
|
+
mfa_token: params.mfaToken,
|
|
1588
|
+
credential: assertionToJson(cred)
|
|
1589
|
+
});
|
|
1590
|
+
if (res.data) {
|
|
1591
|
+
this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
|
|
1592
|
+
this._emitEvent("SIGNED_IN", res.data.user, res.data.access_token);
|
|
1593
|
+
}
|
|
1594
|
+
return res;
|
|
1595
|
+
}
|
|
1596
|
+
webauthnAnnule(cause) {
|
|
1597
|
+
const nom = cause instanceof Error ? cause.name : "Error";
|
|
1598
|
+
return {
|
|
1599
|
+
data: null,
|
|
1600
|
+
error: new AuraError(0, {
|
|
1601
|
+
error: {
|
|
1602
|
+
code: nom === "NotAllowedError" ? "webauthn_cancelled" : "webauthn_failed",
|
|
1603
|
+
message: nom === "NotAllowedError" ? "The passkey prompt was cancelled or timed out." : `The passkey ceremony failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1604
|
+
}
|
|
1605
|
+
})
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1352
1608
|
/** MFA : démarre l'enrôlement TOTP et renvoie le `secret` et l'URL `otpauth_url` à afficher en QR code. */
|
|
1353
1609
|
async mfaEnroll() {
|
|
1354
1610
|
return this.request("POST", `${this.prefix}/mfa/setup`);
|
|
@@ -1361,10 +1617,31 @@ var AuthService = class extends BaseService {
|
|
|
1361
1617
|
* MFA : termine une connexion qui a renvoyé un défi (`mfa_token` et code TOTP). Enregistre la session et émet `SIGNED_IN`.
|
|
1362
1618
|
*/
|
|
1363
1619
|
async mfaVerify(req) {
|
|
1364
|
-
const
|
|
1620
|
+
const raw = req;
|
|
1621
|
+
const mfa_token = raw.mfa_token ?? raw.mfaToken ?? raw.challenge_id ?? raw.challengeId;
|
|
1622
|
+
const code = raw.code;
|
|
1623
|
+
const recovery_code = raw.recovery_code ?? raw.recoveryCode;
|
|
1624
|
+
const sms_code = raw.sms_code ?? raw.smsCode;
|
|
1625
|
+
const body = {};
|
|
1626
|
+
if (mfa_token) body.mfa_token = mfa_token;
|
|
1627
|
+
if (code !== void 0) body.code = code;
|
|
1628
|
+
if (recovery_code !== void 0) body.recovery_code = recovery_code;
|
|
1629
|
+
if (sms_code !== void 0) body.sms_code = sms_code;
|
|
1630
|
+
const res = await this.request("POST", `${this.prefix}/mfa/verify-login`, body);
|
|
1365
1631
|
if (res.data) {
|
|
1366
|
-
|
|
1367
|
-
|
|
1632
|
+
const tp = res.data;
|
|
1633
|
+
tp.session = {
|
|
1634
|
+
id: extractSessionId(tp.access_token),
|
|
1635
|
+
access_token: tp.access_token,
|
|
1636
|
+
refresh_token: tp.refresh_token,
|
|
1637
|
+
token_type: tp.token_type,
|
|
1638
|
+
expires_in: tp.expires_in,
|
|
1639
|
+
user: tp.user,
|
|
1640
|
+
provider_token: tp.provider_token,
|
|
1641
|
+
provider_refresh_token: tp.provider_refresh_token
|
|
1642
|
+
};
|
|
1643
|
+
this.client.authStore.save(tp.access_token, tp.refresh_token, tp.user);
|
|
1644
|
+
this._emitEvent("SIGNED_IN", tp.user, tp.access_token);
|
|
1368
1645
|
}
|
|
1369
1646
|
return res;
|
|
1370
1647
|
}
|
|
@@ -1518,10 +1795,10 @@ var AuthService = class extends BaseService {
|
|
|
1518
1795
|
async getSettings() {
|
|
1519
1796
|
return this.request("GET", `${this.prefix}/settings`);
|
|
1520
1797
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1798
|
+
// AUTH-023 — `getAnonKey()` retirée : la route `/anon-key` n'est servie qu'aux projets qui
|
|
1799
|
+
// activent l'auth hébergée (garde C1, aucun BaaS n'expose la clé d'un projet à un anonyme), et
|
|
1800
|
+
// un client qui l'appelle possède déjà la clé anon (passée à createClient). La méthode
|
|
1801
|
+
// répondait donc 404 pour presque tous les projets.
|
|
1525
1802
|
// -- Password recovery (token consumption) ----------------------------------
|
|
1526
1803
|
/**
|
|
1527
1804
|
* Consomme le token de récupération de mot de passe et applique le nouveau mot de passe.
|
|
@@ -1558,4 +1835,4 @@ var AuthService = class extends BaseService {
|
|
|
1558
1835
|
}
|
|
1559
1836
|
};
|
|
1560
1837
|
|
|
1561
|
-
export { AuthAdminService, AuthService, SessionManager, isMfaChallenge };
|
|
1838
|
+
export { AuthAdminService, AuthService, SessionManager, assertionToJson, attestationToJson, base64urlToBytes, bytesToBase64url, creationOptionsFromJson, isMfaChallenge, requestOptionsFromJson };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurabase/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Service d'authentification du SDK Aurabase (sessions, OAuth/PKCE, MFA, magic link, OTP, admin)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"authentication"
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@aurabase/core": "0.
|
|
49
|
+
"@aurabase/core": "0.7.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "20.14.15",
|