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.
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.
TypeScript
React Native
Swift
Kotlin
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 }),
})
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 }),
})
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)
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.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)
}
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:
{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:
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.
| 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. |
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' })
}
})