> ## Documentation Index
> Fetch the complete documentation index at: https://docs.noresponse.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Auth0

> Assess a new Auth0 account with fromAuth0 and the Management API, and act on late results on every Auth0 plan.

NoResponse assesses each new account for abuse. It returns the current risk score of the account, from 0 to 100, where a higher score means more risk. It also returns a decision: `allow`, `step_up`, `review` or `block`.

Auth0 already knows facts that make the score more accurate. Examples are whether the user verified the email and when the user last passed a second factor. `auth_context` is the part of the assessment request that carries these facts. `fromAuth0` is the Auth0 adapter in each NoResponse SDK. It builds `auth_context` from the ID token claims that you verified, and you add the facts from the Auth0 user and session records.

Your backend does five things:

1. Verifies the ID token: its signature, issuer and audience.
2. `users.get`: gets the account age and the verified phone.
3. `sessions.get`: gets the session age.
4. `fromAuth0`: turns the claims and the records into `auth_context`.
5. `assessments.create`: NoResponse returns the assessment.

## What Auth0 can supply

| Fact | `auth_context` field | Where Auth0 keeps it | Effect on the risk score |
| - | - | - | - |
| Email verified | `email_verified` | The `email_verified` claim in the ID token. The token holds it when you request the `email` scope. | Lowers `auth_email_verified` |
| Phone verified | `phone_verified` | The `phone_number_verified` claim with the `phone` scope. For users of an SMS connection, also `phone_verified` on the user record. | Lowers `auth_phone_verified` |
| Recent second factor | `mfa` | The `amr` of the ID token holds `mfa` when the user passed a second factor during that login, at `auth_time`. The adapter maps `amr` to `mfa.present` but has no clock. So the samples add `mfa.age_seconds`, the seconds since `auth_time`. | Lowers `auth_mfa_recent`, when the age is 24 hours or less |
| Account age | `account_created_at` | `created_at` on the user record. A user that you imported into Auth0 long before they reached your app is not new. | Raises `auth_new_account`, when under 7 days |
| Session age | `session_created_at` | `created_at` on the session record whose id is the `sid` of the ID token. | Raises `auth_new_session`, when under 15 minutes |
| Last login | `auth_time` | The `auth_time` claim. | None NoResponse stores it with the assessment. |
| Organization, scopes | `organization_id`, `scopes` | The `org_id` claim with Auth0 Organizations, and `scope` or `permissions`. | None NoResponse stores it with the assessment. |
| Session and token | `session_id`, `issued_at`, `expires_at` | The `sid`, `iat` and `exp` claims. | None NoResponse stores it with the assessment. |

The session record comes from the sessions endpoint of the Management API. Auth0 offers this endpoint on its Enterprise plans. On another plan, delete the session lines from the samples. The assessment then has no session age.

## What Auth0 cannot supply

* **Impersonation.** Auth0 no longer offers impersonation. So the adapter sends `impersonated: false`, unless an Action of yours adds an `act` claim.
* **Roles.** Auth0 puts roles in a namespaced custom claim that you choose, so the adapter does not read them. To keep them with the assessment, pass them as an override, such as `roles: ["admin"]`.
* **An assurance level.** Auth0 has no equivalent of `assurance_level`.

## Install

```bash title="TypeScript" theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
npm install auth0 jose @noresponse/node
```

```bash title="Python" theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
pip install auth0-python "pyjwt[crypto]" noresponse
```

```bash title="Go" theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
go get github.com/auth0/go-auth0 github.com/coreos/go-oidc/v3 github.com/noresponsehq/go/v5
```

The samples use `auth0` 7 for Node.js, `auth0-python` 6 and `go-auth0` 1. Version 4 of the Node.js package has a different Management API client.

Do these three things in Auth0:

1. Request the `openid email phone` scopes at login, so that the ID token carries `email_verified` and `phone_number_verified`.
2. Create a machine-to-machine application with access to the Management API and the `read:users` and `read:sessions` scopes.
3. Keep the ID token signing algorithm at RS256, the Auth0 default.

Set these variables in the environment of your backend:

