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

# Backend wallet verification

> Verify an OMS Wallet ID token on your backend against the configured token issuer JWKS.

Verify a non-custodial wallet user on your backend by accepting an OMS Wallet ID token from the client and checking its ES256 signature and required claims against the configured token issuer.

<Warning>
  An OIDC provider ID token is an input to wallet authentication. It is not the OMS Wallet ID token returned by `getIdToken`, and your backend must not verify it with the OMS Wallet issuer keys.
</Warning>

<Note>
  Start with the quickstart for your SDK first: [TypeScript](/wallets/sdk/typescript/quickstart), [React Native](/wallets/sdk/react-native/quickstart), [Swift](/wallets/sdk/swift/quickstart), or [Kotlin](/wallets/sdk/kotlin/quickstart).

  Kotlin snippets call `suspend` SDK APIs; run them from a coroutine.
</Note>

<Warning>
  During backend setup, inspect the `iss` and `aud` claims in an ID token returned by `getIdToken` from your own OMS project. Store those expected values as `OMS_TOKEN_ISSUER` and `OMS_PROJECT_ID`. Do not choose trusted values dynamically from a token received in an application request.
</Warning>

## 1. Send the OMS Wallet ID token to your backend

After the user authenticates in the app, request an ID token for the active wallet and pass it to your app's backend client. That client sends the token to your backend over HTTPS.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const idToken = await omsWallet.wallet.getIdToken({
      ttlSeconds: 300,
    })
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    const idToken = await omsWallet.wallet.getIdToken({
      ttlSeconds: 300,
    })
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={null}
    let idToken = try await omsWallet.wallet.getIdToken(ttlSeconds: 300)
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val idToken = omsWallet.wallet.getIdToken(ttlSeconds = 300u)
    ```
  </Tab>
</Tabs>

Send the returned string to your backend over HTTPS using your application's backend client.

Configure these backend environment variables:

| Variable           | Value                                                                                         |
| ------------------ | --------------------------------------------------------------------------------------------- |
| `OMS_TOKEN_ISSUER` | Exact `iss` value from a trusted setup token. The verifier also uses it as the JWKS base URL. |
| `OMS_PROJECT_ID`   | Project ID in the trusted setup token's `aud` claim.                                          |

## 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:

```text theme={null}
{OMS_TOKEN_ISSUER}/.well-known/jwks.json
```

Validate:

| Field            | Expected value                                                     |
| ---------------- | ------------------------------------------------------------------ |
| JWT header `alg` | `ES256`                                                            |
| `iss`            | Matches `OMS_TOKEN_ISSUER`                                         |
| `aud`            | Equals `OMS_PROJECT_ID` or is an array containing `OMS_PROJECT_ID` |
| `exp`            | Is not in the past                                                 |
| `sub`            | Non-empty wallet ID                                                |
| `wallet_address` | EVM address for the wallet                                         |
| `wallet_type`    | Expected wallet type, such as `ethereum`                           |

Install `jose` in the backend project that verifies the token:

```bash theme={null}
pnpm add jose
```

```typescript Node.js theme={null}
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'],
      requiredClaims: ['exp'],
    })
  }

  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.

| Claim            | Description                                                          |
| ---------------- | -------------------------------------------------------------------- |
| `wallet_address` | User's EVM wallet address.                                           |
| `wallet_type`    | Wallet type. Compare it with the type your application accepts.      |
| `sub`            | Wallet subject identifier. Use `wallet_address` for the EVM address. |
| `email`          | User's email address, when available.                                |

```typescript Express theme={null}
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' })
  }
})
```
