Appearance
Authentication Sessions
Reduce biometric prompts by enabling VRF-backed authentication sessions. After initial login, users can interact with your backend APIs without repeated TouchID/FaceID prompts.
Overview
By default, every backend API call requiring authentication triggers a WebAuthn prompt. Sessions enable:
- Single sign-on: One TouchID prompt at login, then session-based auth for subsequent requests
- Zero-gas verification: Backend verifies VRF proofs via contract VIEW calls (no transactions)
- Freshness anchored on-chain: VRF challenges tied to recent NEAR block data
Enabling Sessions
Client Configuration
typescript
const result = await passkeyManager.loginPasskey('alice.testnet', {
session: {
kind: 'jwt', // or 'cookie'
relayUrl: 'https://relay.example.com',
route: '/verify-authentication-response' // optional, defaults to this
}
})Session modes:
jwt: Relay returns JWT token, client includes inAuthorization: Bearer <token>headerscookie: Relay sets HttpOnly cookie, browser sends automatically
Relay Server Setup
Express:
typescript
import { SessionService } from '@tatchi-xyz/sdk/server'
const sessionService = new SessionService({
jwt: {
signToken: async (payload) => jwt.sign(payload, JWT_SECRET, { expiresIn: '24h' }),
verifyToken: async (token) => jwt.verify(token, JWT_SECRET)
}
})
app.use('/', createRelayRouter(authService, {
sessionService,
healthz: true
}))Cloudflare Workers:
typescript
const sessionService = new SessionService({
jwt: {
signToken: async (payload) => /* use Workers crypto or KV */,
verifyToken: async (token) => /* verify with Workers crypto */
}
})
export default {
async fetch(request, env, ctx) {
return createCloudflareRouter(authService, {
sessionService,
expectedOrigins: env.EXPECTED_ORIGIN?.split(',')
})(request, env, ctx)
}
}How It Works
With Shamir 3-Pass (No Prompt)
When Shamir auto-unlock succeeds:
- SDK unlocks VRF keypair using cached server-encrypted data
- Fetches latest NEAR block height/hash
- Generates VRF challenge anchored to that block
- Collects WebAuthn assertion (single TouchID prompt)
- POSTs to relay
/verify-authentication-response - Relay verifies via contract VIEW call, issues session token
- SDK stores token, uses for subsequent API calls
Fallback (Two Prompts or Deferred)
When Shamir unlock fails:
- First prompt: Unlock VRF keypair using PRF from WebAuthn
- Second prompt (if session enabled): Generate VRF challenge, collect assertion, mint session
Alternative: Omit session at login, defer until first API call needs it (one prompt total, but delayed UX).
Login Flow Comparison
Without sessions:
typescript
// Every API call triggers TouchID
await passkeyManager.loginPasskey('alice.testnet')
await apiCall1() // TouchID prompt
await apiCall2() // TouchID prompt
await apiCall3() // TouchID promptWith sessions:
typescript
// One TouchID at login, then session-based auth
await passkeyManager.loginPasskey('alice.testnet', {
session: { kind: 'jwt', relayUrl: 'https://relay.example.com' }
})
await apiCall1() // No prompt, uses session
await apiCall2() // No prompt, uses session
await apiCall3() // No prompt, uses sessionSecurity
VRF freshness: Challenges include recent block height/hash. Contract VIEW rejects stale inputs (e.g., >10 blocks old).
rpId binding: VRF inputs include the rpId used at registration. Contract verifies it matches allowed origins.
Session expiry: Set short TTLs (1-24 hours). Rotate JWT signing keys regularly.
Cookie security (if using cookies):
typescript
cookie: {
buildSetHeader: (token) => [
`session=${token}`,
'Path=/',
'HttpOnly', // JS can't read
'Secure', // HTTPS only
'SameSite=Lax', // CSRF protection
'Max-Age=86400' // 24 hours
].join('; ')
}Session Lifecycle
typescript
// Login with session
await passkeyManager.loginPasskey('alice.testnet', { session: { kind: 'jwt', relayUrl } })
// SDK stores token internally
// Subsequent signing operations include it automatically
// Logout clears session
await passkeyManager.logout()Troubleshooting
Two prompts at login: Shamir auto-unlock failed. Check relay server is configured correctly and server key hasn't rotated without client refresh.
Session verification fails: VRF challenge expired. Relay accepts challenges based on recent blocks (check contract freshness window).
Cookie not sent: Verify SameSite and Secure flags match your deployment (cross-origin requires SameSite=None; Secure).
JWT invalid: Check JWT_SECRET matches between token issuance and verification. Verify token hasn't expired.
See Also
- Relay Server Deployment - Deploy relay with session support
- VRF Challenges - How VRF-backed auth works
- Registration & Login Progress Events - WebAuthn flows and Shamir 3-pass progress events