@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/README.md
CHANGED
|
@@ -39,8 +39,13 @@ await aura.auth.signOut()
|
|
|
39
39
|
### Session
|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
|
-
// getSession()
|
|
43
|
-
const {
|
|
42
|
+
// getSession() lit le store local (synchrone en pratique) :
|
|
43
|
+
const { data: { session } } = await aura.auth.getSession()
|
|
44
|
+
if (session) {
|
|
45
|
+
console.log('Session ID:', session.id)
|
|
46
|
+
console.log('Access token:', session.access_token)
|
|
47
|
+
console.log('User ID:', session.user.id)
|
|
48
|
+
}
|
|
44
49
|
|
|
45
50
|
const refreshed = await aura.auth.refreshSession()
|
|
46
51
|
|
|
@@ -55,6 +60,32 @@ if (first) await aura.auth.revokeSession(first.id)
|
|
|
55
60
|
await aura.auth.revokeAllSessions()
|
|
56
61
|
```
|
|
57
62
|
|
|
63
|
+
### Configuration du stockage (SSR & Node.js)
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { createClient, LocalAuthStore, BaseAuthStore } from '@aurabase/aurabase-js'
|
|
67
|
+
|
|
68
|
+
// Par défaut, le client choisit automatiquement :
|
|
69
|
+
// - Navigateur : `LocalAuthStore` avec clé scopée par projet (`aura-<slug>-auth`)
|
|
70
|
+
// - Node.js / SSR : `BaseAuthStore` en mémoire avec exportToCookie() / loadFromCookie()
|
|
71
|
+
|
|
72
|
+
// Pour personnaliser le stockage :
|
|
73
|
+
const aura = createClient('https://mon-projet.aurabase.cloud', 'aura_anon_xxx', {
|
|
74
|
+
auth: {
|
|
75
|
+
storage: new LocalAuthStore('custom-auth-key'),
|
|
76
|
+
autoRefreshToken: true,
|
|
77
|
+
persistSession: true,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Sérialisation cookie (SSR) :
|
|
82
|
+
const setCookieHeader = aura.authStore.exportToCookie({
|
|
83
|
+
httpOnly: true,
|
|
84
|
+
secure: true,
|
|
85
|
+
sameSite: 'lax',
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
58
89
|
### OAuth (implicite ou PKCE)
|
|
59
90
|
|
|
60
91
|
`signInWithOAuth` ne redirige plus « en dur » : il renvoie toujours `{ url }` (et redirige en plus via `window.location.href` si un `window` existe).
|
|
@@ -288,14 +319,19 @@ await admin.authAdmin.listUsers({ limit: 50 })
|
|
|
288
319
|
import { AuthAdminService } from '@aurabase/auth'
|
|
289
320
|
```
|
|
290
321
|
|
|
291
|
-
Méthodes clés : `listUsers`, `getUser`, `createUser`, `updateUser`, `deleteUser`, `banUser`, `unbanUser`, `generateLink`, `getUserStats`, `listUserIdentities`, `listSessions`, `revokeSession`, `getAuditLog`, `getSettings`, `updateSettings`, `listEmailTemplates`/`getEmailTemplate`/`updateEmailTemplate`/`deleteEmailTemplate`, `testEmailTemplate`.
|
|
322
|
+
Méthodes clés : `listUsers`, `getUser`, `createUser`, `updateUser`, `deleteUser`, `banUser`, `unbanUser`, `generateLink`, `getUserStats`, `listUserIdentities`, `listSessions`, `revokeSession`, `impersonate` (RFC 8693 avec claim `act`), `importUsers` (import en masse avec conversion automatique bcrypt vers Argon2id), `listSigningKeys` (clés ES256 du projet), `getAuditLog`, `getSettings`, `updateSettings`, `listEmailTemplates`/`getEmailTemplate`/`updateEmailTemplate`/`deleteEmailTemplate`, `testEmailTemplate`.
|
|
323
|
+
|
|
324
|
+
### Authentification Multi-Facteurs (MFA v0.7.0)
|
|
325
|
+
|
|
326
|
+
Aurabase supporte l'authentification multifacteur complète avec niveaux d'assurance (AAL1, AAL2) et claim standard `amr` :
|
|
327
|
+
- **TOTP** : `c.auth.mfa.enroll()`, `c.auth.mfa.verifySetup(token)`, `c.auth.mfa.verify({ mfa_token, code })`
|
|
328
|
+
- **Facteurs disponibles** : `c.auth.mfa.factors(mfa_token)` liste les méthodes éligibles (`totp`, `webauthn`, `sms`)
|
|
329
|
+
- **WebAuthn / Passkeys** : `c.auth.mfa.webauthn`
|
|
330
|
+
- **SMS OTP** : `c.auth.mfa.sms`
|
|
292
331
|
|
|
293
|
-
|
|
332
|
+
### Vérification locale de jeton (JWKS / ES256)
|
|
294
333
|
|
|
295
|
-
|
|
296
|
-
- Pas de SSO/SAML.
|
|
297
|
-
- MFA limité au TOTP : pas de `listFactors()`, pas de niveau d'authentification (AAL), pas de MFA par téléphone.
|
|
298
|
-
- `getClaims()` en fallback (HS256 / JWKS vide) ne renvoie pas `exp`/`iat` — ne pas s'y fier après un fallback.
|
|
334
|
+
Chaque projet émet des jetons signés avec sa propre clé asymétrique **ES256**. Vous pouvez vérifier les jetons hors-ligne via n'importe quelle bibliothèque standard (`jose`, Go, Python) en récupérant la clé publique sur `https://<projet>.aurabase.cloud/v1/auth/.well-known/jwks.json`.
|
|
299
335
|
|
|
300
336
|
## Documentation
|
|
301
337
|
|
package/dist/index.cjs
CHANGED
|
@@ -52,9 +52,81 @@ var AuthAdminService = class extends core.BaseService {
|
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Génère un lien à usage unique pour un utilisateur (`'magic'`, `'recovery'` ou `'invite'`) et renvoie `{ link, token }`.
|
|
55
|
+
* Accepte soit `generateLink(userId, type)` soit `generateLink({ userId, type })`.
|
|
55
56
|
*/
|
|
56
|
-
async generateLink(
|
|
57
|
-
|
|
57
|
+
async generateLink(userIdOrParams, type) {
|
|
58
|
+
let resolvedUserId;
|
|
59
|
+
let resolvedType;
|
|
60
|
+
if (typeof userIdOrParams === "object" && userIdOrParams !== null) {
|
|
61
|
+
resolvedUserId = userIdOrParams.userId ?? userIdOrParams.user_id;
|
|
62
|
+
resolvedType = userIdOrParams.type ?? type;
|
|
63
|
+
} else if (typeof userIdOrParams === "string") {
|
|
64
|
+
resolvedUserId = userIdOrParams;
|
|
65
|
+
resolvedType = type;
|
|
66
|
+
}
|
|
67
|
+
if (!resolvedUserId || typeof resolvedUserId !== "string" || resolvedUserId.trim() === "" || resolvedUserId.includes("[object")) {
|
|
68
|
+
return {
|
|
69
|
+
data: null,
|
|
70
|
+
error: new core.AuraError(400, {
|
|
71
|
+
error: {
|
|
72
|
+
code: "INVALID_ARGUMENT",
|
|
73
|
+
message: "userId must be a non-empty string (UUID). Usage: generateLink(userId, type) or generateLink({ userId, type })."
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (!resolvedType) {
|
|
79
|
+
return {
|
|
80
|
+
data: null,
|
|
81
|
+
error: new core.AuraError(400, {
|
|
82
|
+
error: {
|
|
83
|
+
code: "INVALID_ARGUMENT",
|
|
84
|
+
message: 'type is required for generateLink ("magic" | "recovery" | "invite").'
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return this.request("POST", `${this.prefix}/users/${encodeURIComponent(resolvedUserId)}/generate-link`, { type: resolvedType });
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Impersonation (AUTH-037) : jeton COURT (10 minutes au plus) pour agir en tant que l'utilisateur,
|
|
93
|
+
* avec le claim `act: { sub }`. Pas de refresh token, journalisé (`admin_impersonation`).
|
|
94
|
+
* Un compte `owner`/`admin` exige `allowPrivileged: true` ET une `reason`.
|
|
95
|
+
*/
|
|
96
|
+
async impersonate(userId, options) {
|
|
97
|
+
return this.request("POST", `${this.prefix}/users/${userId}/impersonate`, {
|
|
98
|
+
reason: options?.reason,
|
|
99
|
+
ttl_seconds: options?.ttlSeconds,
|
|
100
|
+
allow_privileged: options?.allowPrivileged ?? false
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Import en masse (AUTH-037) : jusqu'a 1000 utilisateurs par appel, avec hash bcrypt ou argon2id.
|
|
105
|
+
* Rapport ligne par ligne ; une ligne en erreur n'arrete pas les autres.
|
|
106
|
+
*/
|
|
107
|
+
async importUsers(users) {
|
|
108
|
+
return this.request("POST", `${this.prefix}/users/import`, {
|
|
109
|
+
users: users.map((u) => ({
|
|
110
|
+
email: u.email,
|
|
111
|
+
email_verified: u.emailVerified ?? false,
|
|
112
|
+
password_hash: u.passwordHash,
|
|
113
|
+
name: u.name,
|
|
114
|
+
user_metadata: u.userMetadata,
|
|
115
|
+
app_metadata: u.appMetadata
|
|
116
|
+
}))
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Cles de signature du projet (active + previous), URL du JWKS et emetteur (`iss`). */
|
|
120
|
+
async listSigningKeys() {
|
|
121
|
+
return this.request("GET", `${this.prefix}/signing-keys`);
|
|
122
|
+
}
|
|
123
|
+
/** Rotation : une nouvelle cle signe les nouveaux jetons, l'ancienne reste publiee 24 h. */
|
|
124
|
+
async rotateSigningKey() {
|
|
125
|
+
return this.request("POST", `${this.prefix}/signing-keys/rotate`, {});
|
|
126
|
+
}
|
|
127
|
+
/** Revoque une ancienne cle compromise (retiree du JWKS, jetons refuses). */
|
|
128
|
+
async revokeSigningKey(kid) {
|
|
129
|
+
return this.request("DELETE", `${this.prefix}/signing-keys/${encodeURIComponent(kid)}`);
|
|
58
130
|
}
|
|
59
131
|
/**
|
|
60
132
|
* Statistiques des utilisateurs : total, vérifiés, bannis, avec MFA et nouveaux sur 7 jours.
|
|
@@ -353,6 +425,77 @@ async function codeChallengeS256(verifier) {
|
|
|
353
425
|
return b64url(new Uint8Array(digest));
|
|
354
426
|
}
|
|
355
427
|
|
|
428
|
+
// src/webauthn.ts
|
|
429
|
+
function base64urlToBytes(valeur) {
|
|
430
|
+
const b64 = valeur.replace(/-/g, "+").replace(/_/g, "/");
|
|
431
|
+
const bin = atob(b64 + "=".repeat((4 - b64.length % 4) % 4));
|
|
432
|
+
const octets = new Uint8Array(new ArrayBuffer(bin.length));
|
|
433
|
+
for (let i = 0; i < bin.length; i++) octets[i] = bin.charCodeAt(i);
|
|
434
|
+
return octets;
|
|
435
|
+
}
|
|
436
|
+
function bytesToBase64url(octets) {
|
|
437
|
+
const vue = octets instanceof ArrayBuffer ? new Uint8Array(octets) : new Uint8Array(octets.buffer, octets.byteOffset, octets.byteLength);
|
|
438
|
+
let bin = "";
|
|
439
|
+
for (const o of vue) bin += String.fromCharCode(o);
|
|
440
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
441
|
+
}
|
|
442
|
+
function creationOptionsFromJson(json) {
|
|
443
|
+
const pk = json.publicKey;
|
|
444
|
+
return {
|
|
445
|
+
publicKey: {
|
|
446
|
+
...pk,
|
|
447
|
+
challenge: base64urlToBytes(pk.challenge),
|
|
448
|
+
user: { ...pk.user, id: base64urlToBytes(pk.user.id) },
|
|
449
|
+
excludeCredentials: (pk.excludeCredentials ?? []).map((c) => ({
|
|
450
|
+
...c,
|
|
451
|
+
id: base64urlToBytes(c.id)
|
|
452
|
+
}))
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function requestOptionsFromJson(json) {
|
|
457
|
+
const pk = json.publicKey;
|
|
458
|
+
return {
|
|
459
|
+
publicKey: {
|
|
460
|
+
...pk,
|
|
461
|
+
challenge: base64urlToBytes(pk.challenge),
|
|
462
|
+
allowCredentials: (pk.allowCredentials ?? []).map((c) => ({
|
|
463
|
+
...c,
|
|
464
|
+
id: base64urlToBytes(c.id)
|
|
465
|
+
}))
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function attestationToJson(cred) {
|
|
470
|
+
const r = cred.response;
|
|
471
|
+
return {
|
|
472
|
+
id: cred.id,
|
|
473
|
+
rawId: bytesToBase64url(cred.rawId),
|
|
474
|
+
type: cred.type,
|
|
475
|
+
response: {
|
|
476
|
+
attestationObject: bytesToBase64url(r.attestationObject),
|
|
477
|
+
clientDataJSON: bytesToBase64url(r.clientDataJSON),
|
|
478
|
+
transports: typeof r.getTransports === "function" ? r.getTransports() : void 0
|
|
479
|
+
},
|
|
480
|
+
extensions: cred.getClientExtensionResults?.() ?? {}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function assertionToJson(cred) {
|
|
484
|
+
const r = cred.response;
|
|
485
|
+
return {
|
|
486
|
+
id: cred.id,
|
|
487
|
+
rawId: bytesToBase64url(cred.rawId),
|
|
488
|
+
type: cred.type,
|
|
489
|
+
response: {
|
|
490
|
+
authenticatorData: bytesToBase64url(r.authenticatorData),
|
|
491
|
+
clientDataJSON: bytesToBase64url(r.clientDataJSON),
|
|
492
|
+
signature: bytesToBase64url(r.signature),
|
|
493
|
+
userHandle: r.userHandle ? bytesToBase64url(r.userHandle) : null
|
|
494
|
+
},
|
|
495
|
+
extensions: cred.getClientExtensionResults?.() ?? {}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
356
499
|
// src/SessionManager.ts
|
|
357
500
|
var SessionManager = class {
|
|
358
501
|
opts;
|
|
@@ -425,6 +568,15 @@ var SessionManager = class {
|
|
|
425
568
|
};
|
|
426
569
|
|
|
427
570
|
// src/AuthService.ts
|
|
571
|
+
function extractSessionId(token) {
|
|
572
|
+
if (!token) return void 0;
|
|
573
|
+
try {
|
|
574
|
+
const payload = decodeJwt(token).payload;
|
|
575
|
+
return payload?.session_id ?? payload?.sid ?? void 0;
|
|
576
|
+
} catch {
|
|
577
|
+
return void 0;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
428
580
|
function isMfaChallenge(v) {
|
|
429
581
|
return typeof v === "object" && v !== null && v.mfa_required === true;
|
|
430
582
|
}
|
|
@@ -454,10 +606,16 @@ var AuthService = class extends core.BaseService {
|
|
|
454
606
|
_stateListeners = /* @__PURE__ */ new Set();
|
|
455
607
|
_sessionManager;
|
|
456
608
|
_applyingRemote = false;
|
|
609
|
+
/** AUTH-011 — séquence des effacements de session et dernier effacement annoncé par SIGNED_OUT. */
|
|
610
|
+
_clearSeq = 0;
|
|
611
|
+
_signedOutSeq = 0;
|
|
457
612
|
_unsubStore = null;
|
|
458
613
|
_disposed = false;
|
|
614
|
+
/** AUTH-015 — flux OAuth par défaut du client. */
|
|
615
|
+
_defaultFlowType;
|
|
459
616
|
constructor(client, options) {
|
|
460
617
|
super(client);
|
|
618
|
+
this._defaultFlowType = options?.flowType;
|
|
461
619
|
const browser = typeof window !== "undefined";
|
|
462
620
|
const enabled = !!options?.forceEnabled || (options?.autoRefresh ?? true) && browser && (options?.persistSession ?? true);
|
|
463
621
|
this._sessionManager = new SessionManager({
|
|
@@ -473,8 +631,18 @@ var AuthService = class extends core.BaseService {
|
|
|
473
631
|
onRemoteMessage: (data) => this._applyRemoteState(data)
|
|
474
632
|
});
|
|
475
633
|
this._sessionManager.start();
|
|
634
|
+
let hadToken = !!this.client.authStore.token;
|
|
476
635
|
this._unsubStore = this.client.authStore.onChange((token) => {
|
|
477
636
|
this._sessionManager.setActive(!!token);
|
|
637
|
+
if (!token && hadToken && !this._applyingRemote) {
|
|
638
|
+
const seq = ++this._clearSeq;
|
|
639
|
+
queueMicrotask(() => {
|
|
640
|
+
if (this._signedOutSeq < seq && !this.client.authStore.token) {
|
|
641
|
+
this._emitEvent("SIGNED_OUT", null, null);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
hadToken = !!token;
|
|
478
646
|
if (!this._applyingRemote) {
|
|
479
647
|
const state = this._broadcastState();
|
|
480
648
|
if (!token || state !== null) {
|
|
@@ -687,10 +855,9 @@ var AuthService = class extends core.BaseService {
|
|
|
687
855
|
queueMicrotask(() => {
|
|
688
856
|
if (this._stateListeners.has(callback)) {
|
|
689
857
|
const session = store.token && store.user ? {
|
|
858
|
+
id: extractSessionId(store.token),
|
|
690
859
|
access_token: store.token,
|
|
691
860
|
refresh_token: store.refreshToken ?? "",
|
|
692
|
-
accessToken: store.token,
|
|
693
|
-
refreshToken: store.refreshToken ?? "",
|
|
694
861
|
token_type: "Bearer",
|
|
695
862
|
expires_in: 900,
|
|
696
863
|
user: store.user
|
|
@@ -701,11 +868,11 @@ var AuthService = class extends core.BaseService {
|
|
|
701
868
|
return result;
|
|
702
869
|
}
|
|
703
870
|
_emitEvent(event, user, accessToken, extra) {
|
|
871
|
+
if (event === "SIGNED_OUT") this._signedOutSeq = this._clearSeq;
|
|
704
872
|
const session = accessToken && user ? {
|
|
873
|
+
id: extractSessionId(accessToken),
|
|
705
874
|
access_token: accessToken,
|
|
706
875
|
refresh_token: this.client.authStore.refreshToken ?? "",
|
|
707
|
-
accessToken,
|
|
708
|
-
refreshToken: this.client.authStore.refreshToken ?? "",
|
|
709
876
|
token_type: "Bearer",
|
|
710
877
|
expires_in: 900,
|
|
711
878
|
user
|
|
@@ -735,16 +902,18 @@ var AuthService = class extends core.BaseService {
|
|
|
735
902
|
}
|
|
736
903
|
if (req.options?.data) payload.data = req.options.data;
|
|
737
904
|
if (req.options?.emailRedirectTo) payload.email_redirect_to = req.options.emailRedirectTo;
|
|
905
|
+
if (req.options?.preventEmailEnumeration) payload.prevent_email_enumeration = true;
|
|
738
906
|
const res = await this.request("POST", `${this.prefix}/register`, payload);
|
|
907
|
+
if (res.data && !isMfaChallenge(res.data) && !res.data.access_token) {
|
|
908
|
+
res.data.session = null;
|
|
909
|
+
return res;
|
|
910
|
+
}
|
|
739
911
|
if (res.data && !isMfaChallenge(res.data)) {
|
|
740
912
|
const tp = res.data;
|
|
741
|
-
tp.accessToken = tp.access_token;
|
|
742
|
-
tp.refreshToken = tp.refresh_token;
|
|
743
913
|
tp.session = {
|
|
914
|
+
id: extractSessionId(tp.access_token),
|
|
744
915
|
access_token: tp.access_token,
|
|
745
916
|
refresh_token: tp.refresh_token,
|
|
746
|
-
accessToken: tp.access_token,
|
|
747
|
-
refreshToken: tp.refresh_token,
|
|
748
917
|
token_type: tp.token_type,
|
|
749
918
|
expires_in: tp.expires_in,
|
|
750
919
|
user: tp.user,
|
|
@@ -756,11 +925,6 @@ var AuthService = class extends core.BaseService {
|
|
|
756
925
|
}
|
|
757
926
|
if (res.data && isMfaChallenge(res.data)) {
|
|
758
927
|
const mfa = res.data;
|
|
759
|
-
mfa.mfaChallenge = {
|
|
760
|
-
mfaRequired: mfa.mfa_required,
|
|
761
|
-
mfaToken: mfa.mfa_token,
|
|
762
|
-
mfaSetupRequired: mfa.mfa_setup_required
|
|
763
|
-
};
|
|
764
928
|
this._emitEvent("MFA_CHALLENGE", null, null, {
|
|
765
929
|
mfaToken: mfa.mfa_token,
|
|
766
930
|
mfaSetupRequired: mfa.mfa_setup_required
|
|
@@ -782,13 +946,10 @@ var AuthService = class extends core.BaseService {
|
|
|
782
946
|
const res = await this.request("POST", `${this.prefix}/login`, payload);
|
|
783
947
|
if (res.data && !isMfaChallenge(res.data)) {
|
|
784
948
|
const tp = res.data;
|
|
785
|
-
tp.accessToken = tp.access_token;
|
|
786
|
-
tp.refreshToken = tp.refresh_token;
|
|
787
949
|
tp.session = {
|
|
950
|
+
id: extractSessionId(tp.access_token),
|
|
788
951
|
access_token: tp.access_token,
|
|
789
952
|
refresh_token: tp.refresh_token,
|
|
790
|
-
accessToken: tp.access_token,
|
|
791
|
-
refreshToken: tp.refresh_token,
|
|
792
953
|
token_type: tp.token_type,
|
|
793
954
|
expires_in: tp.expires_in,
|
|
794
955
|
user: tp.user,
|
|
@@ -800,11 +961,6 @@ var AuthService = class extends core.BaseService {
|
|
|
800
961
|
}
|
|
801
962
|
if (res.data && isMfaChallenge(res.data)) {
|
|
802
963
|
const mfa = res.data;
|
|
803
|
-
mfa.mfaChallenge = {
|
|
804
|
-
mfaRequired: mfa.mfa_required,
|
|
805
|
-
mfaToken: mfa.mfa_token,
|
|
806
|
-
mfaSetupRequired: mfa.mfa_setup_required
|
|
807
|
-
};
|
|
808
964
|
this._emitEvent("MFA_CHALLENGE", null, null, {
|
|
809
965
|
mfaToken: mfa.mfa_token,
|
|
810
966
|
mfaSetupRequired: mfa.mfa_setup_required
|
|
@@ -833,7 +989,7 @@ var AuthService = class extends core.BaseService {
|
|
|
833
989
|
error: new core.AuraError(0, {
|
|
834
990
|
error: {
|
|
835
991
|
code: "invalid_provider",
|
|
836
|
-
message: "signInWithOAuth:
|
|
992
|
+
message: "signInWithOAuth: the provider must be a non-empty string, e.g. signInWithOAuth('google', { redirectTo }) or signInWithOAuth({ provider: 'google', redirectTo })"
|
|
837
993
|
}
|
|
838
994
|
})
|
|
839
995
|
};
|
|
@@ -847,22 +1003,22 @@ var AuthService = class extends core.BaseService {
|
|
|
847
1003
|
params.set(k, v);
|
|
848
1004
|
continue;
|
|
849
1005
|
}
|
|
850
|
-
const raison = OAUTH_SDK_OWNED_QUERY_PARAMS.has(k) ? `
|
|
1006
|
+
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(", ")}`;
|
|
851
1007
|
return {
|
|
852
1008
|
data: null,
|
|
853
1009
|
error: new core.AuraError(400, {
|
|
854
1010
|
error: {
|
|
855
1011
|
code: "invalid_oauth_query_param",
|
|
856
|
-
message: `signInWithOAuth: queryParams.${k}
|
|
1012
|
+
message: `signInWithOAuth: queryParams.${k} rejected \u2014 ${raison}.`
|
|
857
1013
|
}
|
|
858
1014
|
})
|
|
859
1015
|
};
|
|
860
1016
|
}
|
|
861
1017
|
}
|
|
862
|
-
if (options?.flowType === "pkce") {
|
|
1018
|
+
if ((options?.flowType ?? this._defaultFlowType) === "pkce") {
|
|
863
1019
|
if (typeof window !== "undefined" && !this.client.authStore.persistent) {
|
|
864
1020
|
console.warn(
|
|
865
|
-
|
|
1021
|
+
'[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.'
|
|
866
1022
|
);
|
|
867
1023
|
}
|
|
868
1024
|
try {
|
|
@@ -877,7 +1033,7 @@ var AuthService = class extends core.BaseService {
|
|
|
877
1033
|
error: new core.AuraError(0, {
|
|
878
1034
|
error: {
|
|
879
1035
|
code: "pkce_generation_failed",
|
|
880
|
-
message: "PKCE:
|
|
1036
|
+
message: "PKCE: cannot generate the code_verifier/challenge (WebCrypto unavailable)"
|
|
881
1037
|
}
|
|
882
1038
|
})
|
|
883
1039
|
};
|
|
@@ -885,7 +1041,7 @@ var AuthService = class extends core.BaseService {
|
|
|
885
1041
|
} else {
|
|
886
1042
|
if (typeof window !== "undefined" && !this.client.authStore.persistent) {
|
|
887
1043
|
console.warn(
|
|
888
|
-
'[aurabase]
|
|
1044
|
+
'[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).'
|
|
889
1045
|
);
|
|
890
1046
|
}
|
|
891
1047
|
try {
|
|
@@ -894,7 +1050,7 @@ var AuthService = class extends core.BaseService {
|
|
|
894
1050
|
}
|
|
895
1051
|
}
|
|
896
1052
|
const url = `${this.client.baseUrl}/v1/auth/oauth/${encodeURIComponent(provider)}/start?${params.toString()}`;
|
|
897
|
-
if (typeof window !== "undefined") {
|
|
1053
|
+
if (typeof window !== "undefined" && window.location) {
|
|
898
1054
|
window.location.href = url;
|
|
899
1055
|
}
|
|
900
1056
|
return { data: { url }, error: null };
|
|
@@ -951,31 +1107,22 @@ var AuthService = class extends core.BaseService {
|
|
|
951
1107
|
return res;
|
|
952
1108
|
}
|
|
953
1109
|
/**
|
|
954
|
-
* Session courante lue depuis le store (
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
* - Permet `const { data: { session }, error } = await aura.auth.getSession()` (forme canonique)
|
|
958
|
-
* - Permet `const { accessToken, user } = await aura.auth.getSession()` (forme plate historique)
|
|
1110
|
+
* Session courante lue depuis le store (pas d'appel réseau). Forme unique (AUTH-019) :
|
|
1111
|
+
* `const { data: { session } } = await aura.auth.getSession()`.
|
|
1112
|
+
* Côté serveur, cette session n'est PAS vérifiée : autorisez avec `getUser()` (AUTH-004).
|
|
959
1113
|
*/
|
|
960
1114
|
getSession() {
|
|
961
1115
|
const token = this.client.authStore.token;
|
|
962
1116
|
const user = this.client.authStore.user;
|
|
963
1117
|
const session = token && user ? {
|
|
1118
|
+
id: extractSessionId(token),
|
|
964
1119
|
access_token: token,
|
|
965
1120
|
refresh_token: this.client.authStore.refreshToken ?? "",
|
|
966
|
-
accessToken: token,
|
|
967
|
-
refreshToken: this.client.authStore.refreshToken ?? "",
|
|
968
1121
|
token_type: "Bearer",
|
|
969
1122
|
expires_in: 900,
|
|
970
1123
|
user
|
|
971
1124
|
} : null;
|
|
972
|
-
return Promise.resolve({
|
|
973
|
-
data: { session },
|
|
974
|
-
error: null,
|
|
975
|
-
accessToken: token,
|
|
976
|
-
access_token: token,
|
|
977
|
-
user
|
|
978
|
-
});
|
|
1125
|
+
return Promise.resolve({ data: { session }, error: null });
|
|
979
1126
|
}
|
|
980
1127
|
/**
|
|
981
1128
|
* 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).
|
|
@@ -1243,7 +1390,7 @@ var AuthService = class extends core.BaseService {
|
|
|
1243
1390
|
error: new core.AuraError(0, {
|
|
1244
1391
|
error: {
|
|
1245
1392
|
code: "allow_expired_unsupported_on_fallback",
|
|
1246
|
-
message: "getClaims: allowExpired
|
|
1393
|
+
message: "getClaims: allowExpired cannot be honored on the server validation path (HS256/JWKS fallback)"
|
|
1247
1394
|
}
|
|
1248
1395
|
})
|
|
1249
1396
|
};
|
|
@@ -1271,7 +1418,7 @@ var AuthService = class extends core.BaseService {
|
|
|
1271
1418
|
error: new core.AuraError(0, {
|
|
1272
1419
|
error: {
|
|
1273
1420
|
code: "invalid_response",
|
|
1274
|
-
message: "getClaims:
|
|
1421
|
+
message: "getClaims: the server token validation (getUser) answered with no content, so the claims cannot be determined"
|
|
1275
1422
|
}
|
|
1276
1423
|
})
|
|
1277
1424
|
};
|
|
@@ -1287,6 +1434,17 @@ var AuthService = class extends core.BaseService {
|
|
|
1287
1434
|
* Supprime le compte de l'utilisateur connecté après confirmation par son mot de passe, puis vide la session locale.
|
|
1288
1435
|
*/
|
|
1289
1436
|
async deleteUser(password) {
|
|
1437
|
+
if (!password || typeof password !== "string" || password.trim() === "") {
|
|
1438
|
+
return {
|
|
1439
|
+
data: null,
|
|
1440
|
+
error: new core.AuraError(400, {
|
|
1441
|
+
error: {
|
|
1442
|
+
code: "PASSWORD_REQUIRED",
|
|
1443
|
+
message: "Password is required to delete account"
|
|
1444
|
+
}
|
|
1445
|
+
})
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1290
1448
|
const res = await this.request("DELETE", `${this.prefix}/user`, { password });
|
|
1291
1449
|
if (!res.error) {
|
|
1292
1450
|
this.client.authStore.clear();
|
|
@@ -1348,9 +1506,107 @@ var AuthService = class extends core.BaseService {
|
|
|
1348
1506
|
regenerateRecoveryCodes: (codeOrParams) => {
|
|
1349
1507
|
const code = typeof codeOrParams === "string" ? codeOrParams : codeOrParams.code;
|
|
1350
1508
|
return this.regenerateRecoveryCodes(code);
|
|
1509
|
+
},
|
|
1510
|
+
factors: (mfaToken) => this.request("POST", `${this.prefix}/mfa/factors`, { mfa_token: mfaToken }),
|
|
1511
|
+
sms: {
|
|
1512
|
+
setup: (phone) => this.request("POST", `${this.prefix}/mfa/sms/setup`, { phone }),
|
|
1513
|
+
verifySetup: (code) => this.request("POST", `${this.prefix}/mfa/sms/setup/verify`, { code }),
|
|
1514
|
+
send: (mfaToken) => this.request("POST", `${this.prefix}/mfa/sms/send`, {
|
|
1515
|
+
mfa_token: mfaToken
|
|
1516
|
+
}),
|
|
1517
|
+
remove: (password) => this.request("DELETE", `${this.prefix}/mfa/sms`, { password })
|
|
1518
|
+
},
|
|
1519
|
+
webauthn: {
|
|
1520
|
+
register: (params) => this.webauthnRegister(params),
|
|
1521
|
+
verify: (params) => this.webauthnVerify(params),
|
|
1522
|
+
list: () => this.request("GET", `${this.prefix}/mfa/webauthn`),
|
|
1523
|
+
remove: (id, password) => this.request(
|
|
1524
|
+
"DELETE",
|
|
1525
|
+
`${this.prefix}/mfa/webauthn/${encodeURIComponent(id)}`,
|
|
1526
|
+
{ password }
|
|
1527
|
+
)
|
|
1351
1528
|
}
|
|
1352
1529
|
};
|
|
1353
1530
|
}
|
|
1531
|
+
webauthnIndisponible() {
|
|
1532
|
+
return {
|
|
1533
|
+
data: null,
|
|
1534
|
+
error: new core.AuraError(0, {
|
|
1535
|
+
error: {
|
|
1536
|
+
code: "webauthn_unavailable",
|
|
1537
|
+
message: "WebAuthn is not available here: navigator.credentials is missing. Use a browser in a secure context (https or localhost), or pass `credentials` explicitly."
|
|
1538
|
+
}
|
|
1539
|
+
})
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Enrôle une passkey WebAuthn pour l'utilisateur connecté (cérémonie complète : options du serveur,
|
|
1544
|
+
* `navigator.credentials.create`, confirmation). Le premier facteur du compte active le MFA et
|
|
1545
|
+
* renvoie les codes de récupération (`recovery_codes`) — à afficher une seule fois.
|
|
1546
|
+
*/
|
|
1547
|
+
async webauthnRegister(params) {
|
|
1548
|
+
const conteneur = params?.credentials ?? (typeof navigator !== "undefined" ? navigator.credentials : void 0);
|
|
1549
|
+
if (!conteneur) return this.webauthnIndisponible();
|
|
1550
|
+
const debut = await this.request(
|
|
1551
|
+
"POST",
|
|
1552
|
+
`${this.prefix}/mfa/webauthn/register/start`,
|
|
1553
|
+
{ friendly_name: params?.friendlyName }
|
|
1554
|
+
);
|
|
1555
|
+
if (!debut.data) return debut;
|
|
1556
|
+
let cred;
|
|
1557
|
+
try {
|
|
1558
|
+
cred = await conteneur.create(creationOptionsFromJson(debut.data.options));
|
|
1559
|
+
} catch (e) {
|
|
1560
|
+
return this.webauthnAnnule(e);
|
|
1561
|
+
}
|
|
1562
|
+
if (!cred) return this.webauthnAnnule(new Error("no credential returned"));
|
|
1563
|
+
return this.request("POST", `${this.prefix}/mfa/webauthn/register/finish`, {
|
|
1564
|
+
credential: attestationToJson(cred),
|
|
1565
|
+
friendly_name: params?.friendlyName
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Relève un défi MFA (`mfaToken`, reçu d'un `signIn`) avec une passkey : options du serveur,
|
|
1570
|
+
* `navigator.credentials.get`, vérification. Enregistre la session et émet `SIGNED_IN`.
|
|
1571
|
+
*/
|
|
1572
|
+
async webauthnVerify(params) {
|
|
1573
|
+
const conteneur = params.credentials ?? (typeof navigator !== "undefined" ? navigator.credentials : void 0);
|
|
1574
|
+
if (!conteneur) return this.webauthnIndisponible();
|
|
1575
|
+
const debut = await this.request(
|
|
1576
|
+
"POST",
|
|
1577
|
+
`${this.prefix}/mfa/webauthn/verify/start`,
|
|
1578
|
+
{ mfa_token: params.mfaToken }
|
|
1579
|
+
);
|
|
1580
|
+
if (!debut.data) return debut;
|
|
1581
|
+
let cred;
|
|
1582
|
+
try {
|
|
1583
|
+
cred = await conteneur.get(requestOptionsFromJson(debut.data.options));
|
|
1584
|
+
} catch (e) {
|
|
1585
|
+
return this.webauthnAnnule(e);
|
|
1586
|
+
}
|
|
1587
|
+
if (!cred) return this.webauthnAnnule(new Error("no credential returned"));
|
|
1588
|
+
const res = await this.request("POST", `${this.prefix}/mfa/webauthn/verify/finish`, {
|
|
1589
|
+
mfa_token: params.mfaToken,
|
|
1590
|
+
credential: assertionToJson(cred)
|
|
1591
|
+
});
|
|
1592
|
+
if (res.data) {
|
|
1593
|
+
this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
|
|
1594
|
+
this._emitEvent("SIGNED_IN", res.data.user, res.data.access_token);
|
|
1595
|
+
}
|
|
1596
|
+
return res;
|
|
1597
|
+
}
|
|
1598
|
+
webauthnAnnule(cause) {
|
|
1599
|
+
const nom = cause instanceof Error ? cause.name : "Error";
|
|
1600
|
+
return {
|
|
1601
|
+
data: null,
|
|
1602
|
+
error: new core.AuraError(0, {
|
|
1603
|
+
error: {
|
|
1604
|
+
code: nom === "NotAllowedError" ? "webauthn_cancelled" : "webauthn_failed",
|
|
1605
|
+
message: nom === "NotAllowedError" ? "The passkey prompt was cancelled or timed out." : `The passkey ceremony failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1606
|
+
}
|
|
1607
|
+
})
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1354
1610
|
/** MFA : démarre l'enrôlement TOTP et renvoie le `secret` et l'URL `otpauth_url` à afficher en QR code. */
|
|
1355
1611
|
async mfaEnroll() {
|
|
1356
1612
|
return this.request("POST", `${this.prefix}/mfa/setup`);
|
|
@@ -1363,10 +1619,31 @@ var AuthService = class extends core.BaseService {
|
|
|
1363
1619
|
* MFA : termine une connexion qui a renvoyé un défi (`mfa_token` et code TOTP). Enregistre la session et émet `SIGNED_IN`.
|
|
1364
1620
|
*/
|
|
1365
1621
|
async mfaVerify(req) {
|
|
1366
|
-
const
|
|
1622
|
+
const raw = req;
|
|
1623
|
+
const mfa_token = raw.mfa_token ?? raw.mfaToken ?? raw.challenge_id ?? raw.challengeId;
|
|
1624
|
+
const code = raw.code;
|
|
1625
|
+
const recovery_code = raw.recovery_code ?? raw.recoveryCode;
|
|
1626
|
+
const sms_code = raw.sms_code ?? raw.smsCode;
|
|
1627
|
+
const body = {};
|
|
1628
|
+
if (mfa_token) body.mfa_token = mfa_token;
|
|
1629
|
+
if (code !== void 0) body.code = code;
|
|
1630
|
+
if (recovery_code !== void 0) body.recovery_code = recovery_code;
|
|
1631
|
+
if (sms_code !== void 0) body.sms_code = sms_code;
|
|
1632
|
+
const res = await this.request("POST", `${this.prefix}/mfa/verify-login`, body);
|
|
1367
1633
|
if (res.data) {
|
|
1368
|
-
|
|
1369
|
-
|
|
1634
|
+
const tp = res.data;
|
|
1635
|
+
tp.session = {
|
|
1636
|
+
id: extractSessionId(tp.access_token),
|
|
1637
|
+
access_token: tp.access_token,
|
|
1638
|
+
refresh_token: tp.refresh_token,
|
|
1639
|
+
token_type: tp.token_type,
|
|
1640
|
+
expires_in: tp.expires_in,
|
|
1641
|
+
user: tp.user,
|
|
1642
|
+
provider_token: tp.provider_token,
|
|
1643
|
+
provider_refresh_token: tp.provider_refresh_token
|
|
1644
|
+
};
|
|
1645
|
+
this.client.authStore.save(tp.access_token, tp.refresh_token, tp.user);
|
|
1646
|
+
this._emitEvent("SIGNED_IN", tp.user, tp.access_token);
|
|
1370
1647
|
}
|
|
1371
1648
|
return res;
|
|
1372
1649
|
}
|
|
@@ -1520,10 +1797,10 @@ var AuthService = class extends core.BaseService {
|
|
|
1520
1797
|
async getSettings() {
|
|
1521
1798
|
return this.request("GET", `${this.prefix}/settings`);
|
|
1522
1799
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1800
|
+
// AUTH-023 — `getAnonKey()` retirée : la route `/anon-key` n'est servie qu'aux projets qui
|
|
1801
|
+
// activent l'auth hébergée (garde C1, aucun BaaS n'expose la clé d'un projet à un anonyme), et
|
|
1802
|
+
// un client qui l'appelle possède déjà la clé anon (passée à createClient). La méthode
|
|
1803
|
+
// répondait donc 404 pour presque tous les projets.
|
|
1527
1804
|
// -- Password recovery (token consumption) ----------------------------------
|
|
1528
1805
|
/**
|
|
1529
1806
|
* Consomme le token de récupération de mot de passe et applique le nouveau mot de passe.
|
|
@@ -1563,4 +1840,10 @@ var AuthService = class extends core.BaseService {
|
|
|
1563
1840
|
exports.AuthAdminService = AuthAdminService;
|
|
1564
1841
|
exports.AuthService = AuthService;
|
|
1565
1842
|
exports.SessionManager = SessionManager;
|
|
1843
|
+
exports.assertionToJson = assertionToJson;
|
|
1844
|
+
exports.attestationToJson = attestationToJson;
|
|
1845
|
+
exports.base64urlToBytes = base64urlToBytes;
|
|
1846
|
+
exports.bytesToBase64url = bytesToBase64url;
|
|
1847
|
+
exports.creationOptionsFromJson = creationOptionsFromJson;
|
|
1566
1848
|
exports.isMfaChallenge = isMfaChallenge;
|
|
1849
|
+
exports.requestOptionsFromJson = requestOptionsFromJson;
|