/
Contact usSee pricingStart building

    About B2B SaaS Authentication

    Introduction
    Stytch B2B Basics
    Integration Approaches
      Full-stack overview
      Frontend (pre-built UI)
      Frontend (headless)
      Backend
    Next.js
      Routing
      Authentication
      Sessions
    Migrations
      Overview
      Reconciling data models
      Migrating user data
      Additional migration considerations
      Zero-downtime deployment
      Defining external IDs
      Migrating from Stytch Consumer to B2B
      Exporting from Stytch
    Custom Domains
      Overview

    Authentication

    Single Sign On
    • Resources

      • Overview
        External SSO Connections
        Standalone SSO
    • Integration Guides

      • Start here
        Backend integration guide
        Headless integration guide
        Pre-built UI integration guide
    OAuth
    • Resources

      • Overview
        Authentication flows
        Identity providers
        Google One Tap
        Provider setup
    • Integration Guides

      • Start here
        Backend integration
        Headless frontend integration
        Pre-built UI frontend integration
    Connected Apps
      Overview
      Getting started with the SDK
      Client types
      OAuth scopes
    • Integration Guides

      • MCP Authorization Overview
        Integrate with a remote MCP server
        Integrate with AI agents
    • Resources

      • Consent Management
    Sessions
    • Resources

      • Overview
        JWTs vs Session Tokens
        How to use Stytch JWTs
        Custom Claims
    • Integration Guides

      • Start here
        Backend integration
        Frontend integration
    Email OTP
      Overview
    Magic Links
    • Resources

      • Overview
        Email Security Scanner Protections
    • Integration Guides

      • Start here
        Backend integration
        Headless frontend integration
        Pre-built UI frontend integration
    Multi-Factor Authentication
    • Resources

      • Overview
    • Integration Guides

      • Start here
        Backend integration
        Headless frontend integration
        Pre-built UI frontend integration
    Passwords
    • Resources

      • Overview
        Strength policy
    • Integration Guides

      • Pre-built UI frontend integration
    UI components
      Overview
      Implement the Discovery flow
      Implement the Organization flow
    DFP Protected Auth
      Overview
      Setting up DFP Protected Auth
      Handling challenges
    M2M Authentication
      Authenticate an M2M Client
      Rotate client secrets
      Import M2M Clients from Auth0
    Trusted Auth Tokens
      Overview
      Getting Started with External IDPs
      Getting Started with Custom Auth Factors
    Device History
      New Device Notifications

    Authorization & Provisioning

    RBAC
    • Resources

      • Overview
        Stytch Resources & Roles
        Role assignment
    • Integration Guides

      • Start here
        Backend integration
        Headless frontend integration
    SCIM
    • Resources

      • Overview
        Supported actions
    • Integration Guides

      • Using Okta
        Using Microsoft Entra
    Organizations
      Managing org settings
      JIT Provisioning

    Testing

    E2E testing
    Sandbox values
Get support on SlackVisit our developer forum

Contact us

B2B SaaS Authentication

/

Guides

/

Authentication

/

Trusted Auth Tokens

/

Getting Started with Custom Auth Factors

Accepting Custom Auth Factors

The Trusted Auth Tokens feature allows developers to attest end-user identities by exchanging signed JWTs for Stytch sessions. Usually, your existing identity infrastructure will provide a JWT that can be used for this purpose such as an access_token or id_token. Stytch will use the JWKS endpoint hosted by your existing infrastructure to validate these JWTs and provision users automatically as required -- no additional setup is needed.

If your identity infrastructure does not provide a JWT, you can create your own JWT using a private key, as demonstrated below.

Generating a Public-Private Keypair

Use a library such as OpenSSL to generate an RSA public-private keypair. The public key can be added to your Trusted Auth Token profile in the Stytch dashboard here (in the JWKS URL row, click "Use public keys instead"). The private key should be kept secret, and exposed to your application using an environment variable. It is often useful to base64 encode the private key for easy addition to .env files.

# Generate private key
openssl genrsa -out "private_key.pem" 2048
# Extract public key
openssl rsa -in "private_key.pem" -pubout -out "public_key.pem"
# Base64 encode the private key
base64 < ./private_key.pem

Creating and Signing a JWT

