@tktchurch/auth
Framework-agnostic auth SDK for TKTChurch OAuth/AuthFlow services.
- Default production endpoint:
https://prod-auth.tktchurch.com - Works in browser and Node runtimes (requires
fetch) - Pluggable token storage (
memoryby default) - First-class support for AuthFlow endpoints
Install
npm install @tktchurch/auth
Quick Start
import { createAuthClient } from "@tktchurch/auth";
const auth = createAuthClient({
clientId: "your_client_id",
// Optional:
// clientSecret: "your_client_secret",
// baseUrl: "https://prod-auth.tktchurch.com",
});
const tokens = await auth.token.password({
username: "user@example.com",
password: "your_password",
scope: "openid profile email",
});
console.log(tokens.accessToken);
Primary email verification compatibility
SDK 0.9.9's user.emailVerification.start() / verify() contract requires a
backend exposing POST /api/v1/users/me/email/verify with the
identity:write scope and recent-authentication enforcement. Deploy and
validate that backend before releasing or adopting SDK 0.9.9. A successful
verification revokes the caller's existing sessions and tokens, so clients
must sign in again; the stable user identity remains the OIDC sub, not email.
Authorization Code + PKCE (redirect flow)
The recommended browser/SSR login flow. The SDK generates PKCE, state, and
nonce for you and hands back everything you must persist across the redirect —
no more hand-rolling crypto per app.
1. Start the flow (server route that begins login):
import { createAuthClient } from "@tktchurch/auth";
const auth = createAuthClient({ clientId: process.env.TKT_CLIENT_ID! });
const request = await auth.authorize.createRequest({
redirectUri: "https://app.example.com/callback",
// scope defaults to "openid profile email offline_access"
});
// Persist these in an HttpOnly cookie / server session, keyed by `state`:
// request.state, request.nonce, request.codeVerifier, request.redirectUri
// Then redirect the user agent:
// 302 -> request.url
2. Handle the callback — one call validates state (constant-time),
surfaces server errors, and exchanges the code for tokens:
const tokens = await auth.authorize.handleCallback({
callback: requestUrl, // full callback URL or its query string
expectedState: saved.state, // the state you persisted in step 1
codeVerifier: saved.codeVerifier,
redirectUri: saved.redirectUri,
});
// tokens.accessToken / tokens.refreshToken — persist refresh in HttpOnly cookie;
// keep access token in server memory per request (see examples/).
Security: the PKCE
codeVerifieris a secret — keep it server-side (HttpOnly cookie or session store), never inlocalStorage.stateis verified with a constant-time comparison to defeat CSRF and timing oracles.
Open-redirect protection
Always sanitize any next/returnTo value before redirecting after login:
import { sanitizePostAuthRedirect } from "@tktchurch/auth";
return Response.redirect(sanitizePostAuthRedirect(next, "/dashboard"));
// "//evil.com", "https://evil.com", "javascript:…", CRLF → fallback
Low-level PKCE helpers
If you need the primitives directly:
import {
deriveCodeChallenge,
generateNonce,
generatePkce,
generateState,
} from "@tktchurch/auth";
const { codeVerifier, codeChallenge, codeChallengeMethod } =
await generatePkce();
OIDC discovery
const meta = await auth.oidc.discover(); // /.well-known/openid-configuration
console.log(meta.authorizationEndpoint, meta.tokenEndpoint, meta.jwksUri);
Framework-Agnostic Usage
Server route (recommended)
Use in-memory storage (the default) and persist refresh tokens in HttpOnly
cookies. See examples/ for full PKCE redirect flows.
import { createAuthClient } from "@tktchurch/auth";
const auth = createAuthClient({
clientId: process.env.TKT_CLIENT_ID!,
clientSecret: process.env.TKT_CLIENT_SECRET!,
});
export async function GET() {
const me = await auth.user.me();
return Response.json(me);
}
Do not store OAuth tokens in
localStorageorsessionStorage. The SDK does not ship a browser persistence adapter — use server-side cookies or a platform secure store (Keychain, Keystore). SeeSECURITY.md.
Framework Examples (Best Practices)
Production-oriented examples are available in:
They follow server-first patterns:
- Authorization code + PKCE redirect flow (primary).
clientSecretremains server-only.- Refresh token is stored in
HttpOnlycookie. - PKCE
codeVerifieris stored in a signed HttpOnly session cookie. - Refresh token rotation is performed on every refresh call.
- Logout revokes refresh token and clears cookies.
Nuxt Module (@tktchurch/auth/nuxt)
Register the module in nuxt.config.ts:
export default defineNuxtConfig({
modules: ["@tktchurch/auth/nuxt"],
tktAuth: {
baseUrl: process.env.TKT_AUTH_BASE_URL ?? "https://prod-auth.tktchurch.com",
clientId: process.env.TKT_CLIENT_ID,
clientSecret: process.env.TKT_CLIENT_SECRET,
autoRefresh: false,
refreshCookieName: "tkt_refresh_token",
},
});
Then use provided imports:
const auth = createTktServerAuthClient();
const appAuth = useTktAuth();
const refreshCookieName = useTktRefreshCookieName();
AuthFlow Example
const init = await auth.flow.initAuthentication({
username: "user@example.com",
});
let status = await auth.flow.executeStep({
sessionId: init.sessionId,
stepId: init.currentStep.stepId,
continuationToken: init.continuationToken,
credential: {
username: "user@example.com",
password: "secret",
},
});
while (status.status === "in_progress") {
// Render UI for status.nextStep and collect credentials for that step.
status = await auth.flow.executeStep({
sessionId: status.sessionId,
stepId: status.nextStep.stepId,
continuationToken: init.continuationToken,
credential: {},
});
}
// status.status === "complete"
console.log(status.accessToken);
Token Storage
Memory (default)
import { createAuthClient, createMemoryTokenStorage } from "@tktchurch/auth";
const auth = createAuthClient({
clientId: "client_id",
storage: createMemoryTokenStorage(),
});
Custom TokenStorage
For platform-specific secure backends (Keychain, Keystore, encrypted server
session), implement TokenStorage using the exported validators
(assertValidAuthTokens, parseStoredAuthTokens, serializeAuthTokens). See
SECURITY.md.
Error Handling
import { AuthError, MFARequiredAuthError } from "@tktchurch/auth";
try {
await auth.token.password({
username: "user@example.com",
password: "secret",
});
} catch (error) {
if (error instanceof MFARequiredAuthError) {
console.log("MFA required:", error.mfaMethods);
} else if (error instanceof AuthError) {
console.log(error.code, error.message, error.status);
} else {
throw error;
}
}
Authenticated Fetch
fetchWithAuth adds the bearer token automatically and retries once on 401
using refresh token when possible.
Note:
fetchWithAuthandrequest()are available only in the full SDK (GitHub Packages / localfile:build). The public npmjs consumer build omits generic passthrough helpers.
const response = await auth.fetchWithAuth("/api/v1/users/me");
if (!response.ok) {
// handle response
}
API reference (public consumer build)
The public npmjs package ships OAuth/OIDC client namespaces only. Admin console
modules (clients, roles, auditLogs, …) and generic passthrough helpers are
available in the full SDK from GitHub Packages — see
CONTEXT.md.
| Namespace | Methods | Auth |
|---|---|---|
token |
password, refresh, clientCredentials, authorizationCode, tokenExchange, revoke, introspect |
client credentials / stored tokens |
authorize |
createRequest, parseCallback, handleCallback, consent, consentWithCredentials |
public + server session |
oidc |
userInfo, discover, jwks |
bearer / public |
oauth |
publicClient, discoverAuthorizationServer |
public |
flow |
initAuthentication, executeStep, initRegistration, executeRegistrationStep, initRecovery, executeRecoveryStep, getStatus |
public / step session |
user |
me, updateMe, changePassword, deleteAvatar, verifyPhone, securityOverview, recoveryContacts.*, identities.*, deleteMe |
bearer |
sessions |
list, current, get, update, revoke, revokeAll, revokeAllDevices |
bearer |
webauthn |
registerBegin/Finish, authenticateBegin/Finish, credentials, removeCredential, renameCredential, setPrimaryCredential |
bearer / public begin |
passwordReset |
request, validate, complete |
public |
mfa |
setup, verify, devices, removeDevice, regenerateBackupCodes, disable |
bearer |
maintenance |
status |
public |
Integrators must pass an explicit baseUrl in
createAuthClient({ baseUrl, clientId, … }). The public consumer build does not
export DEFAULT_BASE_URL.
Internal admin SDK (GitHub Packages only)
The full build adds consents, admin maintenance.*, clients … comms,
privacy, user.admin.*, plus request() / fetchWithAuth(). Admin methods
require a bearer access token with matching backend permissions (client:read,
user:write, *:*, etc.). The SDK does not enforce permissions — the OAuth
server does.
Migrating from hand-rolled oauth.ts
Replace direct fetch calls with namespaced SDK methods:
// Before
await fetch(`${baseUrl}/oauth/token`, { method: "POST", body });
// After
const tokens = await auth.token.authorizationCode({
code,
redirectUri,
codeVerifier,
});
For admin console CRUD (/api/v1/clients, etc.), use the full SDK from
GitHub Packages — see CONTEXT.md.
Inject internal URL rewriting via createAuthClient({ fetch }) — same pattern
as accounts / developers oauth-fetch.ts.
Release and Publish
- Changelog:
CHANGELOG.md - Publishing guide:
PUBLISHING.md
GitHub Packages (private, tktchurch scope) is configured via:
- Script:
bun run publish:github - Workflow:
.github/workflows/publish-github-packages.yml - Auth token:
NODE_AUTH_TOKEN(GITHUB_TOKENin workflow)
npmjs (public) publishing ships the consumer build only:
- Script:
bun run publish:npmjs(runsrelease:check:consumer) - Workflow:
.github/workflows/publish-npmjs.yml - Auth: npm trusted publishing (OIDC, no
NPM_TOKENsecret) or localNODE_AUTH_TOKEN
GitHub Packages ships the full internal SDK. See
PUBLISHING.md for the dual-build matrix.