* `AUTH0_DOMAIN`: your Auth0 tenant domain, such as `example.us.auth0.com`. The Management API calls use it.
* `AUTH0_ISSUER`: optional. Set it when users log in through a custom domain. The value is `https://`, then that domain, then a trailing `/`. This value is the `iss` of the ID token. The default is `https://`, then `AUTH0_DOMAIN`, then `/`.
* `AUTH0_CLIENT_ID`: the client ID of the application that users log in to. It is the audience of the ID token.
* `AUTH0_M2M_CLIENT_ID` and `AUTH0_M2M_CLIENT_SECRET`: the credentials of the machine-to-machine application.
* `NORESPONSE_API_KEY`: your NoResponse key, with the `assess` scope. The TypeScript and Python clients read it themselves. The Go client takes it as an argument. The [quickstart](https://docs.noresponse.org/quickstart.md) tells you how to get one.

## Assess the new account

Assess each new account once. Do it on the first authenticated request for an Auth0 user that your database does not know yet. Do it before you create your own records for the account or give it access. The API answers within 3 seconds.

The ID token is the `id_token` in the Auth0 token response. Your callback route receives it after the login redirect. Pass an ID token, not an access token for your API, because an access token carries no verified email. Your Auth0 SDK can verify the ID token first. Then pass its claims to the adapter and skip step 1. In `express-openid-connect` the claims are `req.oidc.idTokenClaims`.

```ts title="TypeScript" theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import { createRemoteJWKSet, jwtVerify } from "jose";
import { ManagementClient } from "auth0";
import { NoResponse, fromAuth0 } from "@noresponse/node";

const domain = process.env.AUTH0_DOMAIN!;
const issuer = process.env.AUTH0_ISSUER ?? `https://${domain}/`;
const jwks = createRemoteJWKSet(new URL(`${issuer}.well-known/jwks.json`));
const management = new ManagementClient({
  domain,
  clientId: process.env.AUTH0_M2M_CLIENT_ID!,
  clientSecret: process.env.AUTH0_M2M_CLIENT_SECRET!,
});
const nr = new NoResponse(); // reads NORESPONSE_API_KEY

// The Management API types its times as string or object; they arrive as RFC 3339 strings.
const time = (value: unknown) => (typeof value === "string" ? value : undefined);

export async function assessAccountCreation(idToken: string, ip: string) {
  // 1. Verify the ID token, then read the user and the token's session.
  const { payload: claims } = await jwtVerify(idToken, jwks, { issuer, audience: process.env.AUTH0_CLIENT_ID! });
  const user = await management.users.get(claims.sub!);
  const session = typeof claims.sid === "string" ? await management.sessions.get(claims.sid) : undefined;

  // amr holding "mfa" means a second factor during this login, at auth_time.
  const amr = Array.isArray(claims.amr) ? claims.amr : [];
  const mfa = amr.includes("mfa") && typeof claims.auth_time === "number"
    ? { present: true, methods: ["mfa"], ageSeconds: Math.max(0, Math.floor(Date.now() / 1000) - claims.auth_time) }
    : undefined;

  // 2. The adapter maps the claims; the records supply the rest.
  const authContext = fromAuth0(claims, {
    accountCreatedAt: time(user.created_at),
    sessionCreatedAt: time(session?.created_at),
    phoneVerified: user.phone_verified,
    mfa,
  });

  // 3. NoResponse assesses the account creation.
  return nr.assessments.create({
    accountId: claims.sub!,
    identifiers: { email: user.email, ip },
    flow: "account_creation",
    authContext,
  });
}
```

```python title="Python" theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import os
import time
from datetime import datetime, timezone
from typing import Any

import jwt
from auth0.management import ManagementClient
from noresponse import Assessment, NoResponse, from_auth0

domain = os.environ["AUTH0_DOMAIN"]
issuer = os.environ.get("AUTH0_ISSUER", f"https://{domain}/")
jwks = jwt.PyJWKClient(f"{issuer}.well-known/jwks.json")
management = ManagementClient(
    domain=domain,
    client_id=os.environ["AUTH0_M2M_CLIENT_ID"],
    client_secret=os.environ["AUTH0_M2M_CLIENT_SECRET"],
)
nr = NoResponse()  # reads NORESPONSE_API_KEY


def rfc3339(value: Any) -> str | None:
    """auth0-python returns Management API times as datetime."""
    if isinstance(value, str):
        return value
    if not isinstance(value, datetime):
        return None
    return value.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def mfa_from(claims: dict[str, Any]) -> dict[str, Any] | None:
    """amr holding "mfa" means a second factor during this login, at auth_time."""
    amr = claims.get("amr")
    auth_time = claims.get("auth_time")
    if not isinstance(amr, list) or "mfa" not in amr or not isinstance(auth_time, int):
        return None
    return {"present": True, "methods": ["mfa"], "age_seconds": max(0, int(time.time()) - auth_time)}


def assess_account_creation(id_token: str, ip: str) -> Assessment:
    # 1. Verify the ID token, then read the user and the token's session.
    key = jwks.get_signing_key_from_jwt(id_token).key
    claims = jwt.decode(id_token, key, algorithms=["RS256"], issuer=issuer, audience=os.environ["AUTH0_CLIENT_ID"])
    user = management.users.get(claims["sub"])
    session = management.sessions.get(claims["sid"]) if "sid" in claims else None

    # 2. The adapter maps the claims; the records supply the rest.
    auth_context = from_auth0(claims, {
        "account_created_at": rfc3339(user.created_at),
        "session_created_at": rfc3339(session.created_at) if session else None,
        "phone_verified": user.phone_verified,
        "mfa": mfa_from(claims),
    })

    # 3. NoResponse assesses the account creation.
    return nr.assessments.create(
        account_id=claims["sub"],
        identifiers={"email": user.email, "ip": ip},
        flow="account_creation",
        auth_context=auth_context,
    )
```

```go title="Go" theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
package accounts

import (
	"context"
	"os"
	"slices"
	"time"

	"github.com/auth0/go-auth0/management"
	"github.com/coreos/go-oidc/v3/oidc"
	noresponse "github.com/noresponsehq/go/v5"
	"github.com/noresponsehq/go/v5/models/components"
)

var (
	nr       = noresponse.NewClient(os.Getenv("NORESPONSE_API_KEY"))
	verifier *oidc.IDTokenVerifier
	auth0API *management.Management
)

func init() {
	ctx := context.Background()
	domain := os.Getenv("AUTH0_DOMAIN")
	issuer := os.Getenv("AUTH0_ISSUER")
	if issuer == "" {
		issuer = "https://" + domain + "/"
	}
	// NewProvider reads the discovery document and caches the signing keys.
	provider, err := oidc.NewProvider(ctx, issuer)
	if err != nil {
		panic(err)
	}
	verifier = provider.Verifier(&oidc.Config{ClientID: os.Getenv("AUTH0_CLIENT_ID")})
	auth0API, err = management.New(domain, management.WithClientCredentials(ctx, os.Getenv("AUTH0_M2M_CLIENT_ID"), os.Getenv("AUTH0_M2M_CLIENT_SECRET")))
	if err != nil {
		panic(err)
	}
}

func AssessAccountCreation(ctx context.Context, idToken, ip string) (*components.Assessment, error) {
	// 1. Verify the ID token, then read the user and the token's session.
	tok, err := verifier.Verify(ctx, idToken)
	if err != nil {
		return nil, err
	}
	var claims map[string]any
	if err := tok.Claims(&claims); err != nil {
		return nil, err
	}
	usr, err := auth0API.User.Read(ctx, tok.Subject)
	if err != nil {
		return nil, err
	}

	// 2. The adapter maps the claims; the records supply the rest.
	overrides := map[string]any{
		"account_created_at": usr.GetCreatedAt().UTC().Format(time.RFC3339),
	}
	if usr.PhoneVerified != nil {
		overrides["phone_verified"] = *usr.PhoneVerified
	}
	if sid, ok := claims["sid"].(string); ok {
		sess, err := auth0API.Session.Read(ctx, sid)
		if err != nil {
			return nil, err
		}
		overrides["session_created_at"] = sess.GetCreatedAt() // go-auth0 keeps it as an RFC 3339 string
	}
	// amr holding "mfa" means a second factor during this login, at auth_time.
	amr, _ := claims["amr"].([]any)
	if authTime, ok := claims["auth_time"].(float64); ok && slices.Contains(amr, any("mfa")) {
		age := max(0, time.Now().Unix()-int64(authTime))
		overrides["mfa"] = map[string]any{"present": true, "methods": []string{"mfa"}, "age_seconds": age}
	}
	authContext, err := noresponse.FromAuth0Claims(claims, overrides)
	if err != nil {
		return nil, err
	}

	// 3. NoResponse assesses the account creation. The second argument is the
	// Idempotency-Key, which the SDK reuses across its own retries.
	res, err := nr.Assessments.Create(ctx, noresponse.NewIdempotencyKey(), components.AssessmentRequest{
		AccountID:   tok.Subject,
		Identifiers: components.AssessmentRequestIdentifiers{Email: usr.Email, IP: noresponse.Pointer(ip)},
		Flow:        components.FlowAccountCreation,
		AuthContext: authContext,
	})
	if err != nil {
		return nil, err
	}
	return &res.Result, nil
}
```

`ip` is the public address of the client. Behind a load balancer, read it from the header that the load balancer sets, such as the last address that it appended to `X-Forwarded-For`. Never read it from a value that the client controls.

The Auth0 user id is the `account_id`, unchanged. `auth0|64abc0de1234567890abcdef` is a valid account id, and the Management API takes it when you act on the result later.

The flow is account creation, so NoResponse waits up to 3000 ms for its full set of sources. To wait only for the sources that answer within 500 ms, send `depth: "quick"`. The other sources still finish later, and their results reach you through the webhook.

> [!WARNING]
>
> **Verified claims, never the token**
>
> The adapter takes the claims that verification gave you. It never takes the token string. It sets `verified: true` and `transport: "backend"` to say that your backend did the verification. The API refuses an `auth_context` that holds a token.

## Act on the answer

The answer starts with `assessment.account`, the one current risk of the account. Your code acts on `assessment.account.risk.decision`, the decision for the flow that you sent:

* `allow`: let the account in.
* `step_up`: ask for more proof first, such as a second factor.
* `review`: hold the account until your team looks at it.
* `block`: refuse the account. NoResponse answers `block` after you set a block threshold and the score reaches it.
* Any other value: treat it as `review`.

`assessment.account.risk.score` and `assessment.account.risk.reasons` tell you why. `assessment.detail` holds the facts of this assessment, such as its `status`, the sources that did not answer and the summary facts.

* `assessment.account` is absent while the first assessment of the account is still pending. Apply your [fallback](https://docs.noresponse.org/guides/assess.md#choose-your-fallback). Then take the final assessment from the [webhook](https://docs.noresponse.org/guides/webhooks.md) when it arrives.
* If the call fails or times out, apply the same fallback.

[Turn an assessment into a decision](https://docs.noresponse.org/guides/decisions.md) gives a full rule set to copy.

## What the adapter sends

This is the `auth_context` for a new user with a verified email who passed a one-time code a minute ago:

```json title="auth_context for a new user" theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{
  "provider": "auth0",
  "issuer": "https://example.us.auth0.com/",
  "subject": "auth0|64abc0de1234567890abcdef",
  "audience": "kXnL2p8QmR4sT6vW8yZ0aB2cD4eF6gH8",
  "audiences": ["kXnL2p8QmR4sT6vW8yZ0aB2cD4eF6gH8"],
  "session_id": "Yx7Qm2Np4Rs6Tu8Vw0Xy2Za4Bc6De8F",
  "issued_at": "2026-10-21T16:41:50Z",
  "expires_at": "2026-10-22T02:41:50Z",
  "auth_time": "2026-10-21T16:41:49Z",
  "email_verified": true,
  "mfa": { "present": true, "methods": ["mfa"], "age_seconds": 60 },
  "impersonated": false,
  "account_created_at": "2026-10-21T16:41:48Z",
  "session_created_at": "2026-10-21T16:41:49.512Z",
  "verified": true,
  "transport": "backend"
}
```

`audience` is the audience of the ID token, which is the client ID of your application. `audiences` lists every value when `aud` holds more than one.

That context fires four reasons. `account.risk.reasons` lists each one with its weight:

* `auth_new_session` and `auth_new_account` raise the score for any new account.
* `auth_email_verified` and `auth_mfa_recent` lower it. They make this account different from one with nothing verified.

## Late results

A slow source can raise an assessment after you allowed the new account. NoResponse then sends `assessment.updated`. The Auth0 late-result provider in each SDK then acts on the Auth0 user through the Management API. It is `auth0Actions` in TypeScript, `auth0_actions` in Python and `Auth0Actions` in Go.

[Act on late results](https://docs.noresponse.org/guides/late-results.md) has the full webhook handlers for Auth0. This section tells you what the Auth0 actions do on each plan.

The handler needs its own machine-to-machine application, with access to the Management API and these scopes:

| Scope | Used for |
| - | - |
| `update:users` | Set `blocked: true` on the user. |
| `delete:sessions` | `DELETE /api/v2/users/{id}/sessions`. |
| `delete:refresh_tokens` | `DELETE /api/v2/users/{id}/refresh-tokens`. |
| `read:device_credentials`, `delete:device_credentials` | List and delete the refresh tokens of the user one at a time, when the plan does not have the bulk endpoint. |

Auth0 offers the session and refresh-token endpoints only on Enterprise plans. On every other plan, Free included, they answer `403` with the error code `feature_not_enabled`. The handler then falls back:

| Action | With the sessions API | Without the sessions API | `action` in the result |
| - | - | - | - |
| `revoke_sessions` | Deletes every session, then every refresh token. | Blocks the user (`blocked: true`). Then deletes each refresh token through `/api/v2/device-credentials`. | `revoke_sessions` with the sessions API, `ban` without it |
| `ban` | Blocks the user, then deletes every session and refresh token. | Blocks the user. Then deletes each refresh token through `/api/v2/device-credentials`. | `ban` |

Without the sessions API, your code cannot end a login session. An application can use a login session to get new tokens through silent authentication. Blocking the user stops that, because Auth0 refuses the logins and refresh token grants of a blocked user. So on those plans `revoke_sessions` does what `ban` does, and the result reports `ban`. Log `result.action` to see which one ran.

To let a blocked user back in, unblock the user in the Auth0 dashboard. You can also send `PATCH /api/v2/users/{id}` with `{"blocked": false}`.

Both actions are safe to repeat. A second delivery sets `blocked: true` again and finds no refresh tokens left to delete. A 404 for a user or credential that is already gone counts as done.

> [!WARNING]
>
> Any other refusal is not a plan limit. The handler raises `ProviderActionError` with the Auth0 error code in `code` (`errorCode` in PHP), such as `insufficient_scope` when the token does not have a scope. Answer the webhook with a 5xx so that NoResponse delivers it again.

The Management API has rate limits, and the limits of the Free plan are low. Without the sessions API, a revoke makes one call per refresh token. When Auth0 answers `429`, the handler waits and sends the same call again. A `429` means that Auth0 did nothing, so a repeat is safe for a delete too. The handler waits for the first of these that applies:

* The time that `Retry-After` gives.
* Else, until `X-RateLimit-Reset`.
* Else, 250 ms, 500 ms, 1 s and 2 s, in that order.

The handler retries each call at most 4 times. One revoke or ban waits at most 3 seconds in total, so the handler still answers within the 5 seconds that a delivery has. If the next wait goes past that limit, the handler raises `ProviderActionError` with status `429`. Answer with a 5xx, and NoResponse delivers the event again. Revoke and ban are safe to repeat.

## Next steps

> **[Turn an assessment into a decision](https://docs.noresponse.org/guides/decisions.md)**
>
> Rules for `allow`, `step_up`, `review` and `block`.

> **[Pending, retries and idempotency](https://docs.noresponse.org/guides/pending.md)**
>
> What a `pending` answer carries and how retries work.

> **[Pass your auth session](https://docs.noresponse.org/guides/auth-session.md)**
>
> Every `auth_context` field and the six auth reason codes.

> **[Act on late results](https://docs.noresponse.org/guides/late-results.md)**
>
> Revoke sessions or ban when an assessment rises after account creation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.