Skip to contentSkip to navigation

HTTP Handler

Expose Own Auth through one framework-neutral Web Request and Response handler with secure cookies, CSRF checks, typed errors, and generated OpenAPI.

Overview

Expose Own Auth through one framework-neutral handler built on the standard Web Request and Response APIs.

Create the handler

auth-handler.ts
import { createOwnAuthHandler } from "own-auth/http";

import { auth } from "./auth";

export const authHandler = createOwnAuthHandler(auth);

Mount authHandler at /api/auth/*. Frameworks that use Web Request and Response objects can call it directly.

app/api/auth/[...path]/route.ts
import { authHandler } from "../../../../../auth-handler";

export const GET = authHandler;
export const POST = authHandler;

The auth engine remains framework-independent. The handler only translates HTTP requests into existing Own Auth methods.

Protocol Handlers

The application auth API, OAuth and OpenID Connect server, and SCIM server use separate handlers because they have different authentication, content-type, and error contracts.

handlers.ts
import { createOwnAuthAuthorizationServerHandler } from "own-auth/authorization-server";
import { createOwnAuthHandler } from "own-auth/http";
import { createOwnAuthScimHandler } from "own-auth/scim";

import { auth } from "./auth";

export const authHandler = createOwnAuthHandler(auth);
export const authorizationServerHandler =
  createOwnAuthAuthorizationServerHandler(auth);
export const scimHandler = createOwnAuthScimHandler(auth);

Mount only the optional protocol handlers configured by the application:

HandlerDefault mount
authHandler/api/auth/*
authorizationServerHandler/oauth/* and /.well-known/* as documented by the authorization server
scimHandler/scim/v2/*

These handlers share the same auth instance and storage. They do not call one another.

Endpoints

MethodPathOperation
POST/api/auth/sign-up/emailSign up with email and password
POST/api/auth/sign-in/emailSign in with email and password
GET/api/auth/sessionGet the current session
POST/api/auth/sign-outSign out
POST/api/auth/password/changeChange the current password
POST/api/auth/magic-link/requestSend a magic link
POST/api/auth/magic-link/verifyVerify a magic link
POST/api/auth/email-verification/requestSend a verification link
POST/api/auth/email-verification/verifyVerify an email address
POST/api/auth/password-reset/requestSend a password reset link
POST/api/auth/password-reset/confirmReset a password
POST/api/auth/sms/requestSend an SMS code
POST/api/auth/sms/verifyVerify an SMS code
POST/api/auth/invitations/acceptAccept an organisation invitation
POST/api/auth/oauth/startStart redirect or popup OAuth
GET/api/auth/oauth/google/callbackComplete Google OAuth
GET/api/auth/oauth/github/callbackComplete GitHub OAuth
GET/api/auth/oauth/apple/callbackComplete an Apple query callback
POST/api/auth/oauth/apple/callbackComplete an Apple form_post callback
POST/api/auth/oauth/google/one-tap/prepareCreate a Google One Tap nonce transaction
POST/api/auth/oauth/google/one-tap/verifyVerify a Google One Tap credential
POST/api/auth/oauth/unlinkUnlink a provider from the current user
POST/api/auth/saml/startStart SAML sign-in or identity linking and return the identity-provider URL
POST/api/auth/saml/acsComplete a SAML form-post callback
GET/api/auth/saml/metadata?connectionId=...Return service-provider metadata for one connection
POST/api/auth/mfa/totp/completeComplete MFA with TOTP
POST/api/auth/mfa/recovery/completeComplete MFA with a recovery code
POST/api/auth/mfa/totp/enrollBegin TOTP enrollment
POST/api/auth/mfa/totp/confirmConfirm TOTP and create recovery codes
POST/api/auth/mfa/totp/disableDisable TOTP
POST/api/auth/mfa/recovery/regenerateReplace all recovery codes
POST/api/auth/passkeys/register/optionsCreate passkey registration options
POST/api/auth/passkeys/register/verifyVerify passkey registration
POST/api/auth/passkeys/authenticate/optionsCreate passkey authentication options
POST/api/auth/passkeys/authenticate/verifyVerify passkey authentication or MFA
GET/api/auth/passkeysList the current user's passkeys
POST/api/auth/passkeys/renameRename a passkey
POST/api/auth/passkeys/revokeRevoke a passkey
configured/api/auth/plugins/{plugin-id}/...Run a namespaced plugin endpoint

The exported ownAuthEndpointContract contains each route's method, path, request schema, response schema, session behavior, and auth error codes.

JSON request bodies are limited to 64 KiB by default. Change maxRequestBodyBytes only when an application genuinely needs a different limit.

Request context

Pass a trusted client IP from the framework or hosting adapter so IP-based OAuth and One Tap limits can run. The handler does not trust forwarding headers by default because applications differ in which proxies they trust.

ts
export const authHandler = createOwnAuthHandler(auth, {
  getRequestContext(request) {
    return {
      ipAddress: getTrustedClientIp(request),
      userAgent: request.headers.get("user-agent") ?? undefined,
    };
  },
});

Do not copy an arbitrary X-Forwarded-For value directly. Resolve the address through the framework or platform's trusted-proxy configuration.

Sessions

Completed signup, signin, magic-link verification, phone login, OAuth, One Tap, SAML, and passkey authentication set an HttpOnly session cookie. The JSON response never contains the raw session token, password hash, or session-token hash.

The default cookie is:

text
own_auth_session; Path=/; HttpOnly; SameSite=Lax

Secure is added automatically over HTTPS. It can only be disabled on localhost. Cookie-authenticated requests and Authorization: Bearer <session-token> requests use the same session verification path.

Customize the cookie when creating the handler:

ts
export const authHandler = createOwnAuthHandler(auth, {
  cookie: {
    name: "session",
    sameSite: "strict",
  },
});

MFA challenges

When a first factor succeeds for a user with MFA enabled, the handler does not set a session cookie. It stores the pending challenge in a separate temporary HttpOnly cookie and returns only the available methods and expiry:

json
{
  "status": "mfa_required",
  "methods": ["totp", "recovery_code", "passkey"],
  "expiresAt": "2026-07-14T12:00:00.000Z"
}

The default challenge cookie is own_auth_mfa. It is cleared after successful MFA, signout, or password reset. Browser JavaScript never receives the challenge token.

OAuth callbacks

Google and GitHub use GET callbacks. Apple supports GET and its required application/x-www-form-urlencoded POST callback. Apple form bodies are always limited to 64 KiB, even when maxRequestBodyBytes is larger.

Redirect callbacks set the session or MFA cookie and return to the transaction's validated destination. Popup callbacks post only complete, mfa_required, linked, or failure to the exact stored opener origin. They never post OAuth codes, provider tokens, session tokens, or MFA challenge tokens.

Provider and account-resolution failures are returned through the popup message or redirect query parameters. Direct callback HTTP errors are reserved for malformed or rate-limited requests.

SAML callbacks

SAML is available only when the auth instance uses createSaml from own-auth/saml. POST /saml/start returns an identity-provider URL instead of redirecting automatically, so server code and the TypeScript client can choose when navigation happens.

The assertion consumer service accepts only application/x-www-form-urlencoded POST bodies and always limits them to 64 KiB. It validates the SAML transaction before setting a session or temporary MFA cookie. The response then redirects to the transaction's validated destination with only own_auth_status and, on failure, a generic own_auth_error value. Raw SAML responses, relay state, subjects, session tokens, and MFA challenge tokens are never placed in the redirect URL.

Service-provider metadata is connection-specific. Give the identity provider the exact metadata URL returned for that connection:

text
https://auth.example.com/api/auth/saml/metadata?connectionId=samlc_...

See SAML SSO for setup and connection behavior.

SCIM Handler

SCIM is available only when the auth instance includes scim: {}. The separate handler from own-auth/scim authenticates requests with a connection-scoped SCIM bearer token instead of an application session.

ts
import { createOwnAuthScimHandler } from "own-auth/scim";

export const scimHandler = createOwnAuthScimHandler(auth, {
  getRequestContext(request) {
    return {
      ipAddress: getTrustedClientIp(request),
      userAgent: request.headers.get("user-agent") ?? undefined,
    };
  },
});

The handler supports SCIM User discovery, creation, listing, filtering, reads, replacement, PATCH updates, and deletion under /scim/v2. Request bodies must use application/scim+json and are limited to 256 KiB by default.

SCIM errors use the RFC 7644 error shape rather than the application auth error shape. Generate its separate OpenAPI document with createOwnAuthScimOpenApiDocument.

See SCIM Provisioning for setup, endpoints, ETags, lifecycle behavior, and security rules.

CSRF protection

Mutation requests from a browser must come from the same origin as the handler. Add a frontend origin only when the frontend and auth API use different HTTPS origins:

ts
export const authHandler = createOwnAuthHandler(auth, {
  trustedOrigins: ["https://app.example.com"],
});

HTTPS origins and local development origins are accepted. Cookie-authenticated mutation requests without an Origin header are rejected. Server and native clients can use a bearer session without browser cookie behavior.

Errors

Every failure uses the same response shape:

json
{
  "error": {
    "code": "invalid_credentials",
    "message": "Invalid email or password"
  }
}
StatusMeaning
400Invalid input, token, redirect, or request format
401Invalid credentials, session, token, or code
403Disabled user, missing permission, or failed CSRF check
404Route or required resource not found
409Existing email, member, key, or invitation conflict
413Request body is larger than the configured limit
415Request uses an unsupported content type
429Rate limit or OTP attempt limit reached
500Unexpected server failure with a safe public message
502OAuth provider request failed
504A plugin before-hook timed out

Catch OwnAuthClientError in client code to read the typed code, message, and HTTP status.

OpenAPI

Generate an OpenAPI 3.1 document from the same endpoint contract:

ts
import { createOwnAuthOpenApiDocument } from "own-auth/http";

const openapi = createOwnAuthOpenApiDocument({
  title: "My App Auth API",
  version: "1.0.0",
  serverUrl: "https://api.example.com",
});

Generate the SCIM document separately:

ts
import { createOwnAuthScimOpenApiDocument } from "own-auth/scim";

const scimOpenApi = createOwnAuthScimOpenApiDocument({
  title: "My App SCIM API",
});

When the handler uses a custom cookie name, pass the same value as sessionCookieName.

Request schemas, response schemas, operation IDs, and endpoint error codes come from ownAuthEndpointContract. There is no second route definition to keep in sync.

The core document remains stable and excludes plugin routes. Generate the configured plugin document separately:

ts
import { createConfiguredOwnAuthOpenApiDocument } from "own-auth";

const openapi = createConfiguredOwnAuthOpenApiDocument(plugins);
console.log(openapi["x-own-auth-plugin-fingerprint"]);

The fingerprint covers the core version and complete configured plugin endpoint contract. Regenerate plugin clients when it changes.