> ## 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 OMS Wallet ID tokens on your backend against the configured token issuer JWKS.

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.

<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).
</Note>

<Warning>
  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.
</Warning>

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

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    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 }),
    })
    ```
  </Tab>

  <Tab title="React Native">
    ```typescript theme={null}
    await oms.wallet.startEmailAuth('user@example.com')

    const auth = await oms.wallet.completeEmailAuth({ code: '123456' })

    if (auth.type !== 'walletSelected') {
      throw new Error('Select or create a wallet before requesting an ID token')
    }

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

    await fetch('https://api.example.com/wallet-session', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ idToken }),
    })
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={null}
    try await oms.wallet.startEmailAuth(email: "user@example.com")
    try await oms.wallet.completeEmailAuth(code: "123456")

    let idToken = try await oms.wallet.getIdToken(ttlSeconds: 300)

    var request = URLRequest(url: URL(string: "https://api.example.com/wallet-session")!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "content-type")
    request.httpBody = try JSONEncoder().encode(["idToken": idToken])

    _ = try await URLSession.shared.data(for: request)
    ```
  </Tab>

  <Tab title="Kotlin">
    The Kotlin example uses OkHttp and `kotlinx.serialization` to send the token. Use your app's existing HTTP and JSON clients if it already has them.

    ```kotlin theme={null}
    import com.omsclient.kotlin_sdk.wallet.CompleteAuthResult
    import kotlinx.serialization.encodeToString
    import kotlinx.serialization.json.Json
    import kotlinx.serialization.json.buildJsonObject
    import kotlinx.serialization.json.put
    import okhttp3.MediaType.Companion.toMediaType
    import okhttp3.OkHttpClient
    import okhttp3.Request
    import okhttp3.RequestBody.Companion.toRequestBody

    val okHttpClient = OkHttpClient()

    client.wallet.startEmailAuth("user@example.com")
    val auth = client.wallet.completeEmailAuth("123456")
    check(auth is CompleteAuthResult.WalletSelected)

    val idToken = client.wallet.getIdToken(ttlSeconds = 300u)
    val body = Json.encodeToString(
        buildJsonObject {
            put("idToken", idToken)
        },
    )

    val request = Request.Builder()
        .url("https://api.example.com/wallet-session")
        .post(body.toRequestBody("application/json".toMediaType()))
        .build()

    okHttpClient.newCall(request).execute().use { response ->
        check(response.isSuccessful)
    }
    ```
  </Tab>
</Tabs>

Configure these backend environment variables:

| Variable           | Value                                                                       |
| ------------------ | --------------------------------------------------------------------------- |
| `OMS_TOKEN_ISSUER` | Accepted JWT `iss` value. The verifier also uses this as the JWKS base URL. |
| `OMS_PROJECT_ID`   | Expected 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:

```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
```

<Tabs>
  <Tab title="Node.js">
    ```typescript 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'],
        })
      }

      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,
      }
    }
    ```
  </Tab>
</Tabs>

## 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. This is always `ethereum`.                              |
| `sub`            | Wallet subject identifier. Use `wallet_address` for the EVM address. |
| `email`          | User's email address, when available.                                |

<Tabs>
  <Tab title="Express">
    ```typescript 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' })
      }
    })
    ```
  </Tab>
</Tabs>
