SAIKAT
Hire me
Open source@aetherlabs.agency/oauth

@aetherlabs.agency/oauthv0.1.4

Sign in with Aether Labs SSO — OAuth 2.0 + OIDC for your app, with the callback route scaffolded on install.

748 / monthUNLICENSED licenseUpdated September 29, 2026

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_token verified against the issuer's published JWKS, using only node: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

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 HttpOnly and SameSite=Lax. Strict is 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 Secure when NODE_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.
  • verifyIdToken refuses alg: none and HS256 outright, 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.

↑↓ navigate↵ openesc closeCtrl K toggles this menu