Ecosystem

nuxt-auth-idp

Build your own OpenApe Identity Provider as a Nuxt app.

@openape/nuxt-auth-idp

Drop-in Nuxt module that turns a Nuxt app into a full OpenApe IdP — WebAuthn passkey login, SSH-key agent auth, OAuth/OIDC endpoints, grant approval flows, federation, and admin UI. Counterpart to @openape/nuxt-auth-sp on the service-provider side.

Pair it with a storage backend of your choice: the module ships with an in-memory store suitable for development, and the openape-free-idp reference app wires it to Drizzle + libsql/Turso for persistent production hosting.

Quick Start

1. Install

pnpm add @openape/nuxt-auth-idp

2. Add to nuxt.config.ts

export default defineNuxtConfig({
  modules: ['@openape/nuxt-auth-idp'],
  openapeIdp: {
    issuer: 'https://id.example.com',
    rpName: 'Example IdP',
    sessionSecret: '', // set NUXT_OPENAPE_IDP_SESSION_SECRET in prod
  },
})

In dev the module auto-generates a sessionSecret and derives rpID from the request host. In production you must set sessionSecret and adminEmails explicitly.

3. Register a storage backend

The module is storage-agnostic. Register each store from a server plugin (server/plugins/02.stores.ts):

import {
  defineUserStore,
  defineCredentialStore,
  defineKeyStore,
  defineCodeStore,
  defineRefreshTokenStore,
  defineJtiStore,
  defineWebAuthnChallengeStore,
  defineRegistrationUrlStore,
  defineGrantStore,
  defineGrantChallengeStore,
  defineSshKeyStore,
  defineShapeStore,
} from '#imports'

export default defineNitroPlugin(() => {
  defineUserStore(() => myUserStore)
  defineCredentialStore(() => myCredentialStore)
  // …register all stores
})

For a complete wired-up example (Drizzle + libsql), see apps/openape-free-idp/server/plugins/03.stores.ts.

Module Options

Full configuration surface in nuxt.config.ts:

openapeIdp: {
  // Security
  sessionSecret: '',            // 32+ char secret (required in prod)
  sessionMaxAge: 604800,        // session cookie TTL in seconds (7 days)
  managementToken: '',          // bearer for management endpoints
  adminEmails: '',              // comma-separated list

  // Identity
  issuer: '',                   // OAuth `iss` claim; falls back to request origin
  rpName: '',                   // WebAuthn RP display name
  rpID: '',                     // WebAuthn RP ID; falls back to request host
  rpOrigin: '',                 // WebAuthn origin; falls back to request origin
  rpHostAllowList: '',          // comma-separated; see Multi-Tenant below

  // WebAuthn policy
  requireUserVerification: false,
  residentKey: 'preferred',     // 'preferred' | 'required' | 'discouraged'
  attestationType: 'none',      // 'none' | 'indirect' | 'direct' | 'enterprise'

  // Routes (which groups to register)
  routes: true,                 // or { auth, oauth, grants, admin, agent }
  pages: true,                  // register /login, /register, /account, /admin etc.

  // Grants
  grants: {
    enablePages: true,          // register /grants, /grant-approval, /enroll pages
    storageKey: 'openape-grants',
  },

  // Federation
  federationProviders: '',      // JSON string — see Federation below

  // Embedding
  allowedFrameAncestors: '',    // space-separated origins; defaults to frame-ancestors 'none'

  // Storage namespace (used by the default in-memory store)
  storageKey: 'openape-idp',
}

All options are settable via environment variables following Nuxt's runtimeConfig convention (NUXT_OPENAPE_IDP_*):

VariableConfig key
NUXT_OPENAPE_IDP_SESSION_SECRETsessionSecret
NUXT_OPENAPE_IDP_SESSION_MAX_AGEsessionMaxAge
NUXT_OPENAPE_IDP_MANAGEMENT_TOKENmanagementToken
NUXT_OPENAPE_IDP_ADMIN_EMAILSadminEmails
NUXT_OPENAPE_IDP_ISSUERissuer
NUXT_OPENAPE_IDP_RP_NAMErpName
NUXT_OPENAPE_IDP_RP_IDrpID
NUXT_OPENAPE_IDP_RP_ORIGINrpOrigin
NUXT_OPENAPE_IDP_RP_HOST_ALLOW_LISTrpHostAllowList
NUXT_OPENAPE_IDP_REQUIRE_USER_VERIFICATIONrequireUserVerification
NUXT_OPENAPE_IDP_RESIDENT_KEYresidentKey
NUXT_OPENAPE_IDP_ATTESTATION_TYPEattestationType
NUXT_OPENAPE_IDP_FEDERATION_PROVIDERSfederationProviders
NUXT_OPENAPE_IDP_ALLOWED_FRAME_ANCESTORSallowedFrameAncestors

