Skip to content

Pass your auth session

If signup runs through your auth provider, your backend already knows things that matter: whether the email is verified, how old the session is, whether a second factor was used, whether an admin is acting as the user. auth_context carries those facts into the assessment, and six reason codes can then move the score.

Your backend verifies the session with your auth provider’s SDK, reads the user and session records, and sends what it learned. Each NoResponse SDK has an adapter per auth provider that turns verified claims into auth_context. You add the facts that live on the records as overrides.

browser ──▶ your signup route ──▶ verify the session with your auth provider's SDK
──▶ read the user and session records
──▶ build auth_context with the adapter
──▶ POST /v1/assessments

Verify the session token with Clerk’s backend SDK and its authorised-party check, then fetch the user and the session with the backend API. Clerk’s session token holds sub, sid, exp, the active organisation in o and the factor ages in fva. The adapter maps those. It does not send iat, because Clerk refreshes the token about every minute, so iat is a refresh clock and not the session’s age.

Field Where it comes from
session_created_at Session.createdAt from the backend API. Unix milliseconds, so convert to RFC 3339. The only source of the new-session signal.
account_created_at User.createdAt from the backend API. Unix milliseconds.
email_verified User.primaryEmailAddress.verification.status === "verified". Not read from the token.
phone_verified User.primaryPhoneNumber.verification.status === "verified". Not read from the token.
mfa Mapped from fva[1], the second-factor age in minutes. [7, 1] becomes mfa.age_seconds: 60; [7, -1] sends no mfa.
impersonated Mapped from the act claim.
import { createClerkClient, verifyToken } from "@clerk/backend";
import { fromClerk } from "@noresponse/node";
const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY! });
const claims = await verifyToken(sessionToken, { secretKey: process.env.CLERK_SECRET_KEY!, authorizedParties: ["https://app.example.com"] });
const [user, session] = await Promise.all([clerk.users.getUser(claims.sub), clerk.sessions.getSession(claims.sid)]);
const authContext = fromClerk(claims, {
sessionCreatedAt: new Date(session.createdAt).toISOString(),
accountCreatedAt: new Date(user.createdAt).toISOString(),
emailVerified: user.primaryEmailAddress?.verification?.status === "verified",
phoneVerified: user.primaryPhoneNumber?.verification?.status === "verified",
});

Pass the adapter’s result as auth_context. The body below is what the Clerk adapter produces for Jordan Lee’s signup.

const assessment = await nr.assessments.create(
{
accountId: "usr_8f2k1",
identifiers: { email: "[email protected]", name: "Jordan Lee", ip: "81.2.69.160" },
policyContext: { flow: "signup", region: "GB" },
authContext,
},
{ idempotencyKey: `signup-${signupAttempt.id}` },
);

Six facts can move the score. Each one needs the field in the right column. A fact you leave out is absent, not false, and does not move the score. The reason appears in risk.reasons with its weight when it fires.

Fact Reason code Effect Fires when
Email verified auth_email_verified Lowers the score email_verified is true.
Phone verified auth_phone_verified Lowers the score phone_verified is true.
Recent second factor auth_mfa_recent Lowers the score mfa.present is true and mfa.age_seconds is 86,400 or less.
Impersonated auth_impersonated Raises the score impersonated is true.
New session auth_new_session Raises the score session_created_at is within 15 minutes of the server clock.
New account auth_new_account Raises the score account_created_at is within 7 days of the server clock.

Jordan Lee’s signup above fires auth_email_verified, auth_mfa_recent, auth_new_session and auth_new_account. That is normal for a fresh signup. The weights appear in risk.reasons, so you can see what each fact contributed.

provider, organization_id, tenant_id, roles and scopes are kept for correlation and do not move the score. Your auth provider’s own bot and fraud controls keep working beside NoResponse.

provider, issuer, subject and verified are required. Everything else is optional. All times are RFC 3339 strings, and the adapters write a numeric claim as whole UTC seconds, such as 2026-10-21T16:41:50Z.

Field Meaning
provider clerk, workos, auth0, or a lowercase slug of 2 to 32 characters for any other auth provider.
issuer The verified iss, an HTTPS URL of up to 256 characters.
subject The verified, stable sub. Never an email.
audience, audiences The verified audience your API checked. audiences holds up to 8 values when the token has several.
session_id, token_id The verified sid and jti, when your auth provider issues them.
issued_at, expires_at The token’s iat and exp. They describe the token, not the session’s age.
session_created_at When the session started, from the session record.
auth_time When the person last authenticated. Not a substitute for session_created_at.
account_created_at When the user record was created.
email_verified, phone_verified From a verified ID token, userinfo response or user record.
mfa present, up to 8 methods and age_seconds for the current session.
impersonated true when an admin acts as the user.
organization_id, tenant_id The active organisation or workspace, and the auth provider’s tenant when it differs.
roles, scopes Up to 32 values each, at most 64 characters each. The adapters drop longer values.
assurance_level unknown, low, medium or high, if you map one.
verified true once your backend has checked the signature, issuer, audience and expiry.
transport backend or browser. The adapters set backend.