@aurabase/auth 0.4.1 → 0.5.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 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
@@ -73,15 +73,97 @@ if (!data) throw new Error("aucune URL de redirection renvoyée")
73
73
  console.log(data.url)
74
74
  ```
75
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
+
76
147
  Retour du provider (flux PKCE) :
77
148
 
78
149
  ```ts
79
- 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')
80
153
 
81
154
  // Mobile natif : échange d'un ID token provider (Google/Apple) contre une session Aurabase
82
155
  await aura.auth.signInWithIdToken('google', 'id-token-from-provider')
83
156
  ```
84
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
+
85
167
  ### Magic link / OTP
86
168
 
87
169
  ```ts
@@ -89,10 +171,10 @@ await aura.auth.signInWithMagicLink({ email: 'user@example.com', redirect_url: '
89
171
  await aura.auth.verifyMagicLink('token-from-link')
90
172
 
91
173
  await aura.auth.signInWithEmailOtp('user@example.com')
92
- await aura.auth.verifyEmailOtp('user@example.com', '123456')
174
+ await aura.auth.verifyOtp({ type: 'email', email: 'user@example.com', token: '123456' })
93
175
 
94
176
  await aura.auth.signInWithSmsOtp('+15551234567')
95
- await aura.auth.verifySmsOtp('+15551234567', '123456')
177
+ await aura.auth.verifyOtp({ type: 'sms', phone: '+15551234567', token: '123456' })
96
178
  ```
97
179
 
98
180
  ### Anonyme
@@ -149,9 +231,9 @@ via `history.replaceState`).
149
231
  ### Vérification d'email
150
232
 
151
233
  ```ts
152
- await aura.auth.verifyEmail('token-from-email')
234
+ await aura.auth.verifyOtp({ type: 'signup', token: 'token-from-email' })
153
235
  await aura.auth.resendVerificationEmail() // reprend l'email de la session courante si omis
154
- await aura.auth.confirmEmailChange('token-from-email')
236
+ await aura.auth.verifyOtp({ type: 'email_change', token: 'token-from-email' })
155
237
  ```
156
238
 
157
239
  ### MFA (TOTP)
@@ -196,7 +278,7 @@ Réservé au serveur (clé `service_role`). Deux façons d'y accéder :
196
278
  // Via le client admin (recommandé)
197
279
  import { createAdminClient } from '@aurabase/aurabase-js'
198
280
 
199
- 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!)
200
282
  await admin.authAdmin.listUsers({ limit: 50 })
201
283
  ```
202
284