Server API Routes

The module auto-registers the following routes (gated by the routes option):

Auth (routes.auth)

  • POST /api/session/login — cookie-session login
  • POST /api/session/logout
  • GET /api/session/ssh-keys — list caller's SSH keys
  • POST /api/session/ssh-keys — add an SSH key for the authenticated user
  • DELETE /api/session/ssh-keys/:keyId
  • POST /api/logout
  • GET /api/me — returns { email, name, isAdmin } for the cookie session

WebAuthn (under routes.auth)

  • POST /api/webauthn/register/options · POST /api/webauthn/register/verify
  • POST /api/webauthn/login/options · POST /api/webauthn/login/verify
  • GET /api/webauthn/credentials — list the caller's registered credentials
  • POST /api/webauthn/credentials/add/options · POST /api/webauthn/credentials/add/verify
  • DELETE /api/webauthn/credentials/:id

OAuth / OIDC (routes.oauth)

  • GET /authorize — OAuth authorization endpoint
  • POST /token — token exchange
  • POST /revoke
  • GET /userinfo
  • GET /.well-known/openid-configuration
  • GET /.well-known/jwks.json

Grants (routes.grants)

  • GET /api/grants · POST /api/grants · GET /api/grants/:id
  • POST /api/grants/:id/approve|deny|revoke|token|consume
  • POST /api/grants/verify — verify an AuthZ-JWT
  • POST /api/grants/batch
  • GET|POST /api/delegations · DELETE /api/delegations/:id · POST /api/delegations/:id/validate
  • GET /api/shapes · GET /api/shapes/:cliId · POST /api/shapes/resolve
  • GET|POST /api/standing-grants · DELETE /api/standing-grants/:id
  • GET /api/users/:email/agents

Agents (routes.agent)

  • POST /api/agent/challenge · POST /api/agent/authenticate · POST /api/agent/enroll
  • POST /api/auth/challenge · POST /api/auth/authenticate · POST /api/auth/enroll — unified endpoints that work for agents and humans with SSH keys

Admin (routes.admin, requires isAdmin)

  • /api/admin/users · /api/admin/users/:email · /api/admin/users/:email/credentials|ssh-keys
  • /api/admin/agents · /api/admin/agents/:id
  • /api/admin/sessions · /api/admin/sessions/:familyId · /api/admin/sessions/user/:email
  • /api/admin/registration-urls · /api/admin/registration-urls/:token

Federation (if federationProviders is set)

  • GET /auth/federated/:providerId · /auth/federated/:providerId/callback
  • GET /api/federation/providers

Built-in Pages

When pages: true (default), the module registers these Vue pages. A consuming app can override any by defining its own page with the same path:

  • /login, /register, /account, /admin
  • /grants, /grant-approval, /enroll (when grants.enablePages: true)
  • /agents, /agents/:email

Composables

useIdpAuth()

const { user, loading, fetchUser, logout } = useIdpAuth()
PropertyTypeDescription
userRef<{ email, name, isAdmin } | null>Cookie-session claims for the current user
loadingRef<boolean>Whether fetchUser() is in flight
fetchUser()() => Promise<void>Hydrate user from /api/me
logout()() => Promise<void>Destroy the cookie session

useWebAuthn()

Helper for browser-side passkey enrollment and login flows (wraps @simplewebauthn/browser). Used by the built-in pages; available for custom UIs.

Multi-Tenant (Host-Derived RP)

The module supports hosting multiple IdP domains from a single deployment. RP config is resolved per request: if the incoming Host header is in rpHostAllowList, the module overrides the static rpID/rpOrigin/issuer for that request.

openapeIdp: {
  rpHostAllowList: 'id.example.com,id.example.at',
  // rpID / rpOrigin / issuer are vestigial when the allow-list covers every host
}

The consuming app supplies a middleware that populates event.context.openapeRpConfig — all WebAuthn handlers read from that seam first before falling back to the static module config. See the full walkthrough in Multi-Tenant IdP.

Storage Backend Interfaces

Each define<Name>Store() accepts a factory that receives the current H3Event and returns an instance of the matching interface from @openape/auth (user, credential, key, code, refresh-token, jti, webauthn-challenge, registration-url) or @openape/grants (shape). The IdP module only imports the shape — you control persistence.

The reference openape-free-idp implements each store against Drizzle + libsql (Turso or local SQLite); fork that pattern if you want a persistent IdP quickly, or ship your own backend (Postgres, DynamoDB, etc).

Security Headers

The module installs these headers on every request:

  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • X-Frame-Options: DENY + Content-Security-Policy: frame-ancestors 'none' — unless allowedFrameAncestors is set, in which case X-Frame-Options is dropped and CSP carries frame-ancestors 'self' <origins>.

Session, auth, and admin routes additionally receive Cache-Control: no-store.