QuantumID 2026 Updates
This page consolidates the QuantumID changes shipped during 2026. Most items are also reflected in the affected feature pages (MFA, Federation, Branding, Applications) — but if you are upgrading from an early-2026 build, read this end-to-end first.
MFA method settings — per tenant + per application
MFA methods (TOTP, Email OTP, SMS OTP) are now configured at two levels. The tenant-level default lives in TenantSettings.MfaMethods. Each Application can override the tenant default with its own ApplicationSettings.MfaMethods — the override replaces the tenant default in full (no field-level merging) so it is always obvious from a single read which methods are active for a given login flow.
{
"applicationId": "app_abc123",
"mfaMethods": {
"totp": { "enabled": true, "required": false },
"emailOtp": { "enabled": true, "required": false },
"smsOtp": { "enabled": false, "required": false }
}
}
# Tenant default lives in TenantSettings.MfaMethods. An application that
# sets mfaMethods overrides the tenant default in full (no merging).Split-screen login pages
Both the tenant portal sign-in and the QuantumID end-user sign-in were redesigned as split-screen layouts: the form is on the left, a brandable hero panel on the right (see Branding below). The two surfaces share the same panel definition so a partner-portal sign-in and the customer-portal sign-in both pick up the same imagery without duplicating configuration.
Where you see it
Tenant portal sign-in (https://portal.quantumapi.eu) and end-user OIDC sign-in (https://id.<your-domain>) both use the split-screen layout from 0.13.0-beta onwards.
Login right-panel branding
Each Application can attach a hero image, title, and explanatory text to its sign-in page. Limits per locale: title ≤ 500 characters, text ≤ 1000 characters. A live preview is available in the Backoffice editor; every change goes to the audit log with the editor identity.
From 0.20.0-rc.9 the panel is per-locale: each Application stores translations for EN/ES/FR/DE/IT, with a separate title field. The tenant-level Branding Center exposes the same image+title+text as the fallback used by every application that does not set its own panel.
{
"applicationId": "app_abc123",
"branding": {
"loginPanel": {
"imageUrl": "https://cdn.example.com/login-hero.png",
"translations": {
"en": { "title": "Welcome to Acme", "text": "Secure access for our customer portal." },
"es": { "title": "Bienvenido a Acme", "text": "Acceso seguro al portal de clientes." },
"fr": { "title": "Bienvenue chez Acme", "text": "Accès sécurisé au portail clients." },
"de": { "title": "Willkommen bei Acme", "text": "Sicherer Zugang zum Kundenportal." },
"it": { "title": "Benvenuto in Acme", "text": "Accesso sicuro al portale clienti." }
}
}
}
}
# Limits per locale: title <= 500 chars, text <= 1000 chars.
# Tenant-level branding (Settings -> Branding Center) provides the fallback
# image+title+text used by every application that does not set its own.
# Every change is recorded in the audit log with the editor identity.Identity Content-Security-Policy widening
Identity's CSP was widened in 0.20.0-rc.9 to allow Geist to load from cdn.jsdelivr.net (style + font) and to allow `https:` in img-src so per-application login images can come from any HTTPS CDN. The API and Gateway CSPs are unchanged. `frame-ancestors 'none'` is preserved on Identity to keep clickjacking protection intact.
Content-Security-Policy:
default-src 'self';
style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
font-src 'self' https://cdn.jsdelivr.net; /* Geist */
img-src 'self' data: blob: https:; /* per-app login image */
script-src 'self';
frame-ancestors 'none';
form-action 'self';
# API and Gateway CSPs are unchanged. frame-ancestors 'none' is preserved
# to keep clickjacking protection across the surface.SAML IdP certificate validation (#607)
When an external SAML IdP is configured (Federation), the IdP signing certificate is now validated server-side at registration time. Three rejection conditions:
- Format check — accepts both Base64 (X.509 PEM body without headers) and full PEM (with `-----BEGIN CERTIFICATE-----` headers). Anything else returns 400 `invalid_certificate`.
- Expiry check — `notAfter` must be in the future. Expired certificates return 400 `certificate_expired`.
- Not-yet-valid check — `notBefore` must be in the past. Future-dated certificates return 400 `certificate_not_yet_valid`.
App creator auto-added as end-user
From 0.18.0-rc.1, when a tenant team member registers a new OIDC Application, the team member's email is automatically added to that Application's end-user list. This unblocks the most common test path ("create app, sign in to it as myself") without the previous manual step. Audit log entry is `endusers.create.auto_from_app_creator`.
OIDC discovery + JWKS CORS, dynamic localhost allowlist
Two CORS-related fixes shipped in 0.19.1-rc.2. First, the public OIDC discovery endpoints (`/.well-known/openid-configuration` and `/.well-known/jwks.json`) return `Access-Control-Allow-Origin: *` per the OpenID Connect spec — so any SPA can discover the issuer and validate JWTs without a server-side proxy.
Second, the per-tenant CORS allowlist is rebuilt from the registered redirect URIs of every Application in the tenant (cached in-memory, 5-minute TTL). Any `http(s)://localhost(:port)` and `http(s)://127.0.0.1(:port)` origin is allowed in every environment so local-first development works against any tenant without ad-hoc allowlist edits.
# /.well-known/openid-configuration -> Access-Control-Allow-Origin: *
# /.well-known/jwks.json -> Access-Control-Allow-Origin: *
# (browsers can fetch discovery + JWKS from any SPA)
# Other endpoints check the dynamic allowlist:
# * Every applicationRedirectUri origin registered for the tenant
# * Every postLogoutRedirectUri origin registered for the tenant
# * Any http(s)://localhost(:port) — allowed in all environments
# * Any http(s)://127.0.0.1(:port) — allowed in all environments
#
# The allowlist is rebuilt in-memory on application save and TTL'd to 5 minutes.Password-reset link includes userId
Every reset entry point now embeds `userId` in the reset URL — including the self-service flow (0.20.0-rc.4) and the end-user API flows `send-password-reset` and `create-without-password` (0.20.0-rc.5). This eliminates a class of "reset succeeds for the wrong account" errors that previously could occur when two accounts shared the same email at different tenants.
https://id.<your-domain>/reset-password
?userId=usr_xyz789
&applicationId=app_abc123
&token=<single-use, 1-hour TTL>
# Entry points that emit this link (all include userId since 0.20.0-rc.5):
# - Self-service POST /forgot-password
# - POST /api/v1/endusers/{id}/send-password-reset
# - POST /api/v1/endusers/create-without-password (initial setup link)RequireMfa policies — rejected on write, Forbid on read (#675)
Step-up MFA enforcement is on the roadmap but not yet shipped. Until then, attempts to create or update an access policy with `action: "RequireMfa"` are rejected at the API with 400 `action_not_supported`. Existing rows that already carry the action are evaluated as `Forbid` at runtime — they are not silently allowed.
POST /api/v1/access-policies
{
"name": "RequireMfa for production keys",
"action": "RequireMfa", /* not currently supported */
...
}
-> 400 Bad Request
{ "error": "action_not_supported", "supportedActions": ["Allow", "Deny", "AllowWithApproval"] }
# Existing rows whose action == "RequireMfa" are evaluated as "Forbid"
# until step-up MFA ships. They are NOT silently allowed.Accept-Invitation page UX
The Accept-Invitation surface (the page invitees land on) was rewritten in 0.20.0-rc.10:
- Primary CTA is now "Create account" (was "Accept invitation") — clearer about what is about to happen.
- Explanatory copy on what an end-user account is and what permissions the inviter granted.
- The invitation email body now mentions password setup explicitly so users do not assume an existing password works.
- Email body partial translations for FR/DE/IT (full ES already shipped); EN remains the source of truth.