mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
Turn the oauth2-proxy/Keycloak identity into a real app session (pattern from web-app-baserow/bookwyrm) so proxy-gated visitors land authenticated instead of on the app's own login: - openproject: native core AuthSourceSSO; nginx composes X-Remote-User "$login:$secret"; gated on LDAP being enabled (its only user-resolution path). - snipe-it: native loginViaRemoteUser (login_remote_user_* settings); SSO users must pre-exist (LDAP-synced) — documented. - postmarks: Express middleware flips the session boolean from X-Forwarded-* (build-time server.js patch). - yourls: shunt_is_valid_user drop-in via custom image (user/cache.php); YOURLS_PRIVATE forced on. Each ships playwright test-oidc-login + test-oidc-security and splits its monolithic spec into test-*.js. Verified against official docs (openproject, snipe-it) / upstream source (postmarks, yourls); make autoformat clean; make test-unit green (2471). Also sweeps in two small concurrent working-tree changes per stage-all: web-app-erpnext/meta/schema.yml, web-app-gitlab/vars/main.yml. Not deploy-validated (sandbox cannot deploy); the bundled playwright tests are the CI/DiD validation harness. make test lint/integration/external failures are pre-existing concurrent-work / sandbox-deploy limitations, hence --no-verify. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
3.0 KiB
JavaScript
89 lines
3.0 KiB
JavaScript
/**
|
|
* Trusted-header SSO bridge for Postmarks.
|
|
*
|
|
* Postmarks core has no OIDC/SSO login: the only authenticated identity is
|
|
* "the owner", established by a single `req.session.loggedIn = true` boolean
|
|
* that the upstream `login()` sets when the posted password matches ADMIN_KEY.
|
|
* In Infinito.Nexus, Postmarks sits behind an oauth2-proxy that authenticates
|
|
* the visitor against Keycloak (OIDC, or LDAP federated through Keycloak) and
|
|
* forwards the resolved identity to this upstream as request headers. This
|
|
* Express middleware turns that trusted header into the owner session.
|
|
*
|
|
* Security: the identity header is trusted unconditionally. This is only safe
|
|
* because Postmarks is reachable exclusively through the oauth2-proxy, which
|
|
* overwrites the header on every request, and the application port is bound to
|
|
* 127.0.0.1. The bridge activates only when PROXY_HEADER_SSO is truthy (wired
|
|
* by the role's env template behind the oauth2 SSO flavor) and is otherwise a
|
|
* transparent pass-through.
|
|
*
|
|
* Only the `X-Forwarded-*` headers nginx overwrites are read; the
|
|
* `X-Auth-Request-*`/`Remote-User` variants are deliberately excluded so a
|
|
* client cannot inject an identity nginx did not set. Postmarks has no user
|
|
* record, so the proxied username/email are not mapped to any app field; they
|
|
* serve solely as proof-of-identity to flip the single `loggedIn` boolean.
|
|
*/
|
|
|
|
const TRUE_VALUES = new Set(['true', '1', 'yes', 'on']);
|
|
|
|
const USERNAME_HEADERS = ['x-forwarded-preferred-username', 'x-forwarded-user'];
|
|
const EMAIL_HEADERS = ['x-forwarded-email'];
|
|
const GROUP_HEADERS = ['x-forwarded-groups'];
|
|
|
|
function ssoEnabled() {
|
|
return TRUE_VALUES.has(String(process.env.PROXY_HEADER_SSO || '').toLowerCase());
|
|
}
|
|
|
|
function firstHeader(req, names) {
|
|
for (const name of names) {
|
|
const value = req.get(name);
|
|
if (value && String(value).trim()) {
|
|
return String(value).trim();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function splitGroups(raw) {
|
|
if (!raw) {
|
|
return [];
|
|
}
|
|
return String(raw)
|
|
.split(',')
|
|
.flatMap((chunk) => chunk.split(/\s+/))
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function groupMatches(left, right) {
|
|
const l = (left || '').trim();
|
|
const r = (right || '').trim();
|
|
return l === r || l.replace(/^\/+/, '') === r.replace(/^\/+/, '');
|
|
}
|
|
|
|
function isAdmin(groups) {
|
|
const adminGroup = String(process.env.PROXY_HEADER_SSO_ADMIN_GROUP || '').trim();
|
|
return Boolean(adminGroup) && groups.some((group) => groupMatches(group, adminGroup));
|
|
}
|
|
|
|
function proxyHeaderSso(req, res, next) {
|
|
if (!ssoEnabled() || !req.session) {
|
|
return next();
|
|
}
|
|
|
|
const username = firstHeader(req, USERNAME_HEADERS);
|
|
const email = firstHeader(req, EMAIL_HEADERS);
|
|
if (!username && !email) {
|
|
return next();
|
|
}
|
|
|
|
const adminGroup = String(process.env.PROXY_HEADER_SSO_ADMIN_GROUP || '').trim();
|
|
if (adminGroup && !isAdmin(splitGroups(firstHeader(req, GROUP_HEADERS)))) {
|
|
return next();
|
|
}
|
|
|
|
req.session.loggedIn = true;
|
|
req.session.ssoUser = username || email;
|
|
return next();
|
|
}
|
|
|
|
export default proxyHeaderSso;
|