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:
@@ -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};
|
||||
@@ -36,13 +36,14 @@ function createAuthRequest(){
|
||||
return {state, codeVerifier, codeChallenge};
|
||||
}
|
||||
|
||||
// Build the SSO authorize URL the browser is redirected to.
|
||||
function buildAuthUrl(state, codeChallenge){
|
||||
// Build the SSO authorize URL the browser is redirected to. `redirectUri`
|
||||
// overrides conf.oidc.redirectUri (per-host SSO uses a per-host callback).
|
||||
function buildAuthUrl(state, codeChallenge, redirectUri){
|
||||
let o = conf.oidc;
|
||||
let params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: o.clientId,
|
||||
redirect_uri: o.redirectUri,
|
||||
redirect_uri: redirectUri || o.redirectUri,
|
||||
scope: (o.scopes || ['openid', 'profile', 'email', 'groups']).join(' '),
|
||||
state,
|
||||
code_challenge: codeChallenge,
|
||||
@@ -51,13 +52,14 @@ function buildAuthUrl(state, codeChallenge){
|
||||
return `${o.authorizationEndpoint}?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Exchange an authorization code for tokens at the token endpoint.
|
||||
async function exchangeCode(code, codeVerifier){
|
||||
// Exchange an authorization code for tokens at the token endpoint. `redirectUri`
|
||||
// must match the one used in buildAuthUrl (per-host for per-host SSO).
|
||||
async function exchangeCode(code, codeVerifier, redirectUri){
|
||||
let o = conf.oidc;
|
||||
let body = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: o.redirectUri,
|
||||
redirect_uri: redirectUri || o.redirectUri,
|
||||
client_id: o.clientId,
|
||||
client_secret: o.clientSecret,
|
||||
code_verifier: codeVerifier,
|
||||
|
||||
Reference in New Issue
Block a user