mirror of https://github.com/wg-easy/wg-easy
Browse Source
Add an optional trusted-header authentication path so an authenticated upstream reverse proxy (Authentik forward-auth outpost) can assert the caller identity and wg-easy establishes a session for the matching user, auto-provisioning federated accounts. - server/middleware/trustedAuth.ts: trust gate (feature flag + source-IP allowlist) -> read identity header -> getOrCreateExternal -> mint session - server/utils/config.ts: TRUSTED_PROXY_* env (off by default) - user/service.ts: getOrCreateExternal() for password-less federated users - user/schema.ts: password nullable + auth_provider column - password.ts: isPasswordValid null-safe (federated users fail password/Basic) Migration for the schema change generated separately via drizzle-kit. Authored By: Culpur Defense Inc.pull/2693/head
5 changed files with 187 additions and 2 deletions
@ -0,0 +1,81 @@ |
|||
/* |
|||
* Trusted-header SSO (Culpur Defense fork). |
|||
* |
|||
* Runs before route handlers. When TRUSTED_PROXY_ENABLED is set AND the request's |
|||
* real socket peer is in TRUSTED_PROXY_IPS, this reads the identity header that a |
|||
* trusted upstream (an Authentik forward-auth outpost) has asserted, resolves or |
|||
* auto-provisions the matching wg-easy user, and establishes the normal wg-easy |
|||
* session. Everything downstream (getCurrentUser -> definePermissionEventHandler |
|||
* -> RBAC) then works unchanged, exactly as if the user had logged in with a form. |
|||
* |
|||
* SECURITY MODEL — why this is not an auth bypass: |
|||
* 1. Off by default. Stock wg-easy never runs this branch. |
|||
* 2. Source-IP allowlist. The identity header is honoured ONLY when the request |
|||
* arrives from the trusted proxy. We read the real connection peer via |
|||
* getRequestIP(event, { xForwardedFor: false }) — NOT X-Forwarded-For, which |
|||
* a client could spoof. In our topology the outpost also strips any |
|||
* client-supplied X-authentik-* headers and sets its own, so the header cannot |
|||
* arrive pre-set from a browser. |
|||
* |
|||
* If the feature is disabled, the source is not allowlisted, or no header is |
|||
* present, this middleware does nothing and the request falls through to the |
|||
* existing password/session auth untouched. |
|||
*/ |
|||
export default defineEventHandler(async (event) => { |
|||
if (!WG_ENV.TRUSTED_PROXY_ENABLED) { |
|||
return; |
|||
} |
|||
|
|||
// Only act on API calls and page loads; leave setup/i18n alone (mirrors setup.ts).
|
|||
const url = getRequestURL(event); |
|||
if (url.pathname.startsWith('/_i18n/') || url.pathname.startsWith('/setup/')) { |
|||
return; |
|||
} |
|||
|
|||
// 1. Trust gate — the real socket peer must be an allowlisted proxy.
|
|||
// xForwardedFor:false makes h3 return the connection remoteAddress, so a
|
|||
// spoofed X-Forwarded-For cannot satisfy this check.
|
|||
const peer = getRequestIP(event, { xForwardedFor: false }); |
|||
if (!peer || !WG_ENV.TRUSTED_PROXY_IPS.includes(peer)) { |
|||
return; |
|||
} |
|||
|
|||
// 2. Read the identity the upstream verified.
|
|||
const username = getHeader(event, WG_ENV.TRUSTED_PROXY_HEADER)?.trim(); |
|||
if (!username) { |
|||
return; |
|||
} |
|||
|
|||
// If a valid session already exists for this same user, don't re-mint it.
|
|||
const existingSession = await getWGSession(event); |
|||
if (existingSession.data.userId) { |
|||
const current = await Database.users.get(existingSession.data.userId); |
|||
if (current && current.username === username) { |
|||
return; |
|||
} |
|||
} |
|||
|
|||
// 3. Resolve or provision the federated user, then establish the session.
|
|||
const email = getHeader(event, WG_ENV.TRUSTED_PROXY_EMAIL_HEADER)?.trim(); |
|||
const name = getHeader(event, WG_ENV.TRUSTED_PROXY_NAME_HEADER)?.trim(); |
|||
|
|||
const user = await Database.users.getOrCreateExternal(username, { |
|||
email: email || null, |
|||
name: name || username, |
|||
defaultRole: |
|||
WG_ENV.TRUSTED_PROXY_DEFAULT_ROLE === 'admin' ? roles.ADMIN : roles.CLIENT, |
|||
}); |
|||
|
|||
if (!user.enabled) { |
|||
// A locally-disabled federated user is blocked here; the normal disabled-user
|
|||
// 403 in getCurrentUser also covers this once a session exists.
|
|||
return; |
|||
} |
|||
|
|||
const session = await useWGSession(event); |
|||
await session.update({ userId: user.id }); |
|||
|
|||
SERVER_DEBUG( |
|||
`Trusted-header SSO: session established for ${user.id} (${user.username}) from ${peer}` |
|||
); |
|||
}); |
|||
Loading…
Reference in new issue