Sign in with Aether Labs SSO. Installing the package writes the callback route, the flow behind it, and the environment entries into your app, so an app joins the identity service without anyone hand-rolling an OAuth client.
- Authorization code flow with PKCE (S256 required) and a confidential client
id_tokenverified against the issuer's published JWKS, using onlynode:crypto- Zero runtime dependencies
- Next.js App Router and Express — works under Turbopack and webpack alike
- Idempotent scaffolding that never overwrites a file it did not write
Requires Node 18.17 or newer.
Contents
- Install
- Quick start: Next.js
- Quick start: Express
- Register the OAuth client
- Configuration
- Reading the session
- Using the flow directly
- Single-page apps
- Security
- How it works
- Why it has no dependencies
- Contributing
Install
npm install @aetherlabs.agency/oauth
The postinstall hook detects your framework and writes the routes. If npm
scripts are disabled, or you would rather run it deliberately:
npx aetherlabs-oauth init
Set AETHERLABS_OAUTH_AUTOINIT=0 to switch the automatic pass off entirely.
Then register a client so the redirect URI exists on the identity server, and check your configuration:
npx aetherlabs-oauth register --name "Your Product" --admin-token <token>
npx aetherlabs-oauth doctor
Quick start: Next.js
With the App Router — either app/ or src/app/ — init writes four routes:
app/auth/login/route.ts GET — starts the flow
app/auth/callback/route.ts GET — finishes it (this is your redirect URI)
app/auth/logout/route.ts POST — clears the session, revokes the token
app/auth/session/route.ts GET — the signed-in user as JSON
Each is a one-liner, so the flow itself lives in the package:
export { callback as GET } from '@aetherlabs.agency/oauth/next'
export const dynamic = 'force-dynamic'
Link to /auth/login, optionally with a return path:
<a href="/auth/login?next=/dashboard">Sign in</a>
Only same-origin paths are honoured, so next cannot be used as an open
redirect. In a server component:
import { cookies } from 'next/headers'
import { sessionFromCookieHeader } from '@aetherlabs.agency/oauth'
const session = sessionFromCookieHeader((await cookies()).toString())
if (!session) redirect('/auth/login')
Quick start: Express
init writes src/aetherlabs-auth.ts, ready to mount:
import { Router } from 'express'
import { createAuthRouter } from '@aetherlabs.agency/oauth/express'
app.use('/auth', createAuthRouter({ Router }))
Register the OAuth client
The redirect URI has to exist on the identity service before sign-in works:
npx aetherlabs-oauth register --name "Your Product" \
--admin-token <an admin access token>
It reads the redirect URI init wrote, creates the client, and puts the
generated client_id and client_secret into your env file. The secret is
returned exactly once and is stored hashed — it cannot be read back.
Configuration
| Variable | Meaning |
|---|---|
AETHERLABS_ISSUER |
Identity service. Defaults to https://sso.aetherlabs.agency |
AETHERLABS_CLIENT_ID |
From register |
AETHERLABS_CLIENT_SECRET |
From register. Server-side only |
AETHERLABS_REDIRECT_URI |
Must match the registered redirect URI exactly |
AETHERLABS_SESSION_SECRET |
Signs the session cookie. openssl rand -hex 32 |
AETHERLABS_POST_LOGIN_REDIRECT |
Where to land after sign-in. Defaults to / |
AETHERLABS_ADMIN_TOKEN |
Only used by register |
Anything can be overridden in code by passing options to the handlers or to
resolveConfig(). A whitespace-only value counts as unset, because a
copy-pasted variable often carries a stray non-breaking space.
Reading the session
GET /auth/session returns the profile, never the tokens:
const { user } = await fetch('/auth/session').then(r => r.json())
If you would rather not expose the route, read the cookie directly:
import { decodeSession, parseCookies } from '@aetherlabs.agency/oauth'
const cookies = parseCookies(request.headers.get('cookie'))
const session = decodeSession(cookies['aetherlabs_session'], process.env.AETHERLABS_SESSION_SECRET!)
Using the flow directly
Every step is exported as a plain function if you would rather wire it up yourself:
import {
buildAuthorizeUrl, codeChallengeFor, createCodeVerifier, createState,
completeLogin, exchangeCode, fetchUserInfo, refreshTokens, revokeToken,
sessionCookie, transactionCookie,
} from '@aetherlabs.agency/oauth'
completeLogin performs the exchange and verifies the id_token — the RS256
signature against the JWKS, then iss, aud, exp and nonce. An unverified
id_token is only attacker-supplied JSON, so do not skip it. If you only need to
check a token you already hold, verifyIdToken(token, jwks, { issuer, audience })
is exported on its own.
Single-page apps
A browser-only app cannot use this flow directly: the token endpoint requires
a client secret, and shipping one to a browser gives it away. Put the four routes
on a server — Next.js route handlers count — and let the SPA ask
/auth/session.
Security
- The session cookie is
HttpOnlyandSameSite=Lax.Strictis deliberately not used: it would withhold the cookie on the redirect back from the identity server, breaking the first request after sign-in. - The cookie is
SecurewhenNODE_ENV=production. - It is signed, not encrypted: tamper-proof, but a user can read their own claims out of it. Tokens never cross the wire to the browser.
- PKCE state and the verifier live in a separate, short-lived cookie and are compared in constant time.
verifyIdTokenrefusesalg: noneandHS256outright, and validates the issuer, audience, expiry and nonce.
How it works
/auth/login → state + PKCE verifier sealed in a cookie, browser sent to
/api/oauth/authorize
identity server → authenticates the user, shows consent, redirects back
/auth/callback → state compared, code exchanged, id_token verified,
session cookie set
Access and refresh tokens stay inside the signed session cookie; only the profile
is exposed through /auth/session.
Why it has no dependencies
Verifying an id_token is one RSA-SHA256 check over two base64url segments, and
node:crypto already does it. A package that writes files into your project
should be small enough to read, so it carries no JWT library, no HTTP client and
no framework runtime — next and express are optional peer dependencies, used
only by their adapters.
Contributing
Development setup, test commands, the release process and the npm trusted publisher configuration are in CONTRIBUTING.md.
Licence
Proprietary. All rights reserved — see LICENSE.