Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,80 @@ PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

# Single sign-on (social login). Set to "true" to show the configured buttons on
# the login page. Individual providers appear only when their CLIENT_ID is set.
ENABLE_SOCIAL_LOGIN=false

# Generic OpenID Connect SSO. Works against any OIDC issuer via discovery
# ({OIDC_ISSUER}/.well-known/openid-configuration): Zitadel, Keycloak, Authentik,
# Entra, Auth0, Okta, Google, etc. The id_token is fully validated (signature via
# the issuer's JWKS with automatic key-rotation, iss, aud/azp, exp, nonce, at_hash)
# and PKCE is used. New users are provisioned from a provider-verified email.
# The callback is derived per request from the host being served, so one instance works
# across every apex/domain it answers on — register each apex's callback with the IdP:
# {each-apex}/social-auth/openidconnect/callback and {each-apex}/social-auth/logout/callback
# Set OIDC_REDIRECT_URI below to pin a single redirect instead of deriving it.
OIDC_ISSUER=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=

# Shown on the sign-in button, as "Sign in with {OIDC_DISPLAY_NAME}".
OIDC_DISPLAY_NAME="OpenID Connect"

# Icon on that button: any Bootstrap Icons class, e.g. bi-key or bi-building.
# Set it empty for a button with no icon at all.
# OIDC_ICON=bi-shield-lock

# Connection overrides:
# OIDC_SCOPES="openid profile email"
# OIDC_REDIRECT_URI= # pin one redirect instead of deriving it per host
# OIDC_POST_LOGOUT_REDIRECT_URI= # where the IdP returns after ending its session

# Claim mapping. Providers disagree over which claim carries which field, so each takes a
# comma-separated list and the first claim actually released wins. The defaults are the
# standard OIDC claims; override only for a provider that differs.
# OIDC_EMAIL_CLAIM=email
# OIDC_USERNAME_CLAIM=preferred_username,nickname
# OIDC_NAME_CLAIM=name
# OIDC_PICTURE_CLAIM=picture
# OIDC_GROUPS_CLAIM=groups

# Provisioning. Defaults match how social login behaved before: an unknown user is created,
# a known email is adopted, and an address the IdP flags unverified is refused.
# OIDC_AUTO_REGISTER=true # false: only users who already exist may sign in
# OIDC_LINK_EXISTING_USER=true # false: refuse rather than adopt a matching local account
# OIDC_REQUIRE_VERIFIED_EMAIL=true # reject sign-in when the IdP flags email unverified
# OIDC_REQUIRE_EMAIL=false # fail the login when no email is released at all
# OIDC_UPDATE_PROFILE_ON_LOGIN=false # true: the provider's name/picture overwrite local edits

# Authorization. Empty places no restriction; naming a list makes it the allow-list, and it
# is re-checked on every sign-in so access can be withdrawn at the provider.
# OIDC_ALLOWED_GROUPS= # e.g. linkstack-users,staff
# OIDC_ALLOWED_DOMAINS= # e.g. example.com,example.org
# OIDC_ADMIN_GROUP= # members get the admin role; unset leaves roles alone

# Sign-in flow:
# OIDC_AUTO_LAUNCH=false # true: /login goes straight to the provider.
# # /login?local=1 still reaches the form
# OIDC_IDP_LOGOUT=false # true: signing out also ends the session at the
# # provider, and so in every other application using it

# Host header allow-list. Empty trusts the APP_URL domain and its subdomains, which is
# right for a single-domain instance. Serving several apexes from one instance requires
# listing them here — the OIDC callback is derived from the incoming host, so an
# unlisted host is rejected rather than trusted.
# ALLOWED_HOSTS=bio.example.com,bio.example.org
ALLOWED_HOSTS=

# Who may embed this instance in a frame, written into the CSP frame-ancestors directive.
# Empty sends no directive and embedding is unrestricted. Entries are origins, since
# frame-ancestors matches on origin and ignores any path; 'self' is always included.
# ALLOWED_FRAME_ORIGINS=https://apps.example.com
ALLOWED_FRAME_ORIGINS=

# Session cookie SameSite policy. Embedding this instance in a frame on another site
# needs "none" (which also requires a secure cookie), otherwise the browser withholds
# the session and the framed page renders logged out. Leave as lax unless embedding.
# SESSION_SAME_SITE=lax | none | strict
SESSION_SAME_SITE=lax
24 changes: 24 additions & 0 deletions app/Http/Controllers/Auth/AuthenticatedSessionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ public function create(Request $request)
return redirect('/update?finishing');
}

// Straight to the provider when this instance signs in only through it, so no one is
// shown a form nobody uses. ?local=1 still reaches the form, so local accounts — and
// an instance whose provider is down — always keep a way in.
if (! $request->has('local')
&& config('services.openidconnect.auto_launch')
&& ! empty(config('services.openidconnect.client_id'))) {
return redirect()->route('social.redirect', 'openidconnect');
}

return view('auth.login');
}

Expand Down Expand Up @@ -129,6 +138,21 @@ private function getStoredSecurityKey(): ?array
*/
public function destroy(Request $request)
{
$oidc = $request->session()->get('oidc_logout');

// OIDC session: RP-initiated logout. Hand off to the IdP's end_session_endpoint
// (logout() mints a state and keeps the session for it); the local logout is
// completed in the post-logout callback after that state is validated. Local and
// non-OIDC social sessions fall straight through to the plain logout below.
if (is_array($oidc) && ! empty($oidc['provider'])) {
try {
return \Socialite::driver($oidc['provider'])
->logout($oidc['id_token'] ?? null, route('social.logout.callback'));
} catch (\Throwable $e) {
// IdP advertises no end_session_endpoint (or driver gone) — logout locally.
}
}

Auth::guard('web')->logout();

$request->session()->invalidate();
Expand Down
Loading