Multi-tenant authentication and authorization service built with Node.js, Express 5, and MongoDB.
Heimdall provides password-based and WebAuthn/passkey authentication, JWT access and refresh tokens, project-scoped API keys, and a role-based membership system.
- Password authentication with bcrypt hashing
- Passkey/WebAuthn (FIDO2) as an alternative login method
- Social OAuth (Google, GitHub, Apple) login and account linking
- JWT access tokens (15 min) + refresh tokens (7 days) with rotation
- Multi-tenant projects with API key scoping
- Role hierarchy: Owner > Admin > Manager > Member
- Membership management: invite, accept, update roles, remove, leave
- Passkey enrollment nudge: configurable per-project policy with user opt-out
- Rate limiting on auth endpoints
- CORS support
- Swagger UI at
/api/docs
- Runtime: Node.js + TypeScript
- Framework: Express 5
- Database: MongoDB (Mongoose 8)
- Auth: jsonwebtoken, bcrypt, @simplewebauthn/server v10
- Testing: Jest + Supertest
- Node.js 18+
- MongoDB instance (local or Atlas)
npm installCreate a .env file in the project root:
# Required
JWT_SECRET=your-secret-key
CONNECTION_STRING=mongodb+srv://user:pass@cluster.mongodb.net/heimdall
# Optional
PORT=7001
REFRESH_TOKEN_SECRET=your-refresh-secret
# WebAuthn/Passkey (optional, defaults shown)
WEBAUTHN_RP_ID=localhost
WEBAUTHN_RP_NAME=Heimdall
WEBAUTHN_ORIGIN=http://localhost:3000For multi-origin passkey support, provide comma-separated origins:
WEBAUTHN_ORIGIN=https://app.example.com,https://admin.example.com# Development
npm run dev
# Production
npm run build
npm startnpm test
npm run test:watch
npm run test:coverageInteractive API docs are available at /api/docs (Swagger UI) when the server is running.
All project-scoped endpoints require an x-api-key header. Authenticated endpoints require a Bearer token in the Authorization header.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /register |
API Key | Register a new user |
| POST | /login |
API Key | Login with email/password |
| POST | /refresh |
API Key | Refresh access token |
| POST | /logout |
Bearer | Logout (revoke refresh token) |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /register/options |
Bearer | Generate passkey registration challenge |
| POST | /register/verify |
Bearer | Complete passkey registration |
| POST | /login/options |
API Key | Generate passkey login challenge |
| POST | /login/verify |
API Key | Complete passkey login |
| GET | /credentials |
Bearer | List registered passkeys |
| PATCH | /credentials/:id |
Bearer | Rename a passkey |
| DELETE | /credentials/:id |
Bearer | Delete a passkey |
| POST | /opt-out |
Bearer | Opt out of passkey enrollment nudge |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /login |
API Key | Login or register via social provider |
| POST | /link |
Bearer | Link a social account to current user |
| DELETE | /unlink/:provider |
Bearer | Unlink a social account |
| GET | /accounts |
Bearer | List linked social accounts |
Supported providers: Google, GitHub, Apple. Provider credentials (client ID, client secret) are configured per-project in the database.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
Bearer | List all users in project |
| GET | /:id |
Bearer (Admin+) | Get user by ID |
| PUT | /:id |
Bearer (Admin+) | Update user |
| DELETE | /:id |
Bearer (Admin+) | Remove user |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
Bearer | List project members |
| GET | /:userId |
Bearer | Get member details |
| POST | /invite |
Bearer (Admin+) | Invite a member |
| PUT | /:userId/role |
Bearer (Admin+) | Update member role |
| DELETE | /:userId |
Bearer (Admin+) | Remove a member |
| POST | /leave |
Bearer | Leave project |
| POST | /accept |
Bearer | Accept invitation |
| PUT | /metadata |
Bearer | Update own metadata |
POST /api/auth/register (x-api-key) -> creates user + membership
POST /api/auth/login (x-api-key) -> returns accessToken + refreshToken
POST /api/auth/refresh (x-api-key) -> rotates refreshToken, returns new pair
POST /api/auth/logout (Bearer) -> revokes refreshToken
# 1. Register a passkey (must be logged in first)
POST /api/auth/passkey/register/options (Bearer) -> returns WebAuthn options + challengeId
POST /api/auth/passkey/register/verify (Bearer) -> stores credential
# 2. Login with passkey
POST /api/auth/passkey/login/options (x-api-key) -> returns WebAuthn options + challengeId
POST /api/auth/passkey/login/verify (x-api-key) -> returns accessToken + refreshToken
Passkey login returns the same token structure as password login. Users can have both methods active and use either on any device.
POST /api/auth/social/login (x-api-key) -> exchanges OAuth code, returns accessToken + refreshToken
POST /api/auth/social/link (Bearer) -> links a social account to current user
DELETE /api/auth/social/unlink/:provider (Bearer) -> unlinks a social account
GET /api/auth/social/accounts (Bearer) -> lists linked social accounts
New users are automatically created on first social login. Existing users matched by email are linked automatically. Users must retain at least one auth method (password or social) before unlinking.
Projects can set passkeyPolicy to nudge users toward passkey setup:
"optional"(default) -- no nudge, users add passkeys if they choose"encouraged"-- login response includespasskeySetupRequired: truefor users without passkeys who haven't opted out
Users can opt out via POST /api/auth/passkey/opt-out, which stores the preference in their membership metadata.
src/
controllers/ # Request handlers
db/ # Database connection
middleware/ # authenticate, authoriseRole, validateApiKey, validateMembership
config/ # Feature flags
models/ # Mongoose schemas (User, Project, UserProjectMembership,
# RefreshToken, PasskeyCredential, WebAuthnChallenge, SocialAccount)
routes/ # Express routers
services/ # Business logic (social providers, cleanup, access grants)
types/ # TypeScript interfaces and enums
index.ts # App entry point
MIT