Skip to main content
Verify a non-custodial wallet user on your backend by accepting an ID token from the client and checking its ES256 signature and required claims against the configured token issuer.
Start with the quickstart for your SDK first: TypeScript, React Native, Swift, or Kotlin.
Configure OMS_TOKEN_ISSUER as the JWT issuer your backend accepts, and configure OMS_PROJECT_ID as the expected audience. These are backend verifier settings, separate from SDK constructor inputs. Reject tokens whose issuer or audience does not satisfy the validation rules below.

1. Send the ID token to your backend

After the user authenticates in the app, request an ID token for the active wallet. Send only the returned idToken string to your backend over HTTPS.
import { OMSClient } from '@0xsequence/typescript-sdk'

const oms = new OMSClient({
  publishableKey: 'YOUR_PUBLISHABLE_KEY',
})

await oms.wallet.startEmailAuth({ email: 'user@example.com' })
await oms.wallet.completeEmailAuth({ code: '123456' })

const idToken = await oms.wallet.getIdToken({
  ttlSeconds: 300,
})

await fetch('/api/wallet-session', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ idToken }),
})
Configure these backend environment variables:
VariableValue
OMS_TOKEN_ISSUERAccepted JWT iss value. The verifier also uses this as the JWKS base URL.
OMS_PROJECT_IDExpected JWT audience for wallet ID tokens.

2. Verify the token signature and claims

Fetch keys from the accepted issuer’s JWKS endpoint, and use a JWT library to verify the token. The library should select the signing key from the JWT header kid. The JWKS endpoint is:
{OMS_TOKEN_ISSUER}/.well-known/jwks.json
Validate:
FieldExpected value
JWT header algES256
issMatches OMS_TOKEN_ISSUER
audEquals OMS_PROJECT_ID or is an array containing OMS_PROJECT_ID
expIs not in the past
subNon-empty wallet ID
wallet_addressEVM address for the wallet
wallet_typeExpected wallet type, such as ethereum
Install jose in the backend project that verifies the token:
pnpm add jose
import {
  createLocalJWKSet,
  decodeJwt,
  errors,
  jwtVerify,
  type JSONWebKeySet,
  type JWTPayload,
} from 'jose'

function requiredEnv(name: string): string {
  const value = process.env[name]

  if (!value) {
    throw new Error(`Missing ${name}`)
  }

  return value
}

const EXPECTED_ISSUER = requiredEnv('OMS_TOKEN_ISSUER')
const EXPECTED_AUDIENCE = requiredEnv('OMS_PROJECT_ID')
const JWKS_CACHE_TTL_MS = 60 * 60 * 1000
const EVM_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/

const jwksByIssuer = new Map<string, {
  jwks: ReturnType<typeof createLocalJWKSet>
  fetchedAt: number
}>()

function getTokenIssuer(idToken: string): string {
  const { iss } = decodeJwt(idToken)

  if (iss !== EXPECTED_ISSUER) {
    throw new Error('Unexpected token issuer')
  }

  return iss
}

async function getJwksForIssuer(
  issuer: string,
  options: { refresh?: boolean } = {},
) {
  const cached = jwksByIssuer.get(issuer)

  if (
    cached &&
    !options.refresh &&
    Date.now() - cached.fetchedAt < JWKS_CACHE_TTL_MS
  ) {
    return cached.jwks
  }

  const response = await fetch(new URL('/.well-known/jwks.json', issuer))

  if (!response.ok) {
    throw new Error('Failed to fetch issuer JWKS')
  }

  const jwksResponse = await response.json() as JSONWebKeySet
  const jwks = createLocalJWKSet(jwksResponse)
  jwksByIssuer.set(issuer, { jwks, fetchedAt: Date.now() })
  return jwks
}

type VerifiedWalletUser = {
  walletAddress: string
  walletType: 'ethereum'
  walletId: string
  email?: string
}

export async function verifyWalletIdToken(
  idToken: string,
): Promise<VerifiedWalletUser> {
  const issuer = getTokenIssuer(idToken)

  async function verifyWithJwks(refresh = false) {
    return jwtVerify(idToken, await getJwksForIssuer(issuer, { refresh }), {
      issuer,
      audience: EXPECTED_AUDIENCE,
      algorithms: ['ES256'],
    })
  }

  let payload: JWTPayload

  try {
    const verified = await verifyWithJwks()
    payload = verified.payload
  } catch (error) {
    if (!(error instanceof errors.JWKSNoMatchingKey)) {
      throw error
    }

    const verified = await verifyWithJwks(true)
    payload = verified.payload
  }

  if (payload.wallet_type !== 'ethereum') {
    throw new Error('Unexpected wallet_type claim')
  }

  if (
    typeof payload.wallet_address !== 'string' ||
    !EVM_ADDRESS_PATTERN.test(payload.wallet_address)
  ) {
    throw new Error('Invalid wallet_address claim')
  }

  if (typeof payload.sub !== 'string' || payload.sub.length === 0) {
    throw new Error('Missing sub claim')
  }

  return {
    walletAddress: payload.wallet_address,
    walletType: payload.wallet_type,
    walletId: payload.sub,
    email: typeof payload.email === 'string' ? payload.email : undefined,
  }
}

3. Use the wallet claims

After verification succeeds, use only the OMS Wallet identity claims required for the current request. Treat custom claims as client-provided context unless your backend controls their values.
ClaimDescription
wallet_addressUser’s EVM wallet address.
wallet_typeWallet type. This is always ethereum.
subWallet subject identifier. Use wallet_address for the EVM address.
emailUser’s email address, when available.
app.post('/api/wallet-session', async (req, res) => {
  const idToken = req.body.idToken

  if (typeof idToken !== 'string') {
    res.status(400).json({ error: 'Missing token' })
    return
  }

  try {
    const user = await verifyWalletIdToken(idToken)

    res.json({
      walletAddress: user.walletAddress,
      walletId: user.walletId,
      email: user.email ?? null,
    })
  } catch {
    res.status(401).json({ error: 'Invalid wallet token' })
  }
})