← Back to blog

Use Own Auth as an OAuth authorization server


Use the Own Auth authorization server when another application or API needs independently scoped access to users who already authenticate through your Own Auth instance. This is different from adding Google or GitHub sign in. In this design, Own Auth is the issuer that handles authorization, consent, token exchange, refresh, revocation, discovery, and user information.

Configure the issuer and signing key

Apply the Own Auth migrations first. Generate an RSA signing key for OpenID Connect ID tokens and a separate shared encryption key for stored authorization requests and nonces. The issuer is the public origin that serves the protocol handler. The interaction URL is an application page for sign in, account selection, MFA, and consent.

auth.ts
import { readFileSync } from "node:fs";
import { createOwnAuth } from "own-auth";

export const auth = createOwnAuth({
  tokenPepper: process.env.OWN_AUTH_TOKEN_PEPPER!,
  encryption: {
    current: {
      id: "2026-01",
      key: process.env.OWN_AUTH_ENCRYPTION_KEY!,
    },
  },
  authorizationServer: {
    issuer: "https://auth.example.com",
    interactionUrl: "https://auth.example.com/authorize",
    signingKeys: {
      current: {
        id: "2026-01",
        privateKey: readFileSync(
          process.env.OWN_AUTH_SIGNING_KEY_PATH!,
          "utf8",
        ),
      },
    },
    scopes: {
      "documents:read": {
        label: "Read documents",
        description: "Read documents the user can access.",
      },
    },
  },
});

Mount the protocol handler

The authorization server has a separate handler because OAuth and OpenID Connect use their own content types, client authentication, redirects, and error responses. Mount it for the discovery, authorization, token, revocation, introspection, user information, and key routes.

authorization-handler.ts
import {
  createOwnAuthAuthorizationServerHandler,
} from "own-auth/authorization-server";

import { auth } from "./auth";

export const authorizationHandler =
  createOwnAuthAuthorizationServerHandler(auth);

Keep the normal Own Auth HTTP handler mounted for application sign in and session management. The protocol handler redirects unauthenticated authorization requests to the configured interaction page rather than replacing the application's login interface.

Register clients from trusted server code

Public native and desktop clients use authorization code with PKCE and do not receive a client secret. Confidential web clients receive a secret once. Store that value immediately because Own Auth retains only its protected hash and visible prefix.

register-client.ts
const created = await auth.authorizationServer.createClient({
  name: "Documents Web App",
  clientType: "confidential",
  applicationType: "web",
  redirectUris: ["https://client.example.com/oauth/callback"],
  allowedScopes: [
    "openid",
    "profile",
    "email",
    "documents:read",
    "offline_access",
  ],
});

saveClientSecret(created.client.clientId, created.clientSecret);

Build the interaction page

The interaction page receives an opaque interaction token. Load it on the backend with the current Own Auth session. The returned action tells the application to sign in, reauthenticate, select an account, complete MFA, ask for consent, or continue. Show client and scope details only after a valid session is present.

approve-interaction.ts
const interaction = await auth.authorizationServer.getInteraction({
  interactionToken,
  sessionToken,
});

const approved = await auth.authorizationServer.approveInteraction({
  interactionToken,
  sessionToken,
  approvedScopes: interaction.scopes.map((scope) => scope.name),
});

return Response.redirect(approved.redirectUrl);

Keep approval and denial behind the application's session and CSRF protections. The interaction token is hashed and single use. Never let browser input choose scopes that were not requested and allowed for the client.

Bind tokens to an API

Register a protected resource with an exact HTTPS identifier and allowed scopes. The client includes that resource in the authorization request. An API beside the auth instance can call auth.authorizationServer.verifyAccessToken. A separate API uses authenticated remote introspection through own-auth/protected-resource without receiving database or signing key access.

documents-api.ts
const verified = await auth.authorizationServer.verifyAccessToken({
  accessToken,
  resource: "https://api.example.com/",
  requiredScopes: ["documents:read"],
});

const documents = await loadDocumentsForUser(verified.userId);

Use DPoP where token copying is a material risk

DPoP binds access and refresh tokens to a client held key. Enable it for a client or protected resource when the client can retain that private key securely. Each token request and API request carries a fresh proof for the exact method and URL. A copied token cannot be used without the matching key.

DPoP adds key lifecycle and canonical URL requirements, so do not enable it as a label without testing proxy rewriting, proof replay, clock tolerance, and refresh rotation. Bearer tokens remain appropriate when the client or resource cannot implement the proof correctly and the risk model accepts bearer semantics.

Add device authorization deliberately

Command line tools and input limited devices can use device authorization when it is enabled in the server configuration and client grant types. The device receives a short code and polling interval. The user signs in on the verification page and approves or denies the request. Polling must respect the protocol interval and expiry.

Publish and monitor protocol metadata

Clients discover the issuer, authorization endpoint, token endpoint, user information endpoint, and signing keys from the public metadata. Verify those documents from outside the deployment after every routing change. Keep previous public signing keys in JWKS until every ID token they signed has expired.

Monitor authorization starts, approved and denied interactions, code exchange outcomes, refresh rotation, reuse detection, revocation, introspection, and provider latency. Logs must exclude authorization codes, access tokens, refresh tokens, client secrets, interaction tokens, DPoP proofs, and complete request bodies.

Give every client and protected resource a named owner, an approved redirect or identifier inventory, and a credential rotation date. Revoke unused clients rather than leaving permanent secrets active. A protocol server becomes shared infrastructure, so changes to scopes, signing keys, issuer routing, and consent text need coordinated release evidence from every dependent application.

Access tokens are opaque. Refresh tokens rotate on every use. Reusing an old refresh token revokes the complete grant family, including a token created by a racing request. Test PKCE, redirect URI equality, consent, MFA step up, refresh rotation, reuse detection, revocation, resource mismatch, insufficient scope, discovery metadata, signing key rotation, and remote introspection before allowing another application to depend on the issuer.