ed5fc5ad5c
Authorization-code + PKCE against a single configurable IdP via
openid-client v5, with JIT provisioning. Verified end-to-end against a
real Keycloak 26 (realm + confidential client + verified-email user):
settings → discovery test → login button → Keycloak → dashboard.
Backend:
- migration 162: admin_users.auth_provider ('local' default) +
external_subject, composite unique index
- oidcService: settings-driven config (client secret AES-256-GCM at
rest, mfaService pattern, OIDC_ENCRYPTION_KEY fallback JWT_SECRET),
cached discovery, sub-based identity binding — email linking of
existing admins only with email_verified=true; JIT behind
oidc_autoprovision with configurable default role and an unusable
random password hash
- GET /api/auth/admin/sso/login + /callback: state/nonce/PKCE verifier
cross the redirect in a 10-min signed httpOnly SameSite=Lax cookie;
the callback reuses the local login's session establishment
(completeAdminLogin split into establishAdminSession + JSON wrapper)
so SSO sessions are identical downstream; every failure lands on
/admin/login?sso_error=<key> as a translated toast
- dedicated /admin/settings/sso GET/PUT/test endpoints (secret
write-only, redacted to a set-flag; registered ABOVE the generic
/:type matcher which would shadow them); oidc_client_secret added to
the reserved keys stripped from generic settings upserts
- public settings expose only oidc_enabled + oidc_button_label for the
login page
Frontend:
- Settings → Single Sign-On (OIDC) tab: issuer/client/secret, scopes,
autoprovision + default role, button label, enable toggle, redirect
URI copy box, server-side discovery test
- login page: SSO button (custom label) when enabled; sso_error query
param surfaced as translated toasts; EN+DE i18n
Tests: 11 integration cases against an in-process mock IdP (real
discovery/JWKS/PKCE/ID-token validation) — JIT on/off, sub-vs-email
binding, unverified-email rejection, deactivated admin, missing/forged
state cookie, nonce tamper, secret encryption round-trip, disabled 404.
MFA is delegated to the IdP on the SSO path; local login stays
available as break-glass. Role-claim mapping and logout-to-IdP follow
in phase 2/3.
95 lines
2.5 KiB
JSON
95 lines
2.5 KiB
JSON
{
|
|
"name": "picpeak-backend",
|
|
"version": "3.88.0-beta.0",
|
|
"description": "Backend for PicPeak event photo sharing platform",
|
|
"main": "server.js",
|
|
"scripts": {
|
|
"start": "node server.js",
|
|
"dev": "nodemon server.js",
|
|
"migrate": "node migrations/run-migrations.js",
|
|
"migrate:safe": "node migrations/run-migrations-safe.js",
|
|
"generate:watermarks": "node scripts/generate-watermarks.js",
|
|
"test": "jest",
|
|
"test:s3": "SKIP_S3_TESTS=false jest __tests__/integration/backup-s3",
|
|
"lint": "eslint src/"
|
|
},
|
|
"dependencies": {
|
|
"@aws-sdk/client-s3": "^3.850.0",
|
|
"@aws-sdk/lib-storage": "^3.850.0",
|
|
"@aws-sdk/s3-request-presigner": "^3.850.0",
|
|
"archiver": "^5.3.1",
|
|
"axios": "1.16.0",
|
|
"bcrypt": "6.0.0",
|
|
"chokidar": "4.0.3",
|
|
"cookie-parser": "^1.4.7",
|
|
"cors": "^2.8.5",
|
|
"dotenv": "^16.0.3",
|
|
"exifr": "^7.1.3",
|
|
"express": "^4.18.2",
|
|
"express-rate-limit": "^6.7.0",
|
|
"express-validator": "^7.0.1",
|
|
"fluent-ffmpeg": "^2.1.3",
|
|
"form-data": "4.0.6",
|
|
"helmet": "^7.0.0",
|
|
"i18next": "25.3.2",
|
|
"i18next-browser-languagedetector": "^8.2.0",
|
|
"i18next-http-backend": "3.0.5",
|
|
"imapflow": "^1.4.0",
|
|
"ipaddr.js": "^2.3.0",
|
|
"joi": "^17.13.4",
|
|
"js-yaml": "^4.2.0",
|
|
"jsonwebtoken": "^9.0.0",
|
|
"knex": "^2.4.2",
|
|
"mailparser": "^3.9.9",
|
|
"mime-types": "^3.0.1",
|
|
"multer": "2.2.0",
|
|
"node-cron": "^3.0.2",
|
|
"node-stream-zip": "^1.15.0",
|
|
"nodemailer": "^9.0.1",
|
|
"openid-client": "^5.7.1",
|
|
"otplib": "^12.0.1",
|
|
"pdf-lib": "^1.17.1",
|
|
"pdfkit": "^0.17.2",
|
|
"pg": "^8.16.3",
|
|
"postcss": "8.5.10",
|
|
"qrcode": "^1.5.4",
|
|
"react-i18next": "^15.6.0",
|
|
"sanitize-html": "^2.17.0",
|
|
"sharp": "0.34.3",
|
|
"sqlite3": "^5.1.6",
|
|
"swagger-jsdoc": "^6.2.8",
|
|
"swagger-ui-express": "^5.0.1",
|
|
"swissqrbill": "^4.3.0",
|
|
"tar": ">=7.5.16",
|
|
"uuid": "^11.1.1",
|
|
"winston": "^3.8.2",
|
|
"zxcvbn": "^4.4.2"
|
|
},
|
|
"devDependencies": {
|
|
"eslint": "^8.40.0",
|
|
"jest": "^29.5.0",
|
|
"mock-fs": "^5.5.0",
|
|
"nodemon": "^3.1.10",
|
|
"supertest": "^6.3.3"
|
|
},
|
|
"overrides": {
|
|
"prebuild-install": {
|
|
"tar-fs": "2.1.4"
|
|
},
|
|
"glob": "^11.1.0",
|
|
"js-yaml": "^4.2.0",
|
|
"fast-xml-parser": ">=5.7.0",
|
|
"qs": ">=6.15.2",
|
|
"tar": ">=7.5.16",
|
|
"brace-expansion": ">=5.0.6",
|
|
"minimatch": ">=9.0.7",
|
|
"path-to-regexp": "0.1.13",
|
|
"lodash": ">=4.18.1",
|
|
"follow-redirects": ">=1.16.0",
|
|
"@tootallnate/once": ">=3.0.1",
|
|
"ip-address": ">=10.1.1",
|
|
"uuid": "^11.1.1",
|
|
"nodemailer": "^9.0.1"
|
|
}
|
|
}
|