⚠️ Porta is in beta — APIs and features may change before v1.0
Skip to content

Architecture & node-oidc-provider

Porta is built on top of node-oidc-provider, a certified OpenID Connect provider implementation for Node.js. This page explains the relationship between Porta and node-oidc-provider, and how Porta extends it into a full multi-tenant identity platform.

Built on node-oidc-provider

node-oidc-provider is an open-source, OpenID Certified™ implementation authored by Filip Skokan (panva). It implements the core OIDC specification with strict standards compliance and is widely used in production systems.

Credit

Porta would not exist without the excellent work of Filip Skokan and the node-oidc-provider project. If you're building OIDC solutions on Node.js, we highly recommend exploring node-oidc-provider directly — it is one of the best OIDC implementations available in any language.

What node-oidc-provider Provides

node-oidc-provider handles the core OIDC protocol:

  • Authorization endpoint — Handles /auth requests, validates parameters, initiates interactions
  • Token endpoint — Issues access tokens, refresh tokens, handles client credentials
  • UserInfo endpoint — Returns claims about the authenticated user
  • JWKS endpoint — Publishes public signing keys
  • Discovery endpoint/.well-known/openid-configuration
  • Revocation and Introspection — Token lifecycle management
  • End Session — RP-initiated logout
  • PKCE — Proof Key for Code Exchange
  • Adapter interface — Pluggable storage for all OIDC artifacts

What Porta Adds on Top

Porta wraps node-oidc-provider and adds everything needed for a production multi-tenant identity platform:

