@aurabase/auth 0.4.0 → 0.5.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/README.md CHANGED
@@ -23,7 +23,7 @@ Dans l'usage courant, `AuthService` n'est jamais instancié directement : il est
23
23
  ```ts
24
24
  import { createClient } from '@aurabase/aurabase-js'
25
25
 
26
- const aura = createClient('http://localhost:8080', 'my-project', 'aura_anon_xxx')
26
+ const aura = createClient('https://mon-projet.aurabase.cloud', 'aura_anon_xxx')
27
27
  ```
28
28
 
29
29
  ### Mot de passe
@@ -48,7 +48,10 @@ const refreshed = await aura.auth.refreshSession()
48
48
  await aura.auth.setSession('stored-access-token', 'stored-refresh-token')
49
49
 
50
50
  const sessions = await aura.auth.listSessions()
51
- await aura.auth.revokeSession(sessions.data![0].id)
51
+ // `data` peut être `null` même en succès (204 / corps vide) : on le teste plutôt
52
+ // que `error`, ce qui évite l'assertion `!` et fait narrower TypeScript.
53
+ const first = sessions.data?.[0]
54
+ if (first) await aura.auth.revokeSession(first.id)
52
55
  await aura.auth.revokeAllSessions()
53
56
  ```
54
57
 
@@ -64,20 +67,103 @@ const { data, error } = await aura.auth.signInWithOAuth('github', {
64
67
  })
65
68
 
66
69
  if (error) throw error
70
+ if (!data) throw new Error("aucune URL de redirection renvoyée")
67
71
 
68
- // Côté serveur / SSR : pas de redirection automatique, il faut rediriger manuellement vers data!.url
69
- console.log(data!.url)
72
+ // Côté serveur / SSR : pas de redirection automatique, il faut rediriger manuellement vers data.url
73
+ console.log(data.url)
70
74
  ```
71
75
 
