feat(auth): OIDC / SSO single sign-on (v0.17.0-beta)

Add OpenID Connect single sign-on so the platform can authenticate against
any standards-compliant provider (Entra ID, Okta, Google Workspace, Keycloak,
Authentik, …) alongside local accounts. Resolves the feature request in #73.

Flow (backend-driven Authorization Code + PKCE):
- GET /api/auth/oidc/login builds the authorize URL and stores the PKCE/nonce
  transaction in Redis (single-use, 10m TTL); the callback validates the ID
  token via openid-client (JWKS signature, iss/aud/exp/nonce), resolves the
  identity to a local user, and mints the platform's normal session JWT —
  handed to the SPA via URL fragment. No sessions/cookies introduced.

Identity mapping (users table gains email/oidc_subject/oidc_issuer/
auth_provider; password_hash now nullable; partial unique indexes):
- First sign-in auto-provisions a user, or links to an existing local account
  when the IdP email is verified and matches.
- Roles derive from a configurable IdP group -> role map with a default
  fallback; existing users are never auto-demoted on a groupless login.

Configuration (admin-only, no env/redeploy needed):
- Stored as an app_settings row with the client secret encrypted at rest
  (utils/crypto); masked in responses and excluded from GET /api/settings.
- New UI: Settings -> SSO / OIDC panel (issuer/client, claim + group->role
  mapping, provisioning toggles, discovery test, copyable redirect URI), a
  login-page SSO button, and an SSO badge in Users & Roles.

Security: local break-glass login preserved; PKCE + single-use state; email
linking requires email_verified; same-site-only post-login redirect.

Adds openid-client@^5 (CommonJS). 17 new unit tests (claim/role mapping,
username derivation, config secret round-trip/masking); 114 backend tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2GT-Rich
2026-07-16 18:54:36 -07:00
co-authored by Claude Opus 4.8
parent ea1ad956ff
commit c0f230bdf2
21 changed files with 1056 additions and 14 deletions
+63 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mikrotik-manager-backend",
"version": "0.16.6-beta",
"version": "0.16.12-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mikrotik-manager-backend",
"version": "0.16.6-beta",
"version": "0.16.12-beta",
"dependencies": {
"@influxdata/influxdb-client": "^1.33.2",
"@influxdata/influxdb-client-apis": "^1.35.0",
@@ -22,6 +22,7 @@
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"nodemailer": "^9.0.1",
"openid-client": "^5.7.1",
"otpauth": "^9.5.1",
"pg": "^8.21.0",
"qrcode": "^1.5.4",
@@ -5865,6 +5866,15 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jose": {
"version": "4.15.9",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6517,6 +6527,15 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -6529,6 +6548,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/oidc-token-hash": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz",
"integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==",
"license": "MIT",
"engines": {
"node": "^10.13.0 || >=12.0.0"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -6576,6 +6604,39 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openid-client": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
"license": "MIT",
"dependencies": {
"jose": "^4.15.9",
"lru-cache": "^6.0.0",
"object-hash": "^2.2.0",
"oidc-token-hash": "^5.0.3"
},
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/openid-client/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/openid-client/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",