|
| 1 | +# Migrating the other authentication flows |
| 2 | + |
| 3 | +This is the incremental part of the [Authentication Migration Guide](./index.md). Start with the guide's [OIDC token grants](./index.md#oidc-token-grants) and cross-cutting breaking changes before you touch anything here. Everything below builds on those changes, so apply them to every rewrite on this page too. |
| 4 | + |
| 5 | +> **Migrating with an AI agent?** Point it at the Auth0 migration skill (the `auth0` skill in [`auth0/agent-skills`](https://github.com/auth0/agent-skills), migration intent `migrate-node-auth0`). |
| 6 | +
|
| 7 | +Migrate one flow at a time. Only the flows your app actually uses need attention; skip the rest. |
| 8 | + |
| 9 | +- [Database connections](#database-connections) |
| 10 | +- [Passwordless](#passwordless) |
| 11 | +- [Backchannel authentication (CIBA)](#backchannel-authentication-ciba) |
| 12 | +- [Token exchange (RFC 8693)](#token-exchange-rfc-8693) |
| 13 | +- [UserInfoClient](#userinfoclient) |
| 14 | +- [Quick lookup table](#quick-lookup-table) |
| 15 | + |
| 16 | +Unless a row routes explicitly to `@auth0/auth0-server-js`, the replacement lives on the `@auth0/auth0-auth-js` `AuthClient` (or a sub-client: `authClient.database`, `authClient.passwordless`, `authClient.mfa`, `authClient.passkey`). |
| 17 | + |
| 18 | +## Database connections |
| 19 | + |
| 20 | +Database connection operations move to the `authClient.database` sub-client. Names and required parameters stay the same; only casing and return shape change. |
| 21 | + |
| 22 | +### `database.signUp` → `authClient.database.signUp` |
| 23 | + |
| 24 | +```ts |
| 25 | +// before |
| 26 | +const resp = await auth0.database.signUp({ |
| 27 | + email, |
| 28 | + password, |
| 29 | + connection: "Username-Password-Authentication", |
| 30 | + given_name: "Ada", |
| 31 | + family_name: "Lovelace", |
| 32 | + user_metadata: { plan: "free" }, |
| 33 | +}); |
| 34 | +const userId = resp.data.id; |
| 35 | +// after |
| 36 | +const result = await authClient.database.signUp({ |
| 37 | + email, |
| 38 | + password, |
| 39 | + connection: "Username-Password-Authentication", |
| 40 | + givenName: "Ada", |
| 41 | + familyName: "Lovelace", |
| 42 | + userMetadata: { plan: "free" }, |
| 43 | +}); |
| 44 | +const userId = result.id; |
| 45 | +``` |
| 46 | + |
| 47 | +> ID normalization is preserved: node-auth0 mapped the server's `_id | user_id | id` onto a single `id`. The new SDK does the same, so `result.id` is always present. Do not add your own `_id` fallback. |
| 48 | +
|
| 49 | +### `database.changePassword` → `authClient.database.changePassword` |
| 50 | + |
| 51 | +node-auth0 returned a `TextApiResponse` (read via `.data`); the new SDK returns the plain `string` directly. |
| 52 | + |
| 53 | +```ts |
| 54 | +// before |
| 55 | +const resp = await auth0.database.changePassword({ email, connection: "Username-Password-Authentication" }); |
| 56 | +const message = resp.data; // plain-text confirmation |
| 57 | +// after |
| 58 | +const message = await authClient.database.changePassword({ email, connection: "Username-Password-Authentication" }); |
| 59 | +``` |
| 60 | + |
| 61 | +> `changePassword` requires `connection` plus at least one of `email` or `username`: either identifier is accepted, not `email` alone. |
| 62 | +
|
| 63 | +## Passwordless |
| 64 | + |
| 65 | +node-auth0 lumped "start" (send the code or link) and "login" (redeem the code) onto one sub-client. The new SDK splits them: starting stays on `authClient.passwordless`; redeeming a code becomes a top-level grant method on `AuthClient`. |
| 66 | + |
| 67 | +### `passwordless.sendEmail` → `authClient.passwordless.sendEmail` |
| 68 | + |
| 69 | +```ts |
| 70 | +// before |
| 71 | +await auth0.passwordless.sendEmail({ email, send: "code" }); |
| 72 | +// after |
| 73 | +await authClient.passwordless.sendEmail({ email, send: "code" }); |
| 74 | +``` |
| 75 | + |
| 76 | +> Default changed: node-auth0 defaulted `send` to `'link'` (magic link). The new SDK defaults `send` to `'code'` (one-time password). If you relied on the implicit default to send magic links, set `send: 'link'` explicitly. |
| 77 | +
|
| 78 | +### `passwordless.sendSMS` → `authClient.passwordless.sendSms` |
| 79 | + |
| 80 | +Note the casing change: `sendSMS` → `sendSms`, and `phone_number` → `phoneNumber`. |
| 81 | + |
| 82 | +```ts |
| 83 | +// before |
| 84 | +await auth0.passwordless.sendSMS({ phone_number: "+15551234567" }); |
| 85 | +// after |
| 86 | +await authClient.passwordless.sendSms({ phoneNumber: "+15551234567" }); |
| 87 | +``` |
| 88 | + |
| 89 | +### `passwordless.loginWithEmail` → `getTokenByPasswordlessEmail` |
| 90 | + |
| 91 | +Redeeming the one-time password is now a grant method on `AuthClient`, not on the passwordless sub-client. |
| 92 | + |
| 93 | +```ts |
| 94 | +// before |
| 95 | +const resp = await auth0.passwordless.loginWithEmail({ email, code, audience, scope }); |
| 96 | +const token = resp.data.access_token; |
| 97 | +// after |
| 98 | +const tokens = await authClient.getTokenByPasswordlessEmail({ email, code, audience, scope }); |
| 99 | +const token = tokens.accessToken; |
| 100 | +``` |
| 101 | + |
| 102 | +### `passwordless.loginWithSMS` → `getTokenByPasswordlessSms` |
| 103 | + |
| 104 | +```ts |
| 105 | +// before |
| 106 | +const resp = await auth0.passwordless.loginWithSMS({ phone_number, code }); |
| 107 | +// after |
| 108 | +const tokens = await authClient.getTokenByPasswordlessSms({ phoneNumber, code }); |
| 109 | +``` |
| 110 | + |
| 111 | +> Session apps: `@auth0/auth0-server-js` exposes `startPasswordless` / `completePasswordless` / `completePasswordlessMagicLink`, which both send the code and establish a session. Use those instead of the two-step auth-js flow when the SDK owns the session. See [Migrating session apps](./server-side-sessions.md). |
| 112 | +
|
| 113 | +## Backchannel authentication (CIBA) |
| 114 | + |
| 115 | +CIBA is Client-Initiated Backchannel Authentication. |
| 116 | + |
| 117 | +### `backchannel.authorize` → `initiateBackchannelAuthentication` |
| 118 | + |
| 119 | +```ts |
| 120 | +// before |
| 121 | +const resp = await auth0.backchannel.authorize({ |
| 122 | + binding_message: "ABC123", |
| 123 | + scope: "openid", |
| 124 | + userId: "auth0|123", |
| 125 | +}); |
| 126 | +const authReqId = resp.auth_req_id; |
| 127 | +// after |
| 128 | +const { authReqId, expiresIn, interval } = await authClient.initiateBackchannelAuthentication({ |
| 129 | + bindingMessage: "ABC123", |
| 130 | + loginHint: { sub: "auth0|123" }, // login_hint is an object with `sub`, not a bare string |
| 131 | + authorizationParams: { scope: "openid" }, // scope goes here, NOT as a top-level key |
| 132 | +}); |
| 133 | +``` |
| 134 | + |
| 135 | +### `backchannel.backchannelGrant` → `backchannelAuthenticationGrant` |
| 136 | + |
| 137 | +```ts |
| 138 | +// before |
| 139 | +const resp = await auth0.backchannel.backchannelGrant({ auth_req_id: authReqId }); |
| 140 | +// after |
| 141 | +const tokens = await authClient.backchannelAuthenticationGrant({ authReqId }); |
| 142 | +``` |
| 143 | + |
| 144 | +> One-shot convenience: `authClient.backchannelAuthentication({ ... })` initiates and polls to completion, returning a `TokenResponse`. Use it if your code did the initiate-then-poll loop by hand. |
| 145 | +> |
| 146 | +> Session apps: for CIBA that also establishes a session, see [Migrating session apps](./server-side-sessions.md). |
| 147 | +
|
| 148 | +## Token exchange (RFC 8693) |
| 149 | + |
| 150 | +```ts |
| 151 | +// before |
| 152 | +const resp = await auth0.tokenExchange.exchangeToken({ |
| 153 | + subject_token_type: "urn:example:custom", |
| 154 | + subject_token: token, |
| 155 | + audience: "https://api.example.com", |
| 156 | + scope: "read", |
| 157 | +}); |
| 158 | +// after |
| 159 | +const tokens = await authClient.exchangeToken({ |
| 160 | + subjectTokenType: "urn:example:custom", |
| 161 | + subjectToken: token, |
| 162 | + audience: "https://api.example.com", |
| 163 | + scope: "read", |
| 164 | +}); |
| 165 | +``` |
| 166 | + |
| 167 | +> `exchangeToken` is overloaded: a custom-exchange profile shape (`subjectTokenType` + `subjectToken` + `audience`) and a Token Vault shape (`connection` present). Presence of `connection` routes to the vault path. The custom-exchange profile is the RFC 8693 replacement for `tokenExchange.exchangeToken`. |
| 168 | +> |
| 169 | +> Session apps: `@auth0/auth0-server-js` exposes `loginWithCustomTokenExchange` (exchange, then establish a session) and `customTokenExchange` (exchange, then return tokens with no session). |
| 170 | +
|
| 171 | +## UserInfoClient |
| 172 | + |
| 173 | +The standalone `UserInfoClient` from node-auth0 does not exist in the new SDK. Choose the replacement based on what the app needs: |
| 174 | + |
| 175 | +| Your intent | Replacement | |
| 176 | +| --- | --- | |
| 177 | +| Wanted user profile claims right after login | Read `TokenResponse.claims` from the grant result; the SDK already decodes the ID token. No extra `/userinfo` round-trip needed. **Preferred.** | |
| 178 | +| Wanted a live `/userinfo` response for an arbitrary access token | `await authClient.getUserInfo({ accessToken })`, a direct method on `AuthClient`. | |
| 179 | +| Wanted the profile in a server-rendered app with a session | `await serverClient.getUser()` returns the stored user claims from the session. | |
| 180 | + |
| 181 | +**Before (node-auth0):** |
| 182 | + |
| 183 | +```ts |
| 184 | +import { UserInfoClient } from "auth0"; |
| 185 | +const userInfo = new UserInfoClient({ domain }); |
| 186 | +const resp = await userInfo.getUserInfo(accessToken); |
| 187 | +const profile = resp.data; // { sub, name, email, ... } |
| 188 | +``` |
| 189 | + |
| 190 | +**After (preferred): use the claims you already have:** |
| 191 | + |
| 192 | +```ts |
| 193 | +const tokens = await authClient.getTokenByCode(callbackUrl, {}); |
| 194 | +const profile = tokens.claims; // { sub, name, email, ... } decoded from the id_token |
| 195 | +``` |
| 196 | + |
| 197 | +**After (direct method):** for when you only have an access token: |
| 198 | + |
| 199 | +```ts |
| 200 | +// Takes an options object: { accessToken, expectedSubject? } |
| 201 | +const profile = await authClient.getUserInfo({ accessToken }); |
| 202 | +``` |
| 203 | + |
| 204 | +> Prefer reading `claims` over any `/userinfo` call: it avoids a network round-trip and the claims are already validated by the SDK. |
| 205 | +
|
| 206 | +## Quick lookup table |
| 207 | + |
| 208 | +The complete node-auth0 → new SDK map, including the OIDC methods covered in the main guide. |
| 209 | + |
| 210 | +| node-auth0 | new SDK equivalent | Layer | |
| 211 | +| --- | --- | --- | |
| 212 | +| `oauth.authorizationCodeGrant` | `authClient.getTokenByCode(url, opts)` | auth-js | |
| 213 | +| `oauth.authorizationCodeGrantWithPKCE` | `authClient.getTokenByCode(url, { codeVerifier })` | auth-js | |
| 214 | +| `oauth.refreshTokenGrant` | `authClient.getTokenByRefreshToken({ refreshToken })` | auth-js | |
| 215 | +| `oauth.passwordGrant` | `authClient.getTokenByPassword({ ... })` | auth-js | |
| 216 | +| `oauth.clientCredentialsGrant` | `authClient.getTokenByClientCredentials({ audience })` | auth-js | |
| 217 | +| `oauth.revokeRefreshToken` | `authClient.revokeToken({ token })` / `serverClient.revokeRefreshToken()` | auth-js / server-js | |
| 218 | +| `oauth.tokenForConnection` | `authClient.exchangeToken({ connection, ... })` | auth-js | |
| 219 | +| `oauth.pushedAuthorization` | `authClient.buildAuthorizationUrl({ pushedAuthorizationRequests: true })` | auth-js | |
| 220 | +| `database.signUp` | `authClient.database.signUp({ ... })` | auth-js | |
| 221 | +| `database.changePassword` | `authClient.database.changePassword({ ... })` | auth-js | |
| 222 | +| `passwordless.sendEmail` | `authClient.passwordless.sendEmail({ ... })` | auth-js | |
| 223 | +| `passwordless.sendSMS` | `authClient.passwordless.sendSms({ phoneNumber })` | auth-js | |
| 224 | +| `passwordless.loginWithEmail` | `authClient.getTokenByPasswordlessEmail({ ... })` | auth-js | |
| 225 | +| `passwordless.loginWithSMS` | `authClient.getTokenByPasswordlessSms({ ... })` | auth-js | |
| 226 | +| `backchannel.authorize` | `authClient.initiateBackchannelAuthentication({ ... })` | auth-js | |
| 227 | +| `backchannel.backchannelGrant` | `authClient.backchannelAuthenticationGrant({ authReqId })` | auth-js | |
| 228 | +| `tokenExchange.exchangeToken` | `authClient.exchangeToken({ subjectTokenType, subjectToken, audience })` | auth-js | |
| 229 | +| `UserInfoClient.getUserInfo` | `TokenResponse.claims` (preferred) / `authClient.getUserInfo({ accessToken })` / `serverClient.getUser()` | auth-js / server-js | |
| 230 | +| (no equivalent): build `/authorize` URL | `authClient.buildAuthorizationUrl({ ... })` | auth-js | |
| 231 | +| (no equivalent): build `/v2/logout` URL | `authClient.buildLogoutUrl({ returnTo })` | auth-js | |
| 232 | +| `ManagementClient.*` | **not migrated, stays on `auth0`** | n/a | |
| 233 | + |
| 234 | +When you finish a flow, return to the [verification checklist](./index.md#verification-checklist) and confirm the four cross-cutting changes for every call site you touched. |
0 commit comments