OAuth Integration
Every service deployed on Tawa gets an OAuth client automatically. Use the @insureco/bio SDK for the simplest integration — it handles PKCE, token exchange, refresh, and userinfo with zero config.
Quick Start with SDK
Install the SDK and initialize it from auto-injected environment variables:
npm install @insureco/bioimport { BioAuth } from '@insureco/bio'
// Reads BIO_CLIENT_ID, BIO_CLIENT_SECRET, BIO_ID_URL from env
const bio = BioAuth.fromEnv()That's it. The builder injects BIO_CLIENT_ID and BIO_CLIENT_SECRET automatically on every deploy. The SDK defaults to https://bio.tawa.insureco.io as the issuer.
Login Route
Build an authorization URL with PKCE and redirect the user to Bio-ID. The SDK generates the PKCE challenge and state automatically.
Next.js (App Router)
// app/api/auth/login/route.ts
import { cookies } from 'next/headers'
import { BioAuth } from '@insureco/bio'
export async function GET() {
const bio = BioAuth.fromEnv()
const { url, state, codeVerifier } = bio.getAuthorizationUrl({
redirectUri: `${process.env.APP_URL}/api/auth/callback`,
})
const cookieStore = await cookies()
cookieStore.set('oauth_state', state, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600,
})
cookieStore.set('oauth_code_verifier', codeVerifier, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600,
})
return Response.redirect(url)
}Express
import { BioAuth } from '@insureco/bio'
const bio = BioAuth.fromEnv()
app.get('/api/auth/login', (req, res) => {
const { url, state, codeVerifier } = bio.getAuthorizationUrl({
redirectUri: `${process.env.APP_URL}/api/auth/callback`,
})
res.cookie('oauth_state', state, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600000,
})
res.cookie('oauth_code_verifier', codeVerifier, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600000,
})
res.redirect(url)
})Callback Route
Your callback must be at /api/auth/callback — the builder registers this path automatically.
/api/auth/callback. Using /api/auth/bio-id/callback or any other path will fail with “Invalid Redirect URI”.// app/api/auth/callback/route.ts
import { cookies } from 'next/headers'
import { BioAuth } from '@insureco/bio'
export async function GET(request: Request) {
const bio = BioAuth.fromEnv()
const cookieStore = await cookies()
const url = new URL(request.url)
const code = url.searchParams.get('code')
const state = url.searchParams.get('state')
const error = url.searchParams.get('error')
if (error) return Response.redirect(`${process.env.APP_URL}/?error=${error}`)
if (!code || !state) return Response.redirect(`${process.env.APP_URL}/?error=missing_params`)
// Validate CSRF state
const storedState = cookieStore.get('oauth_state')?.value
if (state !== storedState) {
return Response.redirect(`${process.env.APP_URL}/?error=invalid_state`)
}
const codeVerifier = cookieStore.get('oauth_code_verifier')?.value
if (!codeVerifier) {
return Response.redirect(`${process.env.APP_URL}/?error=missing_verifier`)
}
// Exchange code for tokens
const tokens = await bio.exchangeCode(
code, codeVerifier, `${process.env.APP_URL}/api/auth/callback`,
)
// Get user profile
const user = await bio.getUserInfo(tokens.access_token)
// Store tokens in secure cookies
cookieStore.set('access_token', tokens.access_token, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: tokens.expires_in,
})
cookieStore.set('refresh_token', tokens.refresh_token!, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60 * 60 * 24 * 30,
})
// Clean up OAuth cookies
cookieStore.delete('oauth_state')
cookieStore.delete('oauth_code_verifier')
return Response.redirect(`${process.env.APP_URL}/dashboard`)
}Get Current User
Check if the user is logged in and auto-refresh expired tokens:
import { BioAuth, isTokenExpired } from '@insureco/bio'
import { cookies } from 'next/headers'
export async function getCurrentUser() {
const bio = BioAuth.fromEnv()
const cookieStore = await cookies()
let accessToken = cookieStore.get('access_token')?.value
const refreshToken = cookieStore.get('refresh_token')?.value
if (!accessToken && !refreshToken) return null
// Auto-refresh if expired
if ((!accessToken || isTokenExpired(accessToken)) && refreshToken) {
try {
const tokens = await bio.refreshToken(refreshToken)
accessToken = tokens.access_token
cookieStore.set('access_token', tokens.access_token, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: tokens.expires_in,
})
if (tokens.refresh_token) {
cookieStore.set('refresh_token', tokens.refresh_token, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60 * 60 * 24 * 30,
})
}
} catch {
cookieStore.delete('access_token')
cookieStore.delete('refresh_token')
return null
}
}
if (!accessToken) return null
return bio.getUserInfo(accessToken)
}Logout
Revoke the refresh token and clear cookies:
// app/api/auth/logout/route.ts
import { cookies } from 'next/headers'
import { BioAuth } from '@insureco/bio'
export async function GET() {
const bio = BioAuth.fromEnv()
const cookieStore = await cookies()
const refreshToken = cookieStore.get('refresh_token')?.value
if (refreshToken) {
await bio.revokeToken(refreshToken, 'refresh_token').catch(() => {})
}
cookieStore.delete('access_token')
cookieStore.delete('refresh_token')
return Response.redirect(`${process.env.APP_URL}/`)
}Verify Tokens (RS256)
Bio-ID tokens are RS256-signed JWTs. Use verifyTokenJWKS() to verify them locally using Bio-ID's public JWKS endpoint — no shared secret required.
import { verifyTokenJWKS } from '@insureco/bio'
// In API middleware or a route handler:
const token = req.headers.authorization?.slice(7) // strip "Bearer "
const payload = await verifyTokenJWKS(token)
console.log(payload.bioId) // "BIO-XXXXXXXX"
console.log(payload.orgSlug) // "my-org"
console.log(payload.roles) // ["admin", ...]The JWKS is cached in-process for 24 hours and auto-refreshed on key rotation. No configuration needed — it defaults to https://bio.tawa.insureco.io/.well-known/jwks.json.
auth: required, Janus verifies the token before proxying the request. Only services with their own auth middleware (Next.js edge middleware, standalone API servers) need to call verifyTokenJWKS() directly.Client Credentials (Service-to-Service)
For backend services that need to call APIs without a user context:
const bio = BioAuth.fromEnv()
const tokens = await bio.getClientCredentialsToken(['service:read'])
// tokens.access_token identifies your service, not a userClient credentials tokens have a 1-hour lifetime and do not include a refresh token.
Auto-Provisioning
When you run tawa deploy, the builder automatically:
- Creates an OAuth client in Bio-ID (if it doesn't already exist)
- Registers the redirect URI based on naming conventions
- Injects
BIO_CLIENT_IDandBIO_CLIENT_SECRETas environment variables
| Component | Format | Example |
|---|---|---|
| OAuth Client ID | {service}-{environment} | ppay-board-sandbox |
| Redirect URI (sandbox) | https://{service}.sandbox.tawa.insureco.io/api/auth/callback | |
| Redirect URI (prod) | https://{service}.tawa.insureco.io/api/auth/callback |
Token Lifetimes
| Token | Lifetime | Notes |
|---|---|---|
| Access token | 1 hour | JWT (RS256), carries user claims |
| Refresh token | 30 days | Rotated on each use |
| Client credentials | 1 hour | No refresh token issued |
| Authorization code | 10 minutes | Single use |
Local Development
For local development, pull your deployed credentials:
tawa config pullThis writes all config and secrets to .env.local. Or set them manually:
# .env.local
BIO_CLIENT_ID=your-dev-client-id
BIO_CLIENT_SECRET=your-dev-client-secret
BIO_ID_URL=https://bio.tawa.insureco.io
APP_URL=http://localhost:3000Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| Invalid Redirect URI | Callback not at /api/auth/callback | Use the exact path /api/auth/callback |
| Invalid State | State mismatch between request and callback | Verify cookies are set and read correctly |
client_id=undefined | OAuth provisioning was skipped | Ensure catalog-info.yaml exists and redeploy |
| Token Expired | Access token expired (1 hour lifetime) | Implement auto-refresh with isTokenExpired() |
| Invalid Code Verifier | PKCE verifier doesn't match challenge | Ensure codeVerifier is stored in cookie during login |
Related
- Getting Started — deploy your first service with OAuth auto-configured
- catalog-info.yaml Reference — full service configuration options
- Environment Variables — how secrets are managed and injected