@danainnovations/sonance-gate
v0.3.0
Published
Framework-agnostic Sonance Okta SSO gate for Vercel apps (Routing Middleware)
Readme
@danainnovations/sonance-gate
Sonance Okta SSO for any web app on Vercel — one file, any framework (Next.js, Vite, Astro, SvelteKit, static HTML).
How it behaves
| Environment | Behavior |
|---|---|
| Vercel production | Real Okta sign-in required for every route |
| Vercel preview | Open (protect previews with Vercel Deployment Protection) |
| Local dev | Open — /auth/me returns a fake "Dev User" |
Sessions last 12 hours; re-auth is silent while your Okta session is alive.
Install
npm install @danainnovations/sonance-gateCreate middleware.ts at your project root (NOT inside src/ or app/):
import { createGate } from '@danainnovations/sonance-gate'
export default createGate({
// Paths reachable without sign-in. '/prefix/*' matches the whole subtree;
// a bare '/prefix' must be listed exactly (on its own) to be public.
publicPaths: ['/api/webhook/*'],
})
export const config = {
runtime: 'nodejs',
matcher: ['/((?!_next/static|_next/image|favicon.ico|assets/).*)'],
}Next.js note: on Next.js ≤15 the framework owns root middleware.ts — compose by calling the gate first inside your existing middleware and returning its Response when defined. On Next.js 16+ use proxy.ts for framework logic; middleware.ts belongs to the gate.
Environment variables (Vercel production only)
OKTA_CLIENT_ID, OKTA_CLIENT_SECRET, OKTA_ISSUER — pushed by the Technology team from your app's Okta registration. SESSION_SECRET — any random string ≥32 chars. Nothing is needed locally.
If any are missing in production the gate fails closed with a 500 naming the missing key names.
Showing who's signed in
The gate serves GET /auth/me → { sub, name, email } (a fake Dev User outside production). Framework-free widget:
<div id="whoami"></div>
<script>
fetch('/auth/me').then(r => r.ok ? r.json() : null).then(user => {
if (!user) return
document.getElementById('whoami').innerHTML =
`${user.name} · <a href="/auth/signout">Sign out</a>`
})
</script>Endpoints: /auth/signin (optionally ?returnTo=/path), /auth/signout, /auth/me, /auth/callback (Okta's redirect URI — register https://<your-domain>/auth/callback).
Identity in backend code (v0.2+)
On every request the gate lets through, it injects an x-sonance-user request header (spoof-proof: any client-supplied value is stripped and replaced). Read it with the bundled helper — works in any route handler or server function:
import { getUser } from '@danainnovations/sonance-gate'
export async function POST(req: Request) {
const user = getUser(req) // { sub, name, email } | null
if (!user) return new Response('unauthenticated', { status: 401 })
// ... write rows keyed by user.email, etc.
}Outside production it returns the fake Dev User, so backend code behaves identically in local dev. On publicPaths routes it returns null unless the visitor happens to have a session.
Sign-out clears the app session and forces a fresh Okta credential prompt on the next visit (your Okta org session for other apps stays alive).
Microsoft Teams mode
Apps packaged as Teams tabs authenticate silently — no Okta prompt inside Teams. Enable per app:
export default createGate({
publicPaths: [],
teams: true,
})Production env additionally requires ENTRA_CLIENT_ID and ENTRA_TENANT_ID
(non-secret, pushed by the Technology team alongside the Okta keys). Missing
keys fail closed with a 500 naming them.
How it works: the Teams manifest's contentUrl carries ?inTeams=true. For an
unauthenticated HTML request with that hint (or the __Host-sg_teams marker
cookie), the gate serves a static bootstrap page that calls
authentication.getAuthToken() (Teams JS SDK) and POSTs the Entra ID JWT to
/auth/teams. The gate validates it against Microsoft's JWKS (issuer
https://login.microsoftonline.com/{tenant}/v2.0, audience = client ID or
api://{host}/{clientId}, tenant pinned), then mints the ordinary session.
Server code stays lane-blind: getUser(request) works identically.
?inTeams=trueis a routing hint, never a bypass: outside Teams the bootstrap falls back to the normal Okta redirect. Fail closed always.- Teams-lane cookies use
SameSite=None; Secure; Partitioned(CHIPS) so they survive the cross-site iframe. Browser-lane cookies staySameSite=Lax. - Foreign-tenant users get a friendly "Sonance employees only" page (403).
- Identity: key on
email.subis lane-specific (Okta subject vs Entra object ID) — do not use it as a cross-lane key.
