Skip to content

feat(auth): Add OIDC/OAuth2 login with admin + self-service tiers - #6192

Open
CRASH-Tech wants to merge 5 commits into
MHSanaei:mainfrom
CRASH-Tech:add-oauth2
Open

feat(auth): Add OIDC/OAuth2 login with admin + self-service tiers#6192
CRASH-Tech wants to merge 5 commits into
MHSanaei:mainfrom
CRASH-Tech:add-oauth2

Conversation

@CRASH-Tech

@CRASH-Tech CRASH-Tech commented Aug 9, 2026

Copy link
Copy Markdown

feat(auth): OIDC/OAuth2 login with admin + self-service tiers

Summary

Adds single sign-on through any OpenID Connect provider (Keycloak, Authentik,
Authelia, Google, Azure AD, Okta, …). It can be configured declaratively via
XUI_OAUTH_* environment variables
, from the panel settings UI, or both —
env values take precedence and lock the matching UI fields read-only.

A group claim in the ID token maps each caller to one of two tiers:

  • Admin (member of the admin group) → full admin panel, bound to the existing
    first user (no per-admin data, so no new rows / migration).
  • User (member of a user group) → a client is auto-provisioned into the
    inbound(s) whose remark matches the configured list, and the user lands in a
    self-service cabinet showing only their own connection: subscription URL,
    share links with protocol/transport badges, QR codes, and traffic/expiry.

How it works

  • Authorization-Code flow with PKCE + nonce + state; the ID token signature,
    audience and nonce are verified before a session is opened.
  • Login reuses the existing cookie session; the role and (for the user tier) the
    client subId live in the session — no DB schema change / migration.
  • Hybrid config with env precedence. Effective settings merge XUI_OAUTH_*
    (priority) over stored values. An env-pinned field is shown read-only in the UI
    with its live value; the client secret is never echoed (only a "configured"
    flag). Server-side, env-locked fields cannot be changed through the API. The
    enable toggle is auto-on and locked when the issuer + client id come from env.
  • Provisioning reuses ClientService.Create (runtime-safe, the LDAP-sync path),
    so multi-node deployments are handled. It is idempotent and reconciling: on
    later logins an existing client is attached to any newly matching inbound (same
    subId/limits, no reset).
  • A background sync job (every 5 min) does the same for already-logged-in
    users, so a new matching inbound reaches them without a re-login. It only
    attaches — never deletes or disables.

Configuration

Every knob is available as a XUI_OAUTH_* env var and as a panel setting:

Setting Env var Notes
Enable (auto) Auto-enabled when issuer + client id come from env
Issuer / Client ID / Client secret XUI_OAUTH_ISSUER / _CLIENT_ID / _CLIENT_SECRET Secret is write-only in the UI
Redirect URL XUI_OAUTH_REDIRECT_URL Derived from the request when empty
Admin group XUI_OAUTH_ADMIN_GROUP Full panel access
User group(s) XUI_OAUTH_USER_GROUP Self-service cabinet; comma-separated
User inbound remark(s) XUI_OAUTH_USER_INBOUND_REMARK Provisioning targets; comma-separated
User limits XUI_OAUTH_USER_TOTAL_GB / _EXPIRY_DAYS / _LIMIT_IP 0 = unlimited/never
Scopes / Groups claim / Username claim XUI_OAUTH_SCOPES / _GROUPS_CLAIM / _USERNAME_CLAIM Defaults: openid,profile,email,groups / groups / email

Changes

  • Backend: internal/config/oauth.go; internal/util/oauth/ (go-oidc/oauth2
    wrapper: discovery, auth URL, code exchange, claim extraction); routes
    GET /oauth/login, /oauth/callback, /getOAuthEnable, /cabinet/,
    /cabinet/data; session role/subid/flow helpers; OAuthProvisionService
    (provision + reconcile) and OAuthSyncJob; role gating in checkLogin.
    Settings: Oauth* fields on AllSetting, HasOauthClientSecret +
    OauthEnvLocked on AllSettingView, GetEffectiveOAuthConfig /
    OAuthEnvLocks / OAuthEnabledEffective / enforceOauthEnvLocks. The OIDC
    provider is cached by a config signature so UI edits apply without a restart.
  • Frontend: SSO button on the login page (shown when SSO is active); new
    cabinet bundle reusing the subscription page's link/QR components; an "OIDC"
    section in Settings mirroring LDAP, with env-pinned fields disabled.
  • Deps: github.com/coreos/go-oidc/v3, golang.org/x/oauth2.
  • Docs/config: i18n keys across all 13 locales; new routes in endpoints.ts
    • regenerated OpenAPI; XUI_OAUTH_* documented in all 7 README translations;
      a commented example in docker-compose.yml.

Testing

  • Go: config parsing; the OIDC flow against an httptest fake IdP (discovery +
    JWKS + signed ID token, incl. nonce/audience rejection); role mapping;
    provisioning (create/reuse, multi-remark, attach-on-new-inbound); batch
    reconcile; effective-config env precedence, env locks, enable resolution and
    the server-side lock enforcement; a regression test for callback host
    resolution (see notes).
  • Frontend: typecheck, lint, build, i18n dead-key test, storybook build.
  • go build ./..., golangci-lint (0 issues), gofumpt, route-registry contract
    — all green.

Notes

  • Fixes a cabinet-link bug where the raw Host header (with port) produced a
    malformed @[host:port]:443 URL; the callback now uses the sub server's own
    host resolution.
  • QR is intentionally hidden for post-quantum (mlkem/pqv) links across the panel,
    SubPage and cabinet — such links are too large to scan; use the subscription QR.
  • The sync job treats every client on a remark-matched inbound as part of the
    user pool (no schema marker), so a manually-added client on such an inbound is
    also mirrored across the pool. A Group/Comment marker could scope this more
    strictly if desired.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant