Authentication on Tawa

The Short Version

  • Routes registered in catalog-info.yaml with auth: required → Janus verifies tokens for you. No SDK needed.
  • Services with custom auth middleware → use verifyTokenJWKS(token). No secret needed.
  • Service-to-service → use BioAuth.fromEnv().getClientCredentialsToken(scopes).
  • NEVER use JWT_SECRET — that was HS256. Bio-ID 0.3.0+ issues RS256 tokens.

How It Works

Bio-ID issues RS256-signed JWTs. The private key lives only in Bio-ID. Consuming services verify using Bio-ID's public JWKS endpoint — no shared secret.

Developer → OAuth flow → Bio-ID → RS256 JWT
JWT → request header → Janus (verifies via JWKS) → your service

When You DON'T Need the SDK

If your service registers routes in catalog-info.yaml with auth: required, Janus verifies the token before proxying. Your handler receives a verified request. Most services never call verifyTokenJWKS() directly.

spec:
  routes:
    - path: /api/my-service/data
      methods: [GET]
      auth: required    # Janus handles verification

When You DO Need the SDK

Only services with custom auth middleware (Next.js edge middleware, standalone API servers that don't register routes in Koko) need to verify tokens themselves.

import { verifyTokenJWKS } from '@insureco/bio'

// In middleware or route handler:
const token = req.headers.authorization?.slice(7)
if (!token) return res.status(401).json({ error: 'Unauthorized' })

const payload = await verifyTokenJWKS(token)
// payload.bioId, payload.orgSlug, payload.roles, payload.email

BIO_ID_URL is auto-injected by the builder. verifyTokenJWKS() reads it automatically.

OAuth Flow (user login)

import { BioAuth } from '@insureco/bio'

// All env vars auto-injected by builder: BIO_CLIENT_ID, BIO_CLIENT_SECRET, BIO_ID_URL
const bio = BioAuth.fromEnv()

// 1. Build authorization URL
const { url, state, codeVerifier } = bio.getAuthorizationUrl({
  redirectUri: `${process.env.APP_URL}/api/auth/callback`,
})
// Store state + codeVerifier in httpOnly cookies, redirect user to url

// 2. Handle callback (must be at /api/auth/callback — builder registers this URI)
const tokens = await bio.exchangeCode(code, codeVerifier, redirectUri)
// Store tokens.access_token and tokens.refresh_token in session

// 3. Refresh when expired
const newTokens = await bio.refreshToken(refreshToken)

Critical: Your callback MUST be at /api/auth/callback. The builder registers this exact path. Any other path fails with "Invalid Redirect URI".

Service-to-Service Auth

const bio = BioAuth.fromEnv()
const { access_token } = await bio.getClientCredentialsToken(['target-service:scope'])

await fetch(`${process.env.TARGET_URL}/api/endpoint`, {
  headers: { Authorization: `Bearer ${access_token}` },
})

Environment Variables (auto-injected)

VariableSourcePurpose
BIO_CLIENT_IDBuilder (auto)Your service's OAuth client ID
BIO_CLIENT_SECRETBuilder (auto)Your service's OAuth client secret
BIO_ID_URLBuilder (auto when bio-id is internalDependency)Bio-ID base URL

What NOT to Do

// ❌ WRONG: HS256 with shared secret
import { verifyToken } from '@insureco/bio'
const payload = verifyToken(token, process.env.JWT_SECRET)  // throws on RS256 tokens

// ❌ WRONG: JWT_SECRET env var — not used with RS256
JWT_SECRET=some-secret-value

// ❌ WRONG: BIO_ID_BASE_URL — renamed to BIO_ID_URL
BIO_ID_BASE_URL=https://bio.tawa.insureco.io

// ✅ CORRECT
import { verifyTokenJWKS } from '@insureco/bio'
const payload = await verifyTokenJWKS(token)

Last updated: February 28, 2026