76
+ #### `queryParams` — ce qui atteint vraiment le provider
77
+
78
+ ```ts
79
+ await aura.auth.signInWithOAuth('google', {
80
+ queryParams: { prompt: 'consent', access_type: 'offline', login_hint: 'user@example.com' },
81
+ })
82
+ ```
83
+
84
+ Ces clés-là — et elles seules — sont recopiées dans l'URL d'autorisation du provider :
85
+
86
+ | Clé | Origine |
87
+ | --- | --- |
88
+ | `prompt`, `login_hint`, `display`, `ui_locales`, `max_age`, `acr_values`, `claims_locales` | OpenID Connect Core §3.1.2.1 |
89
+ | `access_type`, `include_granted_scopes`, `hd` | Google OAuth 2.0 |
90
+
91
+ **Toute autre clé est refusée**, jamais transmise et jamais ignorée en silence : la méthode
92
+ renvoie `error.code === 'invalid_oauth_query_param'` sans déclencher de redirection.
93
+
94
+ > `hd` est un **indice** d'affichage Google, pas un contrôle : il n'empêche pas de se
95
+ > connecter avec un autre domaine. Pour restreindre pour de bon, vérifiez le domaine de
96
+ > l'utilisateur au retour du flux.
97
+
98
+ - Les paramètres du protocole (`state`, `nonce`, `client_id`, `response_type`, `scope`,
99
+ `code_challenge`, `code_challenge_method`, `response_mode`…) sont **calculés par le
100
+ serveur** : les laisser fournir par un client permettrait de détourner le flux.
101
+ - Les paramètres de la requête (`redirect_uri`, `scopes`) ont leur option dédiée
102
+ (`redirectTo`, `scopes`). `project_id` n'est pas fournissable non plus : le projet est
103
+ celui de l'URL passée à `createClient()`.
104
+ - Les variantes de casse (`Prompt`, `State`) et les clés inconnues sont refusées aussi.
105
+
106
+ `redirectTo` part en `redirect_uri` (le backend accepte aussi l'alias `redirect_to`, nom
107
+ employé par les SDK Dart/Python/Rust) : il est validé contre les URLs autorisées du projet
108
+ et ne remplace jamais le `redirect_uri` envoyé au provider, qui reste le callback Aurabase.
109
+
110
+ #### Où atterrit l'utilisateur au retour
111
+
112
+ | Cas | URL servie |
113
+ | --- | --- |
114
+ | `redirectTo` fournie, connexion aboutie | `{redirectTo}#access_token=…&refresh_token=…&expires_in=…&token_type=bearer&user=…` — **l'URL exacte**, aucun segment ajouté |
115
+ | `redirectTo` omise, connexion aboutie | `{site_url du projet}/oauth/callback#access_token=…` (`site_url` est une base, pas une page) |
116
+ | **Utilisateur avec MFA actif** | `{redirectTo}/oauth/mfa#mfa_token=…` — ou `{site_url}/oauth/mfa#mfa_token=…` si `redirectTo` est omise |
117
+
118
+ L'utilisateur MFA n'a pas fini de se connecter : il reste un défi à relever, et `#mfa_token`
119
+ n'est pas une session. Le backend l'envoie donc sur un **sous-chemin dédié**, `…/oauth/mfa`,
120
+ plutôt que sur votre page d'atterrissage où le jeton ne serait attendu par personne. Ce
121
+ sous-chemin reste sous l'origine et le préfixe de chemin validés : il n'échappe pas à
122
+ l'allowlist.
123
+
124
+ Votre application doit donc servir une page sur `{redirectTo}/oauth/mfa`. Le SDK y fait le
125
+ reste : `detectSessionInUrl` reconnaît `#mfa_token`, émet `MFA_CHALLENGE` avec `mfaToken`
126
+ (**aucune session n'est posée**) et nettoie l'URL.
127
+
128
+ ```ts
129
+ aura.auth.onAuthStateChange(async ({ event, mfaToken }) => {
130
+ if (event !== 'MFA_CHALLENGE' || !mfaToken) return
131
+ const code = await demanderLeCodeTotp() // votre UI
132
+ await aura.auth.mfaVerify({ mfa_token: mfaToken, code }) // → SIGNED_IN
133
+ })
134
+ ```
135
+
136
+ > **Divergence Supabase assumée.** Une `redirectTo` **hors allowlist** fait échouer le
137
+ > démarrage (`400`). Supabase, lui, l'ignore en silence et retombe sur le Site URL. Aurabase
138
+ > préfère l'erreur explicite : une redirection silencieuse vers une autre destination que
139
+ > celle demandée est indétectable côté application.
140
+
141
+ La décision appartient au **backend** (`GET /v1/auth/oauth/{provider}/start`, qui répond
142
+ `400` sur les mêmes règles, plus le refus des doublons, des valeurs vides, démesurées ou
143
+ porteuses d'un caractère de contrôle) : cet endpoint est public, une allowlist purement
144
+ cliente ne protégerait rien. Le filtre du SDK n'en est que le miroir — un test de parité lit
145
+ la source Rust pour interdire toute divergence — et sert à échouer avant la redirection.
146
+
72
147
  Retour du provider (flux PKCE) :
73
148
 
74
149
  ```ts
75
- const session = await aura.auth.exchangeCodeForSession('auth-code-from-callback')
150
+ import { isMfaChallenge } from '@aurabase/auth'
151
+
152
+ const { data, error } = await aura.auth.exchangeCodeForSession('auth-code-from-callback')
76
153
 
77
154
  // Mobile natif : échange d'un ID token provider (Google/Apple) contre une session Aurabase
78
155
  await aura.auth.signInWithIdToken('google', 'id-token-from-provider')
79
156
  ```
80
157
 
158
+ Ces deux méthodes renvoient un `LoginResponse` (`TokenPair | MfaChallenge`), pas un `TokenPair` :
159
+ sur un utilisateur avec MFA actif, le backend répond `{ mfa_required: true, mfa_token }` et
160
+ **aucune session n'est enregistrée** — `MFA_CHALLENGE` est émis, à relever par `mfaVerify`.
161
+ Discriminez avant de lire les jetons :
162
+
163
+ ```ts
164
+ if (data && !isMfaChallenge(data)) console.log(data.access_token)
165
+ ```
166
+
81
167
  ### Magic link / OTP
82
168
 
83
169
  ```ts
@@ -85,10 +171,10 @@ await aura.auth.signInWithMagicLink({ email: 'user@example.com', redirect_url: '
85
171
  await aura.auth.verifyMagicLink('token-from-link')
86
172
 
87
173
  await aura.auth.signInWithEmailOtp('user@example.com')
88
- await aura.auth.verifyEmailOtp('user@example.com', '123456')
174
+ await aura.auth.verifyOtp({ type: 'email', email: 'user@example.com', token: '123456' })
89
175
 
90
176
  await aura.auth.signInWithSmsOtp('+15551234567')
91
- await aura.auth.verifySmsOtp('+15551234567', '123456')
177
+ await aura.auth.verifyOtp({ type: 'sms', phone: '+15551234567', token: '123456' })
92
178
  ```
93
179
 
94
180
  ### Anonyme
@@ -145,9 +231,9 @@ via `history.replaceState`).
145
231
  ### Vérification d'email
146
232
 
147
233
  ```ts
148
- await aura.auth.verifyEmail('token-from-email')
234
+ await aura.auth.verifyOtp({ type: 'signup', token: 'token-from-email' })
149
235
  await aura.auth.resendVerificationEmail() // reprend l'email de la session courante si omis
150
- await aura.auth.confirmEmailChange('token-from-email')
236
+ await aura.auth.verifyOtp({ type: 'email_change', token: 'token-from-email' })
151
237
  ```
152
238
 
153
239
  ### MFA (TOTP)
@@ -192,7 +278,7 @@ Réservé au serveur (clé `service_role`). Deux façons d'y accéder :
192
278
  // Via le client admin (recommandé)
193
279
  import { createAdminClient } from '@aurabase/aurabase-js'
194
280
 
195
- const admin = createAdminClient('http://localhost:8080', 'my-project', process.env.SERVICE_ROLE_KEY!)
281
+ const admin = createAdminClient('https://mon-projet.aurabase.cloud', process.env.SERVICE_ROLE_KEY!)
196
282
  await admin.authAdmin.listUsers({ limit: 50 })
197
283
  ```
198
284