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'
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"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.
Set your OAuth callback URL in your provider settings:
https://your-domain/oauth/github/callback
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.
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)
- 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.