Per-host SSO: Node auth endpoints + Redis session (#57)

Adds the /__proxy_auth OIDC flow served on every proxied host:
- routes/host_auth.js: /start (PKCE+state, per-host redirect_uri), /callback
  (exchange, enforce the host allow-list via utils/host_sso.identityAllowed,
  mint session + set __proxy_sso cookie), /logout.
- models/sso_session.js: SsoSession (Redis-backed, TTL'd; read directly by the
  Lua gate) and HostSsoState (in-flight auth request).
- utils/oidc.js: per-host redirect_uri override on buildAuthUrl/exchangeCode.
- conf.hostSso (reuses conf.oidc). Allow-list logic unit-tested.

Enforcement (Lua gate + nginx location) lands next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 15:41:28 -04:00
parent c98214cc76
commit 87c0d024d5
9 changed files with 301 additions and 10 deletions
+35
View File
@@ -0,0 +1,35 @@
'use strict';
/**
* Pure authorization check for per-host SSO (#57).
*
* After the OIDC dance, the callback decides whether the authenticated identity
* may access the host, based on the host's allow-lists. Enforcing here (at
* session creation) keeps the OpenResty side simple — the Lua gate only has to
* confirm a valid session exists for the host.
*
* Semantics: empty allow-lists mean "any authenticated user". Otherwise the
* identity is allowed if its username OR email is in sso_allow_users, or any of
* its groups is in sso_allow_groups. All comparisons are case-insensitive.
*/
function identityAllowed(identity, allowUsers, allowGroups){
identity = identity || {};
allowUsers = Array.isArray(allowUsers) ? allowUsers : [];
allowGroups = Array.isArray(allowGroups) ? allowGroups : [];
if(allowUsers.length === 0 && allowGroups.length === 0) return true;
let lc = s => String(s).trim().toLowerCase();
let ids = [identity.username, identity.email].filter(Boolean).map(lc);
let users = allowUsers.map(lc);
if(ids.some(id => users.includes(id))) return true;
let groups = (Array.isArray(identity.groups) ? identity.groups : []).map(lc);
let allow = allowGroups.map(lc);
if(groups.some(g => allow.includes(g))) return true;
return false;
}
module.exports = {identityAllowed};