@atlas/auth#
Authentication primitives and prebuilt auth flow pipes for the Atlas framework.
Exports#
Password Hashing#
hash(password: string): Promise<string>- Hash a password using Argon2id via Bun.passwordverify(password: string, hashed: string): Promise<boolean>- Verify a password against a hash
JWT Tokens (token namespace)#
token.sign(payload, secret, opts?): Promise<string>- Create an HS256 JWTopts.expiresIn- seconds until expiration
token.verify(token, secret): Promise<TokenPayload>- Verify and decode a JWT- Throws on invalid signature, bad format, or expiration
TokenPayload-Record<string, unknown> & { iat?: number; exp?: number }
Session Management#
createMemoryStore({ ttl? }): SessionStore- In-memory session store for dev/testing;ttl(seconds) expires sessions, without it they live untildestroySessionStoretype withcreate,get,destroymethods
Auth Flows (PipeFn factories for @atlas/server)#
signup({ db, table, fields, onSuccess })- Registration pipe;tableis a name string or adefineSchemaobject- Reads body fields, hashes password, inserts into DB
- Calls onSuccess with conn and new user record
login({ db, table, identity, password, onSuccess })- Login pipe- Looks up user by identity field, verifies password
- Calls onSuccess or halts 401
requireAuth({ secret })- JWT guard pipe- Reads Bearer token from Authorization header
- Puts decoded payload into
conn.assigns.auth - Halts 401 if missing or invalid
passwordReset({ db, table, secret, transport, expiresIn? })- Password reset pipe- Generates reset token (JWT signed with
secret, TTLexpiresInseconds, default 3600), calls transport function - Always returns 200 to prevent email enumeration
- Generates reset token (JWT signed with
Usage#
import { hash, verify, token, requireAuth, login, signup } from "@atlas/auth"
// password hashing
const hashed = await hash("mypassword")
const ok = await verify("mypassword", hashed)
// JWT
const jwt = await token.sign({ userId: 1 }, SECRET, { expiresIn: 3600 })
const payload = await token.verify(jwt, SECRET)
// auth guard pipe
const guard = requireAuth({ secret: SECRET })
// login flow
const loginPipe = login({
db,
table: "users",
identity: "email",
password: "password",
onSuccess: async (conn, user) =>
json(conn, 200, { token: await token.sign({ userId: user.id }, SECRET) }),
})
Social Login (@atlas/auth/social)#
Pluggable OAuth-client side of authentication. Seven built-in providers — each
exported as a factory that takes its config and returns a SocialProvider.
| Factory | Provider | Notes |
|---|---|---|
google({ clientId, clientSecret, redirectUri, hostedDomain?, prompt? }) | Google OIDC | id_token-based profile, offline access |
github({ clientId, clientSecret, redirectUri, allowSignup? }) | GitHub OAuth2 | falls back to /user/emails when primary email is private |
apple({ clientId, teamId, keyId, privateKey, redirectUri, responseMode? }) | Sign in with Apple | mints ES256 client_secret JWT per request; default response_mode=form_post |
microsoft({ clientId, clientSecret, redirectUri, tenant?, prompt? }) | Microsoft / Entra ID | tenant-aware (common, consumers, organizations, or a tenant ID) |
facebook({ clientId, clientSecret, redirectUri, apiVersion?, profileFields? }) | Meta Graph API | scope joined with commas, default v19.0 |
twitter({ clientId, clientSecret?, redirectUri, userFields? }) | X (OAuth 2 + PKCE) | confidential clients use Basic auth on /oauth2/token |
tiktok({ clientKey, clientSecret, redirectUri, userFields? }) | TikTok Login Kit | uses client_key (not client_id), scopes are comma-joined, profile id is open_id |
All providers are PKCE-S256, scope-configurable, and return a normalized
SocialProfile:
type SocialProfile = {
provider: string
id: string
email?: string | null
emailVerified?: boolean
name?: string | null
picture?: string | null
username?: string | null
raw: Record<string, unknown>
}
Usage#
import {
socialAuth, google, github, apple, microsoft, facebook, twitter, tiktok,
} from "@atlas/auth/social"
import { get, redirect, putHeader } from "@atlas/server"
import { token } from "@atlas/auth"
const social = socialAuth({
secret: process.env.OAUTH_STATE_SECRET!,
providers: {
google: google({ clientId, clientSecret, redirectUri: `${origin}/auth/google/callback` }),
github: github({ clientId, clientSecret, redirectUri: `${origin}/auth/github/callback` }),
apple: apple({ clientId, teamId, keyId, privateKey, redirectUri: `${origin}/auth/apple/callback` }),
microsoft: microsoft({ clientId, clientSecret, redirectUri: `${origin}/auth/microsoft/callback` }),
facebook: facebook({ clientId, clientSecret, redirectUri: `${origin}/auth/facebook/callback` }),
twitter: twitter({ clientId, redirectUri: `${origin}/auth/twitter/callback` }),
tiktok: tiktok({ clientKey, clientSecret, redirectUri: `${origin}/auth/tiktok/callback` }),
},
})
const routes = [
get("/auth/google", social.start("google", { returnTo: "/welcome" })),
get("/auth/google/callback", social.callback("google", {
onSuccess: async (conn, { profile, returnTo }) => {
// app owns the user table — upsert here
const user = await upsertUserFromProfile(profile)
const jwt = await token.sign({ sub: user.id }, JWT_SECRET, { expiresIn: 3600 })
return redirect(putHeader(conn, "set-cookie", `session=${jwt}; HttpOnly; Path=/`), returnTo ?? "/")
},
})),
// …same for github / apple / microsoft / facebook / twitter
]
Apple uses response_mode=form_post by default, so the callback arrives as a
POST with form-encoded code/state in the body — mount the route with
post(...) and compose parseForm before social.callback("apple", …).
Stateless flow#
State + PKCE verifier live in a signed, HttpOnly, SameSite=lax cookie
(_atlas_oauth_state by default, 10-minute TTL). Override via
socialAuth({ cookie: { name?, path?, secure?, sameSite? } }). Behind a
non-HTTPS dev proxy, set cookie: { secure: false }.
Pure helpers#
If you need to render the consent flow yourself (e.g. show a "Continue with X"
button list), call social.authorize(name, opts?) to get { url, cookie }
without writing to a Conn. social.complete(name, conn) is the inverse —
verifies + exchanges + fetches and returns the normalized result.
Dependencies#
@atlas/db- database access for flows@atlas/server- Conn/PipeFn types for flows and social pipes- Zero external dependencies; uses Bun.password, Web Crypto, and
fetch
Testing#
bun test packages/auth/