npm.io
0.10.0 • Published yesterday

@tktchurch/auth

Licence
MIT
Version
0.10.0
Deps
0
Vulns
0
Weekly
0

@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 (memory by 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 codeVerifier is a secret — keep it server-side (HttpOnly cookie or session store), never in localStorage. state is 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

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 localStorage or sessionStorage. The SDK does not ship a browser persistence adapter — use server-side cookies or a platform secure store (Keychain, Keystore). See SECURITY.md.

Framework Examples (Best Practices)

Production-oriented examples are available in:

They follow server-first patterns:

  1. Authorization code + PKCE redirect flow (primary).
  2. clientSecret remains server-only.
  3. Refresh token is stored in HttpOnly cookie.
  4. PKCE codeVerifier is stored in a signed HttpOnly session cookie.
  5. Refresh token rotation is performed on every refresh call.
  6. 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: fetchWithAuth and request() are available only in the full SDK (GitHub Packages / local file: 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.*, clientscomms, 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

GitHub Packages (private, tktchurch scope) is configured via:

npmjs (public) publishing ships the consumer build only:

  • Script: bun run publish:npmjs (runs release:check:consumer)
  • Workflow: .github/workflows/publish-npmjs.yml
  • Auth: npm trusted publishing (OIDC, no NPM_TOKEN secret) or local NODE_AUTH_TOKEN

GitHub Packages ships the full internal SDK. See PUBLISHING.md for the dual-build matrix.

Keywords