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}` },
})

Enabling OAuth in catalog-info.yaml

To provision BIO_CLIENT_ID and BIO_CLIENT_SECRET for your service, declare auth: mode: in spec:

spec:
  auth:
    mode: sso   # provisions BIO_CLIENT_ID and BIO_CLIENT_SECRET on every deploy
ModeWhat gets injectedUse case
ssoBIO_CLIENT_ID, BIO_CLIENT_SECRET, redirect URI registeredUser-facing OAuth login flows
service-onlyBIO_CLIENT_ID, BIO_CLIENT_SECRETService-to-service client credentials only, no user login
noneNothingDefault — no OAuth client provisioned

IMPORTANT: Do NOT add bio-id to internalDependencies to enable OAuth. That is incorrect. internalDependencies is for resolving internal service URLs — it does not provision OAuth credentials. BIO_ID_URL is always auto-injected as a core platform variable (no declaration needed). Only auth: mode: sso provisions BIO_CLIENT_ID and BIO_CLIENT_SECRET.

Environment Variables (auto-injected)

VariableSourcePurpose
BIO_CLIENT_IDBuilder (when auth: mode: sso or service-only)Your service's OAuth client ID
BIO_CLIENT_SECRETBuilder (when auth: mode: sso or service-only)Your service's OAuth client secret
BIO_ID_URLAlways — core platform varBio-ID base URL, no declaration needed

Token Lifetimes

TokenLifetimeNotes
Access token1 hourRS256 JWT, carries user claims
Refresh token30 daysRotated on each use, stored in Redis
Client credentials1 hourNo refresh token issued
Authorization code10 minutesSingle use

Available Scopes

ScopeDescription
openidRequired — returns ID token with unique identifier
profileUser's name and avatar
emailEmail address and verification status
platform:readRead platform-level resources
platform:writeWrite platform-level resources
namespace:{id}:adminAdmin access to a specific namespace
service:{name}:callCall a specific service's API

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: March 1, 2026