@everystack/auth
v0.2.1
Published
JWT authentication handlers, password hashing, and React auth context
Readme
@everystack/auth
JWT auth for Expo apps. Zero runtime dependencies. SQL-compatible. Edge-verifiable.
Why
Most JWT libraries (jose, jsonwebtoken, ...) are large dependencies that:
- Don't run in CloudFront Functions (no
Buffer,TextEncoder,crypto.subtle) - Aren't byte-compatible with a Postgres-only implementation
- Pull in WebCrypto polyfills you don't need on Node
This package provides one HS256 envelope that runs in three places — Node, Postgres, and CloudFront Functions — and produces byte-identical tokens in all three.
Three Deployment Modes
| Mode | Mint tokens | Verify tokens | Use when |
|------|-------------|---------------|----------|
| JS-only | Lambda (Node) | Lambda + CFF (edge) | Standard Expo + SST app |
| SQL-only | Postgres (jwt_sign) | Postgres (jwt_verify) | Pure PostgREST, no Lambda |
| Hybrid | Either | Either | DB issues long-lived refresh, edge verifies short-lived access |
All three produce the same wire format: header.payload.signature, base64url, no padding, HS256.
Install
pnpm add @everystack/authZero runtime dependencies. drizzle-orm and react are optional peer deps for the schema and client exports.
Entry Points
| Export | Purpose |
|--------|---------|
| @everystack/auth | High-level handlers: createAuthHandlers, verifyToken, OAuth providers |
| @everystack/auth/jwt | Low-level createJwt({ secret, hmac }) — runtime-agnostic envelope |
| @everystack/auth/jwt/node | hmacNode — HMAC backed by node:crypto |
| @everystack/auth/jwt/web | hmacWeb — HMAC backed by crypto.subtle |
| @everystack/auth/jwt/sql | SQL helpers for the Postgres twin (jwt_sign, jwt_verify) |
| @everystack/auth/oauth | verifyOidcToken (RS256+JWKS), Google, Apple |
| @everystack/auth/cff | generateCffVerifier() — emits a verifier as a string for CloudFront Functions |
| @everystack/auth/client | React AuthProvider, useAuth |
| @everystack/auth/api | API client wired with auto-refresh |
| @everystack/auth/schema | Drizzle schema for refresh_tokens, oauth_accounts |
The Envelope
Library owns:
{"alg":"HS256","typ":"JWT"}header (literal, not negotiated)- base64url encoding (RFC 4648 §5, no padding)
iat/expmath, configurable TTL and clock skew- Optional
jti - Constant-time signature compare
- Algorithm pinning (rejects
alg=none,alg=HS512, RSA confusion)
App owns:
- Claim shape (
sub/user_id/ whatever your DB uses) - Role accessor (where to read role from claims)
- Subject accessor
import { createJwt } from '@everystack/auth/jwt';
import { hmacNode } from '@everystack/auth/jwt/node';
const jwt = createJwt({
secret: process.env.JWT_SECRET!,
hmac: hmacNode,
ttlSec: 3600,
clockSkewSec: 30,
includeJti: true,
// Optional accessors so the verifier knows where to look in your custom claim shape:
getSubject: (c) => c.user_id ?? c.sub,
getRole: (c) => c.role,
getExpiry: (c) => c.exp,
});
const token = await jwt.sign({ user_id: 'u_123', role: 'admin', email: '[email protected]' });
const claims = await jwt.verify(token); // null if invalid/expiredHigh-Level Handlers
import { createAuthHandlers, createAppleProvider, createGoogleProvider } from '@everystack/auth';
import { refreshTokens, oauthAccounts } from '@everystack/auth/schema';
import { createDb, getJwtSecret } from '@everystack/server/db';
import * as schema from '../db/schema';
const { db } = createDb(schema);
const auth = createAuthHandlers(
db,
{ ...schema, refreshTokens, oauthAccounts },
getJwtSecret(),
{
oauth: {
providers: [
createAppleProvider({ clientId: 'com.yourapp.id' }),
createGoogleProvider({ clientId: 'your-client-id.apps.googleusercontent.com' }),
],
},
}
);| Handler | Method | Body | Returns |
|---------|--------|------|---------|
| signup | POST | { email, password, username, displayName } | { token, refreshToken, user } |
| signin | POST | { email, password } | { token, refreshToken, user } |
| refresh | POST | { refreshToken } | { token, refreshToken } |
| oauth | POST | { provider, token } | { token, refreshToken, user } |
| verifyToken | — | token string | TokenPayload \| null |
| revokeRefreshTokens | — | userId | void |
Defaults: HS256, access TTL 1h, refresh TTL 7d.
OAuth (RS256 + JWKS)
import { verifyOidcToken, createJwksCache } from '@everystack/auth/oauth';
const jwks = createJwksCache({
fetcher: () => fetch('https://www.googleapis.com/oauth2/v3/certs').then(r => r.json()),
ttlMs: 3600_000,
});
const claims = await verifyOidcToken(idToken, {
issuer: ['https://accounts.google.com', 'accounts.google.com'],
audience: process.env.GOOGLE_CLIENT_ID!,
jwks,
});The jwks cache deduplicates inflight fetches and force-refreshes when an unknown kid is seen. Algorithm is pinned to RS256.
Built-in providers (createGoogleProvider, createAppleProvider) wrap the above with the right issuer/JWKS URI and map provider claims to a uniform OAuthUserInfo.
Edge Verification (CloudFront Functions)
CloudFront Functions run a strict ES2019 subset — no Buffer, no TextEncoder, no atob/btoa, no crypto.subtle. Only crypto.createHmac('sha256').digest('base64') is available.
generateCffVerifier() emits a self-contained verifier as a string you can embed in your CFF source:
import { generateCffVerifier } from '@everystack/auth/cff';
const source = generateCffVerifier({
functionName: 'verifyJwt',
clockSkewSec: 30,
});
// `source` is portable JS — concatenate into your CFF function file.The emitted verifyJwt(token, secret) returns { payload } or throws. It pins HS256, requires typ:"JWT", requires numeric exp, and applies clock skew.
For SST users, @everystack/server/cdn provides a higher-level helper:
import { createAuthFunction } from '@everystack/server/cdn';
const auth = createAuthFunction({
secret: $output(secret.value),
requireAuth: ['/api/', '!/api/auth/', '!/api/health', '!/api/updates'],
});
new sst.aws.Router('Router', {
edge: { viewerRequest: { injection: auth.injection } },
});The injection runs at the edge before any Lambda hop. It returns 401/403 directly when auth fails — no cold start, no API Gateway round trip.
Edge revocation tradeoff
Edge verification trusts the signature; it cannot consult a revocation list without a network call (which defeats the point). Mitigations:
- Keep access tokens short (≤ 15 min recommended for edge-verified setups)
- Keep refresh tokens long-lived in DB with revocation
- Sign-out / password change → revoke refresh tokens, access tokens expire on their own
SQL Twin
@everystack/auth/jwt/sql ships pgcrypto-based functions that produce byte-identical tokens to the JS path:
jwt_sign(claims jsonb, secret text)→textjwt_verify(token text, secret text)→jsonb
Use this if you want PostgREST to mint tokens directly from a sign_in SQL function, or to verify tokens inside RLS policies. Conformance tests guarantee byte-identical output for the same (claims, secret) pair.
Client
import { AuthProvider, useAuth } from '@everystack/auth/client';
function App() {
return <AuthProvider><YourApp /></AuthProvider>;
}
function Login() {
const { signIn, signUp, signOut, user, isLoading } = useAuth();
// ...
}storage exposes getToken(), getRefresh(), getUser(), save(), clear(). Keys: everystack-token, everystack-refresh, everystack-user.
API Client (@everystack/auth/api)
Pre-configured @everystack/api/client instance:
- Auto-injects
Authorization: Bearer ... - On 401 → calls
/api/auth/refresh, deduplicates concurrent refreshes, retries original request rpc<T>(name, body?)for/api/rpc/*calls
Password Hashing
hashPassword(password)— bcrypt cost 12 (bcryptjs, pure JS). Throws on empty input or >72 UTF-8 bytes (no silent truncation).verifyPassword(password, stored)— accepts$2a$/$2b$/$2y$prefixes; returnsfalse(never throws) on malformed input.- SQL twin: byte-compatible with pgcrypto
crypt(p, gen_salt('bf', 12)).
Types
interface TokenPayload {
sub: string;
email: string;
role: string;
// ...your custom claims
}Peer Dependencies
drizzle-orm(optional) — only for@everystack/auth/schemaand the high-level handlersreact(optional) — only for@everystack/auth/client@everystack/api(optional) — only for@everystack/auth/api
No JWT library dependency.
Part of everystack — a self-hosted application stack for Expo apps on AWS.
License
MIT
