Single Sign-On (OIDC, LDAP & GitHub) ​
Phlix can authenticate users against an external identity provider instead of (or alongside) local Phlix accounts:
- OIDC — any OpenID Connect provider (Authentik, Keycloak, Google, Okta, Azure AD, etc.) using the authorization-code flow.
- LDAP — a corporate directory (OpenLDAP, Active Directory, FreeIPA, …).
- GitHub — a GitHub OAuth App (plain OAuth 2.0, not OIDC).
All three are off by default. Each is enabled independently and only becomes live once it has been configured.
Before you configure OIDC or GitHub: set PHLIX_DOMAIN
Both browser flows send an absolute redirect_uri to the provider, and Phlix will only build one from a request whose Host matches the server's configured public authority (PHLIX_DOMAIN). If neither PHLIX_DOMAIN nor an absolute per-provider redirect_uri is set, sign-in fails closed with 503 callback_url_not_configured. See Callback URLs and PHLIX_DOMAIN — read it before you register anything at your IdP.
Where config lives
Provider settings (issuer, client ID/secret, LDAP host/base-DN, GitHub client id/secret, …) are stored in the database — one row per provider in the plugin_settings table (migration 093_plugin_settings.sql), not in a settings.json file. Nothing to do on upgrade: an existing settings.json is imported once, automatically, the first time the provider is read. Secrets are write-only in every case. The setting keys below are the stable contract.
Enabling a provider ​
There are two pieces of state:
- The enable flag — a server setting:
auth.oidc.enabled,auth.ldap.enabledorauth.github.enabled. - The provider configuration — the issuer / client / directory details.
A provider only goes live when it is both enabled and configured.
To turn one on:
- Open the admin console → Integrations → Auth providers (
/admin/integrations#auth-providers). - Expand the provider (OIDC or LDAP) and fill in its configuration form (see Provider configuration). Save.
- Flip the provider's Enable toggle.
GitHub has no admin-console card yet
The GitHub provider ships with admin API endpoints but no form in the admin console. Configure and enable it with the two calls in GitHub settings; everything else on this page (login flow, linking, security posture) applies to it identically.
Enabling a configured provider returns 200 and the provider becomes live immediately — you do not need to restart the server. (The worker that served the request registers it at once; every other worker registers it on the request path the first time it is needed, and on its next start/reload.)
If you toggle Enable on a provider that has not been configured yet, the request is rejected with 409 not_configured and a message telling you to configure it first — the "Enabled" badge always means "provider live", never just "setting saved".
Provider configuration ​
Configuration is edited under Integrations → Auth providers and saved via the admin API (POST /api/v1/admin/auth-providers/oidc/config, .../ldap/config, .../github/config). Secrets are write-only — they are never returned by the read endpoint, and leaving a secret field blank keeps the stored value.
Partial saves: OIDC and GitHub keep what you do not send
Every save endpoint replaces the whole stored settings document. The OIDC and GitHub endpoints therefore treat an optional key that is absent from your request body as "leave it alone" and re-emit the stored value; only an explicitly empty value clears one. So you can POST just the fields you want to change, and — importantly — saving from the admin console's OIDC form (which has no redirect_uri field) does not erase a redirect_uri you set through the API. To clear a field on purpose, send it as "".
The LDAP endpoint is not like that yet: apart from bind_pw (blank keeps the stored password) it rebuilds the document from the request body alone, so an omitted port, ssl, bind_dn, user_filter or admin_group is reset to its default (389, false, "", (uid=), "") rather than preserved. The admin console's LDAP form always sends all of them, so it is safe; if you script this endpoint, always send the complete settings map — read it back with GET .../ldap/config first.
OIDC settings ​
| Key | Required | Notes |
|---|---|---|
provider_url | yes | The OIDC issuer URL (its /.well-known/openid-configuration is discovered from this). Must be https:// (or http://localhost for development). |
client_id | yes | The client ID registered with your IdP. |
client_secret | yes | The client secret (write-only). |
scopes | no | Space-separated scopes. Default: openid profile email. |
redirect_uri | no | An absolute http(s) callback URL, e.g. https://media.example.com/auth/oidc/callback. Leave empty to derive it from the request host (requires PHLIX_DOMAIN — see below). A relative path is rejected with 400 invalid_redirect_uri. |
Redirect / callback URL to register at your IdP: register
https://<your PHLIX_DOMAIN>/auth/oidc/callbackas an allowed redirect URI for the Phlix client at your identity provider. This is where Phlix asks the IdP to return the browser after the user authenticates, and it must match exactly — scheme, host, port and path. See Callback URLs and PHLIX_DOMAIN.
GitHub settings ​
Create an OAuth App (not a GitHub App) under GitHub → Settings → Developer settings → OAuth Apps, with:
- Homepage URL —
https://<your PHLIX_DOMAIN> - Authorization callback URL —
https://<your PHLIX_DOMAIN>/auth/github/callback
Then copy the client ID and generate a client secret.
| Key | Required | Notes |
|---|---|---|
client_id | yes | The OAuth App client ID. |
client_secret | yes | The OAuth App client secret (write-only). A GitHub OAuth App is a confidential client, so both id and secret are required before the provider can go live. |
scopes | no | Space-separated scopes. Default: read:user user:email. |
redirect_uri | no | An absolute http(s) callback URL — must equal the OAuth App's Authorization callback URL. Leave empty to derive it from the request host (requires PHLIX_DOMAIN). |
There is no admin-console form yet, so save and enable it over the API (any admin bearer token):
# 1. Save the configuration.
curl -sS -X POST https://media.example.com/api/v1/admin/auth-providers/github/config \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"client_id":"Iv1.abc123","client_secret":"…","scopes":"read:user user:email"}'
# → {"message":"Settings saved successfully","configured":true}
# 2. Bring it live (no restart needed).
curl -sS -X POST https://media.example.com/api/v1/admin/auth-providers/github/enable \
-H "Authorization: Bearer $ADMIN_TOKEN"
# → 200 {"name":"github","enabled":true,"live":true,"message":"…"}
# → 409 not_configured if step 1 is incompleteRead the current values back with GET /api/v1/admin/auth-providers/github/config — it returns {client_id, scopes, redirect_uri, configured} and never the secret. configured: true means id and secret are stored, which is exactly the bar enable uses, so a true here can never be followed by a 409 not_configured.
Users then sign in at GET /auth/github/authorize?redirect_uri=/app (the redirect_uri query parameter is the in-app page to land on afterwards and is required — see How users log in), or link GitHub to an existing account with GET /auth/identities/link/github (see Account linking).
GitHub accounts are matched on the numeric id, never the e-mail
The linked identity is github.<numeric user id> — GitHub's account id is stable, while the login name can be changed. Phlix deliberately does not match a GitHub login onto an existing local account by e-mail address (a GitHub e-mail is not necessarily verified, so that would be an account-takeover primitive). If a local account already owns the e-mail or the username, the GitHub login stops with error=email_already_registered and the user must sign in to that account and link GitHub through GET /auth/identities/link/github.
LDAP settings ​
| Key | Required | Default | Notes |
|---|---|---|---|
host | yes | — | LDAP server hostname. |
port | no | 389 | 636 is typical for LDAPS. |
ssl | no | false | Use SSL/LDAPS. |
base_dn | yes | — | Search base for user lookups. |
bind_dn | no | — | Service-account DN used to search (leave blank for anonymous bind). |
bind_pw | no | — | Service-account password (write-only). |
user_filter | no | (uid=) | is substituted with the submitted username. For Active Directory use e.g. (sAMAccountName=). |
admin_group | no | — | Members of this group DN are granted admin. |
The LDAP form also has a Test connection button (POST /api/v1/admin/auth-providers/ldap/test) that dry-runs a bind with the current form values before you save.
Callback URLs and PHLIX_DOMAIN ​
This section applies to the two browser flows (OIDC and GitHub). LDAP has no callback and is unaffected.
Both providers require the redirect_uri Phlix sends them to be an absolute URL that matches what you registered, character for character — scheme, host, port and path. Phlix resolves it in two steps, in this order:
- The provider's
redirect_urisetting, if you set one to a valid absolutehttp(s)URL. This always wins and works regardless ofPHLIX_DOMAIN. - Derived from the request as
<scheme>://<Host><callback path>— but only when the request'sHostmatchesPHLIX_DOMAIN.
If neither yields a URL, the authorize endpoint fails closed.
What PHLIX_DOMAIN is ​
PHLIX_DOMAIN is this server's public authority: a host, optionally with a port — media.example.com or media.example.com:8443. No scheme, no path, no trailing dot. scripts/install.sh --domain <domain> writes it into the environment file the systemd unit reads, so a box installed with --domain already has it. It is the same value the hub integration turns into hub.domain / hub.public_url.
It is used here as an allowlist, because Host is supplied by the client. Left open, an attacker could ask an unconfigured Phlix for an authorize URL carrying their own host as the redirect_uri, phish that URL to one of your users, and — against an IdP that accepts wildcard redirect registrations — receive that user's authorization code. So a Host is only turned into a redirect_uri when it is your configured authority.
The port is part of the origin, with the scheme's default port normalised away on both sides:
PHLIX_DOMAIN | Request Host | Scheme | Result |
|---|---|---|---|
media.example.com | media.example.com | https | derives https://media.example.com/auth/…/callback |
media.example.com | media.example.com:443 | https | same — :443 is the default for https and is normalised away |
media.example.com | media.example.com:8096 | https | refused (503) — a different origin |
media.example.com | 192.168.1.10 | https | refused (503) |
media.example.com:8443 | media.example.com:8443 | https | derives https://media.example.com:8443/auth/…/callback |
media.example.com:8443 | media.example.com | https | refused (503) |
| unset or malformed | anything | any | refused (503) — nothing is derived |
A malformed value is treated as unconfigured, never as an allowlist that happens to match nothing: https://media.example.com/, media.example.com/app, media.example.com:, media.example.com., media.example.com:99999, media.example.com:https and a blank/whitespace value all mean "no derivation", so the outcome is the actionable 503 rather than a silently broken login.
Reaching Phlix directly on :8096 fails closed by design
phlix-server also listens on its own port (8096 by default), so a request that bypasses your reverse proxy arrives with Host: media.example.com:8096 — a different origin, so it gets the 503. Nothing that could have worked is lost: the callback you registered with GitHub / your IdP is the proxied https://… form, so the provider would have rejected the :8096 value with redirect_uri_mismatch anyway. The failure just happens earlier, on your server, where the log says what to fix. If you really do serve sign-in on that port, set the provider's absolute redirect_uri to it and register that with the provider.
When it is misconfigured ​
GET /auth/oidc/authorize / GET /auth/github/authorize answer:
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
{"error":"callback_url_not_configured",
"message":"Sign-in with GitHub is not fully configured on this server. Ask the administrator to check the server log."}- No
Locationheader — the browser is never sent to the provider. - No session or correlation cookie, and no OAuth state row is created, so nothing is left half-started and there is nothing to resume or replay.
- The body is deliberately generic. These routes are unauthenticated, so the response does not tell an anonymous visitor what is unset.
error: callback_url_not_configuredis the stable machine-readable code. - The detail is in the log. One
warningon theAUTHchannel per refusal, carrying which of the two conditions fired (PHLIX_DOMAINnot set, versus the requestHostis notPHLIX_DOMAIN— the port is compared too), the presentedHost, the configured domain, and both remedies.
If the provider is not enabled and configured at all, you get 503 provider_not_configured instead — that check runs first, so this callback_url_not_configured only ever appears to someone who is actively setting a provider up.
Because /auth/{provider}/authorize is a top-level browser navigation, the user sees that JSON in the tab. That is the same behaviour as the other authorize-time errors.
The escape hatch, and when to use it ​
Set the provider's redirect_uri setting to the absolute URL you registered with the IdP / OAuth App. It takes priority over everything above and works with PHLIX_DOMAIN unset. Use it when:
- you serve sign-in on a hostname or port that is not
PHLIX_DOMAIN(a second vanity domain, or Phlix's own:8096listener); - you cannot set
PHLIX_DOMAIN(for example a container image where the value is not plumbed through); - your proxy presents a
Hostyou do not control.
curl -sS -X POST https://media.example.com/api/v1/admin/auth-providers/oidc/config \
-H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"provider_url":"https://idp.example.com","client_id":"phlix",
"redirect_uri":"https://media.example.com/auth/oidc/callback"}'Otherwise prefer PHLIX_DOMAIN: it is one value for every provider, and it is what makes the derived URL safe.
Upgrading an existing OIDC install
Before this change both browser flows sent a path-only redirect_uri (/auth/oidc/callback), which any spec-strict IdP rejects with redirect_uri_mismatch — so for most installs OIDC login could not complete anyway and there is nothing to regress. The exception is an IdP that resolves a relative redirect against the client's root URL (Keycloak does): such an install was working and now needs PHLIX_DOMAIN set to the hostname users browse, or an absolute redirect_uri. Boxes installed with scripts/install.sh --domain already have PHLIX_DOMAIN.
One narrow in-flight window on the upgrade itself: a login started before the deploy, on an origin the new rule refuses, ends in the 503; the retry succeeds. It is bounded by the 600-second OAuth state TTL.
How users log in ​
OIDC ​
- The client sends the browser to
GET /auth/oidc/authorize?redirect_uri=<same-origin path>. Theredirect_urihere is the in-app page Phlix returns the user to after login — it must be a same-origin relative path (see Security posture). - Phlix redirects the browser to your IdP with a PKCE challenge plus a
stateandnonce. - The user authenticates at the IdP, which redirects back to
GET /auth/oidc/callback. - Phlix validates the returned id-token (signature,
iss,aud,exp), consumes the one-timestate, mints a Phlix session, and302s the browser to theredirect_urifrom step 1 — now authenticated.
GitHub ​
Identical in shape to OIDC, minus the OIDC-specific parts (there is no discovery document and no id_token):
- The client sends the browser to
GET /auth/github/authorize?redirect_uri=<same-origin path>. Theredirect_uriquery parameter is required and must be a same-origin relative path (400 missing_redirect_uri/400 invalid_redirect_uriotherwise); it is the in-app page to land on after login, and is unrelated to theredirect_urisetting that GitHub sees. - Phlix redirects to
https://github.com/login/oauth/authorizewith the configured scopes, a PKCE challenge and a one-shotstate, and sets the browser-binding cookie. - The user authorises the OAuth App, and GitHub redirects back to
GET /auth/github/callback. - Phlix consumes the
state, checks the browser-binding cookie, exchanges the code athttps://github.com/login/oauth/access_token(replaying the same absoluteredirect_uriit sent in step 2), then reads the profile fromGET https://api.github.com/user. - The account is resolved by
github.<numeric id>— created on first login, matched to your existing account if you had linked it — and Phlix mints a session and302s to theredirect_urifrom step 1.
If the user keeps their e-mail private, Phlix makes a best-effort follow-up to GET /user/emails (needs the user:email scope) and, failing that, still creates the account with a deterministic placeholder e-mail.
All GitHub endpoints are hardcoded — provider settings only supply the client id/secret/scopes — so there is no operator- or user-influenced URL to be coerced into an SSRF.
LDAP ​
LDAP login rides the normal login endpoint. Send POST /auth/login with an ldap:-prefixed username plus the user's directory password:
{ "username": "ldap:jdoe", "password": "…" }Phlix strips the ldap: prefix, binds against the configured directory, and — on success — mints a session exactly like a local login. A user without the ldap: prefix is always treated as a local account.
Account linking ​
An already-signed-in account can attach one or more external identities to itself, and list what is linked. All the endpoints below are authenticated — the current user is read from the validated session (Bearer token / session cookie), never from the request body — and none of them create a new account or mint a new session.
Linking is a full round-trip
You can link an external identity, list what is linked, unlink one you no longer want, and log in via a linked identity. A freshly-linked identity resolves to your existing account on the next OIDC / LDAP / GitHub login — it no longer creates a duplicate. Multiple instances of the same provider (e.g. two OIDC issuers) are supported at the platform level; an admin UI for configuring named instances is a later step.
The security guarantee: verified links only ​
Linking always proves you actually control the external identity — a client-claimed identifier is never trusted:
- OIDC is verified by a full round-trip to your IdP (authorization-code + PKCE flow and id-token validation, exactly like login). The linked identity is the IdP-verified subject (
sub); anyexternal_idin the request is ignored. - GitHub is verified by the same OAuth round-trip (authorization-code + PKCE, then the profile fetch). The linked identity is the GitHub-verified
github.<numeric id>, never a client-supplied value. - LDAP is verified by a live bind with the supplied credentials. Only a successful bind links the directory identity; the linked identity is the directory's own DN, not anything from the request.
Because the OIDC and GitHub link flows carry the initiating user in server-side state (not a client-visible parameter), an attacker cannot forge or swap the target account — you can only ever link an identity you can authenticate as, onto your own account. Linking never changes your primary login and never issues a new session.
List your linked identities ​
GET /auth/identities (authenticated)Returns the identities linked to the current user:
{
"identities": [
{
"id": "…",
"provider": "oidc",
"provider_instance": "",
"external_id": "oidc.<sub>",
"linked_at": "2026-07-24 00:00:00"
}
]
}provider_instance is "" for the default single instance. Provider secrets and internal provider_data are never returned.
Link an OIDC identity ​
GET /auth/identities/link/oidc (authenticated)Starts the OIDC authorization-code + PKCE flow with a server-side link intent. The browser is redirected to your IdP; after you authenticate, the existing GET /auth/oidc/callback attaches the IdP-verified sub to your account and 302s back to the same-origin app with a ?linked=oidc marker. No login session is minted — you stay logged in as yourself.
Link a GitHub identity ​
GET /auth/identities/link/github (authenticated)The GitHub equivalent, with the same server-side link intent. The browser goes to GitHub; on return, GET /auth/github/callback attaches the verified github.<numeric id> to your account and 302s back with a ?linked=github marker. redirect_uri is optional here and defaults to /app. No login session is minted, and no session cookies are set — the link branch is bound and gated by the same browser-binding cookie as login, and it never mints tokens.
Link an LDAP identity ​
POST /auth/identities/link/ldap (authenticated)
Body: { "username": "jdoe", "password": "…" }Performs a live LDAP bind with the supplied credentials. On success the directory identity is linked and the endpoint returns 200{ success, provider, created, message } (created:false means it was already linked to you — see idempotency below). A failed bind links nothing and returns a generic 401 "Invalid credentials" (no account-enumeration oracle); a genuine directory/config failure returns 503.
Unlink an identity ​
DELETE /auth/identities/{id} (authenticated)Removes one external identity from your own account. The {id} is the id field from List your linked identities. On success it returns 200 { success, message }. Your local password and every other linked identity are left untouched.
Two guards protect you:
- Own-identity-only. You can only unlink an identity that belongs to your account. The target is resolved within your own identity list (keyed on your authenticated session, never on the request body), so an
idthat belongs to another account — or that does not exist — returns an indistinguishable404 identity_not_found, never a cross-account removal. - Never your last sign-in method. If removing the identity would leave your account with no local password and no other linked identity, the request is refused with
409 last_sign_in_methodso you can't lock yourself out. Set a local password, or keep at least one other identity, first.
Logging in with a linked identity ​
Once you've linked an identity you can log in with it directly. The next time you authenticate through that provider — the OIDC or GitHub flow, or an ldap: login (see How users log in) — Phlix resolves the identity to your existing account instead of creating a new one: the login read path looks the identity up in the user_identities store first. No extra step is needed; linking and logging-in target the same account.
Existing and legacy external users keep logging in exactly as before.
Conflicts and idempotency ​
- Already linked to another account →
409 identity_already_linked. An external identity can belong to only one Phlix account; Phlix never silently re-homes it. - Already linked to your account → idempotent success (LDAP returns
created:false; OIDC and GitHub redirect back with?linked=oidc/?linked=github). Linking twice is safe.
The user_identities table's UNIQUE index is the race backstop, so two concurrent link attempts resolve to exactly one row (a genuine server error is surfaced as a 5xx, never mislabeled as a 409).
Security posture ​
- Session cookies, not URL tokens. A successful OIDC or GitHub login delivers the session as httpOnly + Secure + SameSite=Lax cookies (
phlix_sessionandphlix_refresh). Tokens are never placed in the URL or query string, so they can't leak via browser history, referrers, or access logs. (Secureis applied when the request is served over HTTPS.) - Same-origin redirect only. The post-login landing page passed as the
redirect_uriquery parameter is allowlisted to a same-origin relative path. Absolute URLs, protocol-relative//host, back-slash tricks (/\host),javascript:, and any control/CRLF characters are rejected with400 invalid_redirect_uri. This prevents an attacker from phishing the flow and having a victim's freshly-minted session redirected to a foreign origin. It is re-validated on the callback leg, not just at authorize. - The OAuth
stateis bound to the browser that started the flow. At authorize Phlix sets a 32-byte HttpOnly + Secure + SameSite=Lax cookie (phlix_oauth_oidc/phlix_oauth_github,Max-Age600 to match the state TTL) and persists only its SHA-256 alongside the server-side state. The callback compares them withhash_equals()before any token exchange, account link or cookie mint, and answers403on a mismatch. Without this, someone could run the authorize leg themselves and then have a victim's browser deliver the callback, handing the victim a session for the attacker's account —SameSite=Laxdoes not restrictSet-Cookie. The cookie is expired on every response after the state is consumed. One consequence: two sign-in flows started in the same browser at once conflict, and the older tab's callback fails with that403; retrying it works. stateis server-side and one-shot. The PKCEcode_verifierand the authorize-timeredirect_urilive in theoauth_state_storetable (600 s TTL), never in$_SESSION— which under Workerman is process-global and shared between concurrent requests. The consume is aSELECT+DELETEinside a transaction. For GitHub theSELECTtakes aFOR UPDATErow lock, so two concurrent callbacks carrying the samestatecannot both be served. The OIDC store does not take that lock yet, so its one-shot property rests on theDELETEalone and two callbacks racing on the samestatecan both read it. In both cases a replayedstatestill has to carry the matching browser-binding cookie described above.- The
redirect_urisent to the provider cannot disagree between legs. The absolute value resolved at authorize is stored in the state and replayed verbatim at token exchange (and re-validated on the way out), so the two legs match structurally rather than by coincidence. - Brute-force rate limiting.
ldap:logins share the same per-IP rate-limit budget as local logins. Exceeding it returns429with aRetry-After. The OIDC/GitHub authorize and callback routes are not rate-limited. - No account enumeration. A wrong LDAP username or password returns a generic
401 "Invalid credentials". Only a genuine directory misconfiguration or connection failure returns503(so operators can tell a config problem from a bad password without leaking which usernames exist). - Provider error text is never reflected. Errors handed back to the app come from a fixed internal set (e.g.
provider_error,internal,email_already_registered); a provider's ownerror_descriptionis never echoed into the redirect. - Distinct identities per provider. External identities are stored with their real provider (
oidc/ldap/github) and resolved on login by(provider, provider_instance, external_id)via theuser_identitiesstore (with a legacyusersfallback), so the samesub/DN/id presented by two different providers — or by two instances of the same provider — maps to distinct users, never a silent account merge.
Operational notes ​
- Migration 091 applies automatically. External (passwordless) users are created with a null
password_hash; migration091_users_password_hash_nullable.sqlrelaxes that column and is applied by the migration runner on upgrade — no manual step. Users whose IdP supplies no email/username are still created (Phlix assigns a deterministic placeholder derived from the provider + external ID). - Migration 092 applies automatically.
092_user_identities.sqladds auser_identitiesjoin table — the home for multiple external identities per account (account-linking and multi-instance providers) — and backfills a row for every existing external-identity user, deriving the real provider (oidc/ldap) and de-duplicating any legacy duplicates. It is applied by the migration runner on upgrade — no manual step. As of S47 the login read path resolves the owning account through this table first (falling back tousers.provider/users.external_idfor any un-backfilled row), which is what makes a linked identity usable for login; existing and legacy external users log in unchanged. - Multiple providers of the same family. The auth provider registry can hold more than one instance of the same family (e.g. two OIDC issuers) without a name collision — the platform foundation for configuring multiple OIDC/OAuth providers. Each identity records which instance it belongs to (
user_identities.provider_instance,''for the default single instance). Configuring/seeding additional named instances from the admin console is a later step. - Migration 093 applies automatically.
093_plugin_settings.sqladds theplugin_settingstable (one row per provider:plugin_nameprimary key +settings_json), the new home for provider configuration. It is applied by the migration runner on upgrade — no manual step — and is idempotent, so re-running it is safe and never touches existing rows. Nothing needs exporting or importing: the first time a provider's settings are read, an existingsettings.jsonis imported into the table automatically, once. - Provider configuration is shared across workers; the enable flag is a server setting. phlix-server runs several resident workers, each with its own in-memory provider registry, which is why the config had to leave the filesystem. Saving settings or toggling a provider takes effect without a restart: the worker that served the request updates immediately, and every other worker notices on its next sign-in request by comparing a fingerprint of the persisted settings. Enable state stays in server settings (
auth.oidc.enabled,auth.ldap.enabled,auth.github.enabled). - Partial settings saves are safe for OIDC and GitHub. Those endpoints preserve any optional key that was absent from the request body and clear only what you send as empty, so a client (or an admin form) that does not know about a field cannot delete it. The LDAP endpoint still expects the complete map — see the note under Provider configuration.
- Non-blocking OAuth/OIDC I/O. OIDC discovery, token exchange, userinfo and JWKS fetches — and the GitHub token exchange and profile fetch — use a non-blocking HTTP client with TLS verification on, so they don't stall the worker. LDAP binds remain a bounded (5 s) blocking call — the
ext-ldapextension has no Swoole-hookable async client. - Where to look when sign-in fails. The
AUTHlog channel carries awarningfor every refusal, including the callback-URL failures described in Callback URLs andPHLIX_DOMAIN(with the presentedHost, the configured domain and the remedy) andstate/ browser-binding mismatches. Client-supplied values in those log fields are sanitised and length-capped.
Admin API reference ​
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/admin/auth-providers | List providers |
POST | /api/v1/admin/auth-providers/{name}/enable | Enable (200 live / 409 not_configured / 404 unknown_provider) |
POST | /api/v1/admin/auth-providers/{name}/disable | Disable |
GET/POST | /api/v1/admin/auth-providers/oidc/config | Read / save OIDC config (provider_url, client_id, client_secret write-only, scopes, redirect_uri) |
GET/POST | /api/v1/admin/auth-providers/ldap/config | Read / save LDAP config |
POST | /api/v1/admin/auth-providers/ldap/test | Dry-run an LDAP bind |
GET/POST | /api/v1/admin/auth-providers/github/config | Read / save GitHub config (client_id, client_secret write-only, scopes, redirect_uri) |
GET | /api/v1/admin/auth-providers/{oidc,ldap,github}/schema | The provider's JSON settings schema (for form rendering) |
GET | /auth/oidc/authorize | Start the OIDC flow (unauthenticated); ?redirect_uri=<same-origin path> |
GET | /auth/oidc/callback | OIDC callback from the IdP (unauthenticated; also handles the account-link branch) |
GET | /auth/github/authorize | Start the GitHub flow (unauthenticated); ?redirect_uri=<same-origin path> required |
GET | /auth/github/callback | GitHub callback (unauthenticated; also handles the account-link branch) |
POST | /auth/login | Local login, or LDAP with an ldap:-prefixed username |
GET | /auth/identities | List the current user's linked identities (authenticated) |
GET | /auth/identities/link/oidc | Start linking an OIDC identity to the current account (authenticated) |
GET | /auth/identities/link/github | Start linking a GitHub identity to the current account (authenticated) |
POST | /auth/identities/link/ldap | Link an LDAP identity via a live bind (authenticated); body {username, password} |
DELETE | /auth/identities/{id} | Unlink one of the current user's identities (authenticated); 404 if not yours, 409 if it's your last sign-in method |
Both authorize endpoints can answer 503 provider_not_configured (the provider is not enabled + configured) or 503 callback_url_not_configured (no absolute callback URL resolves — see Callback URLs and PHLIX_DOMAIN).
The admin config UI and its endpoints are also covered in Integrations → Auth providers, and PHLIX_DOMAIN in Reference → Environment variables.