Skip to content

Generic OpenID Connect SSO, multi-domain hosting, and social-login hardening - #1005

Open
SoFMeRight wants to merge 2 commits into
LinkStackOrg:betafrom
HomeLabHD:oidc-sso
Open

SoFMeRight wants to merge 2 commits into
LinkStackOrg:betafrom
HomeLabHD:oidc-sso

Conversation

@SoFMeRight

@SoFMeRight SoFMeRight commented Sep 15, 2026

Copy link
Copy Markdown

Closes #918.

Adds a generic OpenID Connect provider so an instance can sign in against any
OIDC issuer — Keycloak included, by pointing OIDC_ISSUER at the realm URL
(https://<host>/realms/<realm>) and letting discovery supply the endpoints.
As #918 notes, Socialite has no generic OIDC driver of its own; this uses the
community socialiteproviders/openidconnect package, which is discovery-based
rather than per-vendor, so Keycloak, Zitadel, Authentik, Entra, Auth0, Okta and
Google are all the same configuration with a different issuer.

It also lets one instance serve several domains, and fixes several issues in the
existing social login along the way.

Every new setting defaults to current behaviour — an instance that sets none of
them behaves exactly as it does today.

Social login fixes (relevant even without OIDC)

These affect existing Google / Facebook / Twitter / GitHub users, so they may be
worth taking regardless of the rest:

  • Identity matched on email alone. SocialLoginController looked the user up
    by email, so an email reassigned at the provider could take over an existing
    account. It now matches on the provider's immutable subject id, and only falls
    back to email — which must not be asserted unverified — to bridge or provision.
  • Provisioned users were incomplete. No password hash, no role, no block
    flag, and a littlelink_name taken straight from the provider without the
    uniqueness or charset rules registration enforces. They now match the shape of
    a locally-registered account.
  • FORCE_HTTPS / FORCE_ROUTE_HTTPS were silently dead under config:cache.
    They were read with env() outside config/, which returns null once the
    config is cached. Moved into config/app.php; values and defaults unchanged.

OpenID Connect

Uses socialiteproviders/openidconnect, configured by discovery from
OIDC_ISSUER. Two things were needed to make a community Socialite driver work
here: the SocialiteWasCalled listener the package registers through (without it
every request fails with Driver [openidconnect] not supported), and dropping
Laravel\Socialite\SocialiteServiceProvider from config/app.php so the
Manager's provider — a superset that adds the extendSocialite hook — binds
instead. Built-in providers are unaffected either way.

Claim mapping is configurable, because providers disagree about which claim
carries which field and the driver maps neither preferred_username nor
picture at all. Without it an avatar is never stored, and the profile slug
silently degrades to the email local part on Zitadel, Keycloak and Authentik.

Multi-domain

With no redirect configured, the OAuth callback is derived from the request
host, so one instance can serve several apexes and return each visitor to the
domain they arrived on. ALLOWED_HOSTS is the Host allow-list that keeps that
derivation safe; left unset, behaviour is unchanged. Setting OIDC_REDIRECT_URI
pins a single redirect as before.

Commits

  1. OIDC sign-in and multi-domain hosting — the above.
  2. Group/domain authorizationOIDC_ALLOWED_GROUPS, OIDC_ALLOWED_DOMAINS,
    OIDC_ADMIN_GROUP. Purely additive (100 insertions, 0 deletions), so it can
    be dropped without touching the first commit if you'd rather not take role
    mapping from an IdP.

Notes for review

  • Translations are unverified. or and Sign in with :provider are added to
    all 14 locales. The or values match the first word of each locale's existing
    "or sign in with other accounts?" string, so those are corroborated; the
    Sign in with :provider strings are mine and only checked for consistency with
    each file's own phrasing. Worth a native speaker's eye.
  • composer.lock adds only socialiteproviders/manager and
    socialiteproviders/openidconnect — no version bumps, no removals.
  • Happy to split this up. The config:cache fixes, the host/frame settings,
    and the social-login hardening are all separable from OIDC if you'd prefer them
    as their own PRs.
  • The sign-in divider renders only when a named provider button is shown, and the
    "other accounts" heading now only renders when the provider icon row is
    non-empty — previously it could introduce an empty row.

Adds a generic OpenID Connect provider so an instance can sign in against
any OIDC issuer, and lets one instance serve several domains by deriving
the OAuth callback from the request host.

OpenID Connect
  socialiteproviders/openidconnect, driver "openidconnect", configured by
  discovery from OIDC_ISSUER. EventServiceProvider gains the
  SocialiteWasCalled mapping the package registers its driver through --
  without it Socialite reports "Driver [openidconnect] not supported"
  however complete the configuration is. config/app.php stops listing
  Laravel's SocialiteServiceProvider so the Manager's provider (a superset
  adding the extendSocialite hook) binds instead; the built-in Google,
  Facebook, Twitter and GitHub providers are unaffected.

Claim mapping
  Providers disagree over which claim carries which field, so the username,
  display name, email and picture are each read from a configurable,
  ordered list of claims, defaulting to the standard OIDC claims. This is
  not cosmetic: the driver maps neither preferred_username nor picture, so
  without it an avatar is never stored at all and the profile slug quietly
  degrades to the email local part on Zitadel, Keycloak and Authentik.

Multi-domain
  SocialLoginController derives the callback from the current request when
  no redirect is configured, so each visitor returns to the domain they
  arrived on. ALLOWED_HOSTS is the Host allow-list that keeps that
  derivation safe; left unset the previous behaviour is unchanged.

Social login hardening
  Identity now matches on the provider's immutable subject id rather than
  the email alone, an email is required and must not be asserted
  unverified before it bridges to an existing account, and provisioned
  users get the role, block flag, hashed password and unique
  littlelink_name a locally-registered account has. Provisioning and
  account linking are each separately refusable.

Also
  ALLOWED_FRAME_ORIGINS writes a CSP frame-ancestors directive (empty
  sends none, as before) and SESSION_SAME_SITE becomes settable, which is
  what embedding across sites needs. FORCE_HTTPS and FORCE_ROUTE_HTTPS
  move to config/app.php so `config:cache` no longer silently disables
  them. OIDC_AUTO_LAUNCH sends the login page straight to the provider,
  with ?local=1 always still reaching the form. RP-initiated logout ends
  the provider session on sign-out, and can be switched off.

Every new setting defaults to prior behaviour; an instance that sets none
of them is unchanged.
Lets the identity provider decide who may sign in and who administers the
instance. Neither was reachable before: every single sign-on user was
provisioned with the plain user role, and no group claim was ever read.

OIDC_ALLOWED_GROUPS and OIDC_ALLOWED_DOMAINS are allow-lists. Empty, which
is the default, places no restriction at all. Both are re-evaluated on
every sign-in rather than only at provisioning, so removing someone from a
group upstream ends their access here on their next attempt.

OIDC_ADMIN_GROUP mirrors the admin role onto a group. Unset, the role
stays entirely the instance's own business. Membership is re-read on each
sign-in so a grant can be withdrawn as well as given, with the first
account exempt so an instance can never be locked out of its own panel.

Group memberships are read from OIDC_GROUPS_CLAIM, accepting both a list
and a comma-separated string, since providers send both shapes.
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