LayerPorta's Addition
Multi-tenancyPath-based organization isolation (/{orgSlug}/*)
User managementFull user CRUD, status lifecycle, password policies
Authentication UIHandlebars templates for login, consent, reset, 2FA, invitations
Login methodsPassword + magic link, configurable per org and client
Two-factor authEmail OTP, TOTP, recovery codes with per-org policies
RBACRoles and permissions per application, injected into tokens
Custom claimsType-validated claim definitions, injected into tokens
BrandingPer-org logo, colors, CSS, company name
Admin APIJWT-authenticated REST API for all management operations
Admin CLICommand-line tool for bootstrapping and management
Hybrid storageRedis for ephemeral data, PostgreSQL for durable data
Audit loggingComprehensive security and admin event logging
Key managementES256 key generation, rotation, and lifecycle
Email systemTransactional emails for magic links, resets, invitations, OTP
i18nLocale-based translations for all UI pages and emails

How Porta Integrates with node-oidc-provider

Provider Configuration

Porta creates a configured Provider instance by building a comprehensive configuration object. This is where the OIDC engine is customized:

src/oidc/
├── configuration.ts      # Builds the Provider configuration
├── provider.ts           # Creates the Provider instance
├── adapter-factory.ts    # Routes models to Redis or PostgreSQL adapters
├── postgres-adapter.ts   # PostgreSQL adapter for durable artifacts
├── redis-adapter.ts      # Redis adapter for ephemeral artifacts
├── account-finder.ts     # User lookup + claims builder
└── client-finder.ts      # Client metadata lookup from DB

The configuration builder (configuration.ts) sets up:

  • Supported flows — Authorization Code + PKCE, Client Credentials, Refresh Tokens
  • Scopes and claims — Standard OIDC scopes plus RBAC roles and custom claims
  • Token TTLs — Configurable via system_config table with 60-second in-memory cache
  • Signing keys — ES256 keys loaded from the database
  • Interaction policy — Delegates login and consent to Porta's Koa routes
  • Client metadata lookup — Reads from PostgreSQL instead of static config
  • Account/claims finder — Builds claim sets from user data, RBAC roles, and custom claims

Interaction Model

node-oidc-provider delegates user interaction (login, consent) to the application. Porta implements this through Koa route handlers:

Multi-Tenant Overlay

Porta implements multi-tenancy via path-based routing. A tenant resolver middleware intercepts requests and resolves the organization:

Request: GET /acme-corp/.well-known/openid-configuration

        Tenant Resolver Middleware

        1. Extract "acme-corp" from path
        2. Check Redis cache for org
        3. If miss, query PostgreSQL
        4. Verify org status (active/suspended/archived)
        5. Set ctx.state.organization

        node-oidc-provider handles the OIDC request
        with org-specific issuer: https://porta.example.com/acme-corp

Each organization effectively gets its own OIDC provider namespace with:

  • Its own discovery document
  • Its own issuer URL
  • Its own set of clients and users
  • Its own branding on login pages

Hybrid Storage Architecture

One of Porta's key architectural decisions is the hybrid adapter pattern. Different OIDC artifacts have different characteristics, and Porta uses the optimal storage for each:

Redis Adapters (Ephemeral Data)

ModelTypical TTLWhy Redis?
Session14 daysFast read/write, auto-expiry, no query needed
Interaction10 minutesVery short-lived, high throughput
AuthorizationCode10 minutesOne-time use, fast lookup
ReplayDetection1 hourPrevents token replay, needs fast exists-check
ClientCredentials10 minutesShort-lived, high throughput
PushedAuthorizationRequest1 minuteVery short-lived

PostgreSQL Adapters (Durable Data)

ModelTypical TTLWhy PostgreSQL?
AccessToken1 hourMust survive Redis restart, queryable for revocation
RefreshToken14 daysLong-lived, must be revocable by grant
Grant14 daysTracks user consent, revocation cascades to tokens
DeviceCode10 minutesNeeds reliable persistence during device flow

Adapter Factory

The adapter factory (adapter-factory.ts) transparently routes each model to the correct storage backend:

Model request → Adapter Factory → Redis Adapter (ephemeral)
                                → PostgreSQL Adapter (durable)

This is invisible to node-oidc-provider — it simply uses the adapter interface and Porta handles the routing.


Request Flow

A complete request through Porta passes through several layers:

Incoming HTTP Request

    ├── Root Page Handler (/, /robots.txt, /favicon.ico)

    ├── Health Check (/health)

    ├── Admin API (/api/admin/*)
    │   ├── Admin Auth Middleware (JWT Bearer, ES256)
    │   └── Route Handlers → Services → Repositories → DB

    ├── Interaction Routes (/interaction/*)
    │   ├── CSRF Middleware
    │   ├── Rate Limiter
    │   ├── Template Engine → Handlebars
    │   └── Login / Consent / 2FA / Magic Link handlers

    └── OIDC Provider (/{orgSlug}/*)
        ├── Tenant Resolver (org lookup + cache)
        ├── Client Secret Hash Middleware
        ├── OIDC CORS Handler
        └── node-oidc-provider (auth, token, userinfo, jwks, etc.)

Middleware Stack

Porta's Koa middleware stack is ordered carefully:

  1. Error handler — Global error catching and formatting
  2. Request logger — Logs all requests with X-Request-Id
  3. Root page — Neutral response for /, /robots.txt, /favicon.ico
  4. Health checkGET /health with DB + Redis checks
  5. Admin auth — JWT Bearer authentication for /api/admin/*
  6. Admin routes — REST API handlers
  7. Interaction routes — Login, consent, 2FA, magic link, password reset
  8. Tenant resolver — Org resolution for /{orgSlug}/*
  9. Client secret hash — SHA-256 pre-hash for client_secret_post
  10. OIDC CORS — CORS headers for OIDC endpoints
  11. OIDC Provider — node-oidc-provider mounted at /{orgSlug}

Technology Choices

DecisionChoiceRationale
Web frameworkKoaRequired by node-oidc-provider (not Express)
OIDC enginenode-oidc-provider 9.xOpenID Certified, actively maintained, extensible
Token signingES256 (ECDSA P-256)Modern, compact signatures, recommended for OIDC
Password hashingArgon2idWinner of the Password Hashing Competition, memory-hard
Template engineHandlebarsLogic-less, secure (auto-escaping), easy for non-developers
DatabasePostgreSQL 16Reliable, feature-rich, JSONB for metadata
CacheRedis 7Fast key-value store, built-in TTL, pub/sub potential
LanguageTypeScript (strict)Type safety, better IDE support, catch errors at compile time
Config validationZodRuntime validation with TypeScript type inference

Further Reading

Released under the MIT License.