nuxt-auth-idp
@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_*):
| Variable | Config key |
|---|---|
NUXT_OPENAPE_IDP_SESSION_SECRET | sessionSecret |
NUXT_OPENAPE_IDP_SESSION_MAX_AGE | sessionMaxAge |
NUXT_OPENAPE_IDP_MANAGEMENT_TOKEN | managementToken |
NUXT_OPENAPE_IDP_ADMIN_EMAILS | adminEmails |
NUXT_OPENAPE_IDP_ISSUER | issuer |
NUXT_OPENAPE_IDP_RP_NAME | rpName |
NUXT_OPENAPE_IDP_RP_ID | rpID |
NUXT_OPENAPE_IDP_RP_ORIGIN | rpOrigin |
NUXT_OPENAPE_IDP_RP_HOST_ALLOW_LIST | rpHostAllowList |
NUXT_OPENAPE_IDP_REQUIRE_USER_VERIFICATION | requireUserVerification |
NUXT_OPENAPE_IDP_RESIDENT_KEY | residentKey |
NUXT_OPENAPE_IDP_ATTESTATION_TYPE | attestationType |
NUXT_OPENAPE_IDP_FEDERATION_PROVIDERS | federationProviders |
NUXT_OPENAPE_IDP_ALLOWED_FRAME_ANCESTORS | allowedFrameAncestors |
Server API Routes
The module auto-registers the following routes (gated by the routes option):
Auth (routes.auth)
POST /api/session/login— cookie-session loginPOST /api/session/logoutGET /api/session/ssh-keys— list caller's SSH keysPOST /api/session/ssh-keys— add an SSH key for the authenticated userDELETE /api/session/ssh-keys/:keyIdPOST /api/logoutGET /api/me— returns{ email, name, isAdmin }for the cookie session
WebAuthn (under routes.auth)
POST /api/webauthn/register/options·POST /api/webauthn/register/verifyPOST /api/webauthn/login/options·POST /api/webauthn/login/verifyGET /api/webauthn/credentials— list the caller's registered credentialsPOST /api/webauthn/credentials/add/options·POST /api/webauthn/credentials/add/verifyDELETE /api/webauthn/credentials/:id
OAuth / OIDC (routes.oauth)
GET /authorize— OAuth authorization endpointPOST /token— token exchangePOST /revokeGET /userinfoGET /.well-known/openid-configurationGET /.well-known/jwks.json
Grants (routes.grants)
GET /api/grants·POST /api/grants·GET /api/grants/:idPOST /api/grants/:id/approve|deny|revoke|token|consumePOST /api/grants/verify— verify an AuthZ-JWTPOST /api/grants/batchGET|POST /api/delegations·DELETE /api/delegations/:id·POST /api/delegations/:id/validateGET /api/shapes·GET /api/shapes/:cliId·POST /api/shapes/resolveGET|POST /api/standing-grants·DELETE /api/standing-grants/:idGET /api/users/:email/agents
Agents (routes.agent)
POST /api/agent/challenge·POST /api/agent/authenticate·POST /api/agent/enrollPOST /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/callbackGET /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(whengrants.enablePages: true)/agents,/agents/:email
Composables
useIdpAuth()
const { user, loading, fetchUser, logout } = useIdpAuth()
| Property | Type | Description |
|---|---|---|
user | Ref<{ email, name, isAdmin } | null> | Cookie-session claims for the current user |
loading | Ref<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: nosniffReferrer-Policy: strict-origin-when-cross-originX-Frame-Options: DENY+Content-Security-Policy: frame-ancestors 'none'— unlessallowedFrameAncestorsis set, in which case X-Frame-Options is dropped and CSP carriesframe-ancestors 'self' <origins>.
Session, auth, and admin routes additionally receive Cache-Control: no-store.
Related
@openape/nuxt-auth-sp— Service-provider counterpart- Grants — Grant model and AuthZ-JWT structure
- Multi-Tenant IdP — Host-derived RP config