Use any compatible JWT library to sign your JWT with your private key. Here is an example using jose, a popular library in the Node ecosystem. Be sure to set the iss and aud claims to match the issuer and audience configured in your Trusted Auth Token Profile. Choose a short expiry for the JWT - 5 minutes is a good default.

Other claims (email, tenant, sub, role_ids, jti, etc... ) should be set according to your previously configured attribute mapping. Remember, an email and a token ID are required, and if you want Stytch to provision organizations in real-time, then organization ID is required as well. All other fields are optional.

import { importPKCS8, SignJWT } from 'jose';
import { randomBytes } from 'node:crypto';

async function createJWT({email, tenant_id, role_assignments, member_id, token_id}) {
  // Expose the private key as an environment variable
  // If you base64 encoded it before, remember to decode it here as well
  const privateKeyString = Buffer.from(process.env.PRIVATE_KEY, 'base64')
    .toString('utf8').trim();
  const privateKey = await importPKCS8(privateKeyString, 'RS256');

  // details to encode in the token
  const claims = {
    // Required - the member's email address
    'email': email,
    // Optional - the external ID of the organization the member is a part of
    // In the Trusted Auth Token Profile attributes, this maps to `external_organization_id`
    'tenant': tenant_id,
    // Optional - RBAC roles to assign to the member
    // In the Trusted Auth Token Profile attributes, this maps to `role_ids`
    'assignments': role_assignments,
  };
  
  // Required - a token ID used to identify the credential
  const tokenID = token_id || randomBytes(30).toString('base64');
  
  const jwt = await new SignJWT(claims)
    .setProtectedHeader({ alg: 'RS256' })
    .setIssuedAt()
    .setSubject(member_id) // sub claim, used as the member's external ID
    .setJti(tokenID)
    .setIssuer('https://auth.example.com') // iss claim
    .setAudience('https://api.example.com') // aud claim
    .setExpirationTime('5 minutes') // token expiration time
    .sign(privateKey);

  return jwt;
};

createJWT({
  email: 'ada.lovelace@example.com',
  tenant: "cust_56789",
  role_assignments: ['editor', 'reader'],
  member_id: 'user_123456'
}).then(jwt => console.log(jwt))

Logging a member in

After you have minted your JWT, call the Attest Session API endpoint to exchange the JWT for a Stytch Session.

const stytch = require('stytch');

const client = new stytch.B2BClient({
  project_id: 'PROJECT_ID',
  secret: 'SECRET',
});

createJWT({
  email: 'ada.lovelace@example.com',
  tenant: "cust_56789",
  role_assignments: ['editor', 'reader'],
  member_id: 'user_123456'
})
  .then(jwt => {
    const params = {
      profile_id: "trusted-auth-token-profile-test-41920359-8bbb-4fe8-8fa3-aaa83f35f02c",
      token: "eyJhb...",
    };
    
    return client.sessions.attest(params)
  })
  .then(resp => {
    console.log(resp)
  })
  .catch(err => {
    console.log(err)
  });

Supporting Custom Auth Factors

Call the Attest Session API endpoint with a session_token or session_jwt to attach the attestation to an existing session instead of creating a new one. It may be useful to pass in a unique token_id to the JWT to tie the custom authentication factor you validated to the session for later reference.

validateCustomFactor()
  .then(validation => createJWT({
    email: 'ada.lovelace@example.com',
    tenant: "cust_56789",
    role_assignments: ['editor', 'reader'],
    member_id: 'user_123456',
    // ID of the validation event - will be passed to the Stytch session as
    // trusted_auth_token_factor.token_id
    token_id: validation.validation_id,
  }))
  .then(jwt => {
    const params = {
      profile_id: "trusted-auth-token-profile-test-41920359-8bbb-4fe8-8fa3-aaa83f35f02c",
      token: "eyJhb...",
      // Pass a session token in to add your custom factor to an existing session
      session_token: "mZAYn5aLEqKUlZ_Ad9U_fWr38GaAQ1oFAhT8ds245v7Q",
    };
    
    return client.sessions.attest(params)
  })
  .then(resp => {
    console.log(resp)
  })
  .catch(err => {
    console.log(err)
  });

Generating a Public-Private Keypair

Creating and Signing a JWT

Logging a member in

Supporting Custom Auth Factors