OAuth 2.0 and OpenID Connect authentication for Harper applications. Supports GitHub, Google, Azure AD, Auth0, and custom OIDC providers with automatic token refresh and lifecycle hooks for user provisioning.
- Multi-provider support - GitHub, Google, Azure AD, Auth0, and custom OIDC providers
- Automatic token refresh - Proactive token renewal on every request
- Lifecycle hooks - Extensible hooks for user provisioning and custom logic
- CSRF protection - Distributed token storage for cluster support
- ID token verification - Full OIDC support with signature validation
- Zero configuration - Works with Harper's session system automatically
Requires Harper v5 (harper >=5.0.0). For Harper v4 (the legacy harperdb package), use the 1.x line.
npm install @harperfast/oauthAdd to your config.yaml:
'@harperfast/oauth':
package: '@harperfast/oauth'
redirectUri: ${OAUTH_REDIRECT_URI} # your app's public origin — required for any deployed app
providers:
github:
clientId: ${OAUTH_GITHUB_CLIENT_ID}
clientSecret: ${OAUTH_GITHUB_CLIENT_SECRET}export OAUTH_GITHUB_CLIENT_ID="your-client-id"
export OAUTH_GITHUB_CLIENT_SECRET="your-client-secret"
export OAUTH_REDIRECT_URI="http://localhost:9926/oauth/callback" # local dev value — must become your public origin when you deploy (step 3)Note: The
exportcommands above are for local development and quick testing only. You can also use a.envfile withdotenv-clifor local dev — just don't commit it. For Harper Fabric deployments, see the Harper Fabric documentation for managing runtime environment variables.
This has two sides — both are required:
- In your app config — set
redirectUrito your app's public origin plus/oauth/callback(step 1 above). The plugin appends the provider name for you, so one setting covers every provider. - In your provider settings — register the provider-specific URL the plugin will send:
https://your-domain/oauth/github/callback
Note:
redirectUridefaults tohttp://localhost:9926/oauth/callback. If you skip side 1 on a deployed app, the provider will send your users tolocalhostand login will never complete — with no configuration error to tell you why. See Understanding Redirects.
Create or update users when they log in:
// resources.ts
import { registerHooks } from '@harperfast/oauth';
registerHooks({
onLogin: async (oauthUser, tokenResponse, session, request, provider) => {
const { User } = tables;
// Find or create user
let user;
for await (const existing of User.search({ email: oauthUser.email })) {
user = existing;
break;
}
if (!user) {
user = await User.create({
email: oauthUser.email,
name: oauthUser.name,
provider: provider,
});
}
return { user: String(user.id) };
},
});
// Export your resources...The hook can also gate the login: return { status: 'denied' } or { status: 'needs_confirmation', redirect } and no session is created — see Lifecycle Hooks.
Security: returning
{ user }is authoritative and bypasses the account-adoption gate. The example above is illustrative — do not resolve an existing account by email alone before you have confirmed the identity, becauseoauthUser.emailVerifiedis not proof the email came from an authenticated source (see onLogin). For adopting an existing account, bind to a stable provider identity (e.g.oauthUser.providerUserId) or use a confirmation flow.
Navigate to:
http://localhost:9926/oauth/github/login
Access authenticated user in your resources:
export class MyResource extends tables.Resource {
async get(target, request) {
if (!request.session?.user) {
throw new ClientError('Not authenticated', 401);
}
return {
userId: request.session.user,
email: request.session.oauthUser.email,
};
}
}Built-in provider templates (not active until configured):
- GitHub - OAuth 2.0
- Google - OpenID Connect
- Azure AD - OpenID Connect
- Auth0 - OpenID Connect
- Okta - OpenID Connect (with multi-tenant support)
- Custom OIDC - Any compliant provider
Note: Built-in providers are templates that require configuration. None are active until you provide OAuth credentials (
clientId,clientSecret, etc.). Having provider code in the plugin does not enable authentication or create security exposure.
The plugin automatically handles:
- OAuth Flow - Redirects to provider, handles callback, exchanges code for tokens
- Token Management - Validates and refreshes tokens on every HTTP request
- Session Integration - Stores OAuth data in Harper sessions
- CSRF Protection - Validates state parameter using distributed token storage
- Auto-logout - Clears session when tokens expire and can't be refreshed
Complete documentation is available in the docs directory:
- Getting Started - Installation and quick start guide
- Configuration - Complete configuration reference
- OAuth Providers - Provider setup guides
- Lifecycle Hooks - User provisioning and custom logic
- Multi-Tenant SSO - Per-organization OAuth providers for B2B SaaS
- Token Refresh and Sessions - How OAuth tokens and Harper sessions interact
- MCP OAuth - Authorization server for Model Context Protocol clients (experimental)
- API Reference - Endpoints and programmatic API
- Changelog - Release history
npm install
npm run buildnpm test
npm run test:coveragenpm run lint
npm run format:check
npm run format:writeThe plugin manages its own tables in the oauth database — see schema/oauth.graphql for the definitions:
csrf_tokens— CSRF protection for the human OAuth flow (10-minute expiration)harper_oauth_mcp_clients— RFC 7591 Dynamic Client Registration; no expiration, soclient_ids cached by MCP clients (Claude Desktop, Cursor,mcp-remote) survive Harper restartsharper_oauth_mcp_keys— MCP JWT signing keys, stored in the database so replicated nodes share one rotation-aware key setmcp_auth_codes— single-use PKCE authorization codes (5-minute expiration)mcp_refresh_families— OAuth 2.1 single-use refresh-token rotation statemcp_assertion_jtis— RFC 7523 client-assertion replay guard (120-second expiration)
The plugin can also act as an OAuth 2.1 authorization server for Model Context Protocol clients (Claude Desktop, Cursor, mcp-remote), letting them authenticate against the same upstream providers. Two steps: enable it in config, and guard your MCP route with withMCPAuth.
# config.yaml
'@harperfast/oauth':
package: '@harperfast/oauth'
providers:
github:
clientId: ${OAUTH_GITHUB_CLIENT_ID}
clientSecret: ${OAUTH_GITHUB_CLIENT_SECRET}
mcp:
enabled: true
issuer: https://my-app.example.com # required; pin to your public origin// resources.ts
import { server } from 'harper';
import { withMCPAuth } from '@harperfast/oauth';
// request.mcp (verified { sub, client_id, aud, scope }) is guaranteed present inside the guarded handler.
server.http(
withMCPAuth((request) => ({ status: 200, body: JSON.stringify({ user: request.mcp.sub }) })),
{
urlPath: '/mcp',
}
);The plugin serves discovery (/.well-known/*), Dynamic Client Registration, the authorize/token endpoints, and JWKS; withMCPAuth verifies the audience-bound JWT on every call and fails closed. There's an onMCPTokenIssued hook to react when a client gains access (client→user mapping, monitoring, rate-limiting) and a built-in audit log.
Headless agents (machine-to-machine). An agent with no browser and no human authenticates as itself: grant_type=client_credentials with an RFC 7523 private_key_jwt client assertion (EdDSA), rate-limited per verified client. Client identity resolves CIMD-first — a URL-shaped client_id is fetched as a Client ID Metadata Document through an SSRF-guarded, rate-limited fetch. See Headless agents.
Status: experimental, opt-in. See MCP OAuth for the full flow, the withMCPAuth registration models and options, the hook, configuration, the production checklist, and troubleshooting — and what's not yet supported (#156) for v1.1 forward-work.
- HTTPS required - OAuth requires HTTPS in production
- CSRF protection - Automatic via state parameter validation
- ID token verification - OIDC providers verify token signatures
- Secure sessions - Use Harper's secure session configuration
- Token storage - Tokens stored in session (configure secure cookies)
- Account adoption requires a verified claim (2.6.0+) - An OAuth login adopts an existing Harper
hdb_useronly when the claim is a verified email from an authenticated source (a signature-verified OIDC id token with a validated issuer, or GitHub's authenticated email). Typical Google/GitHub verified-email setups (and anyonLogin-hook login) are unaffected; adoption from an unverified or non-email claim is now denied, or, when no account matches, given a roleless, non-resolvable identity. See Account-Adoption Gate. - MCP client registration (when
mcp.enabledis true) - The/oauth/mcp/registerendpoint defaults to open registration per RFC 7591. Setmcp.dynamicClientRegistration.initialAccessTokento require a bearer token on registration, ormcp.dynamicClientRegistration.allowedRedirectUriHoststo restrict which hosts may registerredirect_uris. Seedocs/configuration.md. - CIMD resolution (when
mcp.enabledis true) - URL-shapedclient_ids are resolved as Client ID Metadata Documents by default. Disable withmcp.clientIdMetadataDocuments.enabled: false, or restrict which hosts are fetched withmcp.clientIdMetadataDocuments.allowedHosts.
Enable debug endpoints for testing:
'@harperfast/oauth':
debug: true
providers:
github:
clientId: ${OAUTH_GITHUB_CLIENT_ID}
clientSecret: ${OAUTH_GITHUB_CLIENT_SECRET}Debug endpoints:
GET /oauth/- List configured providersGET /oauth/test- Interactive test pageGET /oauth/{provider}/user- View current sessionGET /oauth/{provider}/refresh- Trigger token refresh
Warning: Never enable debug mode in production.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Copyright 2025 HarperDB, Inc.