Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 168 additions & 2 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@
- [Connected Accounts](#connected-accounts)
- [`onCallback` hook](#oncallback-hook)
- [`connectAccount` method](#connectaccount-method)
- [`getConnectedAccounts` method](#getconnectedaccounts-method)
- [`disconnectAccount` method](#disconnectaccount-method)
- [Back-Channel Logout](#back-channel-logout)
- [Session Expiry from the Upstream IdP](#session-expiry-from-the-upstream-idp)
- [Combining middleware](#combining-middleware)
Expand Down Expand Up @@ -4277,7 +4279,14 @@ export const auth0 = new Auth0Client({

### `connectAccount` method

In case you'd like to have more control over the connected accounts flow, a `connectAccount` method is also available on the Auth0 client instance. For example, you could mount a custom route to start the connected accounts flow, like so:
In case you'd like to have more control over the connected accounts flow, a `connectAccount` method is also available on the Auth0 client instance. It accepts an object with the following properties:

- `connection`: (required) the name of the connection to link the account with (e.g., `google-oauth2`, `facebook`).
- `scopes`: (optional) the scopes to request from the Identity Provider during the connect flow.
- `authorizationParams`: (optional) additional parameters passed to the authorization server. This is where a `login_hint` is supplied to pre-select which upstream account to connect (see below).
- `returnTo`: (optional) the URL to redirect to after the account is connected.

The method returns a `NextResponse` that carries the redirect and transaction cookies. For example, you could mount a custom route to start the connected accounts flow, like so:

```ts
import { auth0 } from "@/lib/auth0";
Expand All @@ -4297,9 +4306,144 @@ export async function GET() {
}
```

#### Connecting a specific account with `login_hint`

To connect a specific upstream account (for example, when a user wants to link more than one account on the same connection), pass a `login_hint` through `authorizationParams`. It is forwarded to the authorization server so the correct account is pre-selected during the connect flow:

```ts
import { auth0 } from "@/lib/auth0";

export async function GET() {
const res = await auth0.connectAccount({
connection: "google-oauth2",
scopes: ["openid", "profile", "offline_access"],
authorizationParams: {
login_hint: "alice@example.com"
},
returnTo: "/connected"
});

return res;
}
```

> [!NOTE]
> The `login_hint` on `connectAccount` (an authorization-request parameter passed via `authorizationParams`) is distinct from the top-level `login_hint` on [`getAccessTokenForConnection`](#getting-access-tokens-for-connections) (a token-exchange parameter). Connecting an account and later retrieving a token for it are separate operations, so the hint is supplied in the place appropriate to each.

#### Middleware and dynamic base URLs

When calling from middleware, or when `APP_BASE_URL` is configured dynamically (as an array of allowed origins), pass the `req` object so the redirect and session are resolved from the request context:

```ts
import { NextRequest } from "next/server";

import { auth0 } from "@/lib/auth0";

export async function middleware(request: NextRequest) {
const res = await auth0.connectAccount(
{ connection: "google-oauth2", returnTo: "/connected" },
request
);

return res;
}
```

> [!IMPORTANT]
> You must enable `Offline Access` from the Connection Permissions settings to be able to use the connection with Connected Accounts.

### `getConnectedAccounts` method

The `getConnectedAccounts` method lists the current user's connected accounts from the [My Account API](https://auth0.com/docs/manage-users/my-account-api). It returns an array of `ConnectedAccount` objects, each with the following shape:

- `id`: the unique identifier of the connected account (e.g., `cac_...`).
- `connection`: the name of the connection the account is linked through.
- `accessType` (optional): the access type. Currently returned as `"offline"` by the My Account API when present.
- `scopes`: the scopes granted for the connected account.
- `createdAt`: ISO date string of when the account was connected.
- `expiresAt`: (optional) ISO date string of when the connected account expires.
- `orgId`: (optional) the organization ID the connected account is scoped to. Only present for accounts bound to an organization.

Because the My Account API is the source of truth, this method also reconciles the session: any locally cached connection tokens whose connection is no longer present server-side are pruned, so stale tokens are not re-assembled on subsequent reads. As this may write cookies, call it from a context that can set them.

#### On the server (App Router)

```ts
import { NextResponse } from "next/server";

import { auth0 } from "@/lib/auth0";

export async function GET() {
const accounts = await auth0.getConnectedAccounts();

return NextResponse.json({ accounts });
}
```

> [!IMPORTANT]
> Do not call `getConnectedAccounts()` from a React Server Component. Minting the My Account access token can rotate the refresh token, and Server Components cannot write cookies — the rotated token is silently dropped. On the next request the browser still sends the old refresh token, which the authorization server rejects as replay and logs the user out. Call from a Route Handler, Server Action, API route, or middleware.

#### On the server (Pages Router) and middleware

Pass the `req` and `res` objects so the reconciled session can be persisted to the response cookies:

```ts
import type { NextApiRequest, NextApiResponse } from "next";

import { auth0 } from "@/lib/auth0";

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const accounts = await auth0.getConnectedAccounts(req, res);

res.status(200).json({ accounts });
}
```

### `disconnectAccount` method

The `disconnectAccount` method disconnects (unlinks) connected accounts for a given connection via the [My Account API](https://auth0.com/docs/manage-users/my-account-api). It revokes the connection server-side and removes the corresponding cached connection tokens from the session so they are not re-assembled on subsequent reads. It accepts an object with the following property:

- `connection`: (required) the name of the connection to disconnect (e.g., `google-oauth2`, `facebook`).

> [!NOTE]
> Disconnect is connection-scoped: **all** accounts connected through the given connection are disconnected. Per-account disconnect is not currently supported because the My Account API keys connected accounts by `id` and does not expose the login hint used to disambiguate multiple accounts on the same connection.

#### On the server (App Router)

```ts
import { NextResponse } from "next/server";

import { auth0 } from "@/lib/auth0";

export async function POST() {
await auth0.disconnectAccount({ connection: "google-oauth2" });

return NextResponse.json({ message: "Disconnected!" });
}
```

#### On the server (Pages Router) and middleware

Pass the `req` and `res` objects so the pruned session can be persisted to the response cookies:

```ts
import type { NextApiRequest, NextApiResponse } from "next";

import { auth0 } from "@/lib/auth0";

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
await auth0.disconnectAccount({ connection: "google-oauth2" }, req, res);

res.status(200).json({ message: "Disconnected!" });
}
```

## Back-Channel Logout

The SDK can be configured to listen to [Back-Channel Logout](https://auth0.com/docs/authenticate/login/logout/back-channel-logout) events. By default, a route will be mounted `/auth/backchannel-logout` which will verify the logout token and call the `deleteByLogoutToken` method of your session store implementation to allow you to remove the session.
Expand Down Expand Up @@ -4675,7 +4819,29 @@ export const GET = async (req: NextRequest) => {
You can retrieve an access token for a connection using the `getAccessTokenForConnection()` method, which accepts an object with the following properties:

- `connection`: The federated connection for which an access token should be retrieved.
- `login_hint`: The optional login_hint parameter to pass to the `/authorize` endpoint.
- `login_hint`: (optional) The login hint identifying which connected account to retrieve a token for. Provide it when a user has connected more than one account on the same connection so the correct one is selected; the token is then cached per `connection` + `login_hint`.

**Multi-account note.** The cache key is `connection` + `login_hint`. A call **with** a hint matches only entries stamped with the same hint. A call **without** a hint matches only entries that also have no hint — it does **not** match hinted entries. This isolation means an unhinted call cannot select and later overwrite a hinted entry, so cached per-account tokens for a connection are preserved across mixed hinted/unhinted usage. Existing sessions written before multi-account support have no hint stamped on any entry, so unhinted calls continue to match them as before (back-compat).

Without a login hint (single account per connection, or explicitly unhinted flow):

```ts
const token = await auth0.getAccessTokenForConnection({
connection: "google-oauth2"
});
```

With a login hint (to target a specific account among several on the same connection):

```ts
const token = await auth0.getAccessTokenForConnection({
connection: "google-oauth2",
login_hint: "alice@example.com"
});
```

> [!NOTE]
> If the underlying refresh-token exchange fails (for example, the upstream refresh token was revoked), the stale cached connection token for that account is cleared from the session before the error is thrown, so it is not left behind on subsequent requests.

### On the server (App Router)

Expand Down
4 changes: 3 additions & 1 deletion src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export { MtlsError, MtlsErrorCode } from "./mtls-errors.js";
export {
MyAccountApiError,
ConnectAccountError,
ConnectAccountErrorCodes
ConnectAccountErrorCodes,
ConnectedAccountsError,
ConnectedAccountsErrorCodes
} from "./my-account-errors.js";

export {
Expand Down
48 changes: 48 additions & 0 deletions src/errors/my-account-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,51 @@ export class ConnectAccountError extends SdkError {
this.cause = cause;
}
}

/**
* Enum representing error codes for connected-accounts operations
* (listing and disconnecting).
*/
export enum ConnectedAccountsErrorCodes {
/**
* The session is missing.
*/
MISSING_SESSION = "missing_session",

/**
* Failed to list the connected accounts.
*/
FAILED_TO_LIST = "failed_to_list",

/**
* Failed to delete the connected account.
*/
FAILED_TO_DELETE = "failed_to_delete"
}

/**
* Error class representing a connected-accounts operation error (listing or
* disconnecting).
*/
export class ConnectedAccountsError extends SdkError {
/**
* The error code associated with the connected-accounts error.
*/
public code: string;
public cause?: MyAccountApiError;

constructor({
code,
message,
cause
}: {
code: string;
message: string;
cause?: MyAccountApiError;
}) {
super(message);
this.name = "ConnectedAccountsError";
this.code = code;
this.cause = cause;
}
}
Loading
Loading