Generic OIDC single sign-on for solidtime,
applied as a minimal, rebasable patch on top of the upstream v0.20.0 release.
Works with any OpenID Connect provider. Validated against Keycloak federating Microsoft Entra ID. Not Google-only, and not a full fork of an old base.
Upstream solidtime has no SSO yet. The feature request (discussion #238) has been open since October 2024, and PR #1131 "Add core support for extendable authentication" is still open. The maintainer has indicated SSO may land on-premise first and could become a paid feature.
This repository exists so self-hosters can have OIDC today, with a patch small enough to rebase onto each new upstream release — and to throw away entirely once upstream ships it.
- Upstream: solidtime-io/solidtime — AGPL-3.0. All the actual product is theirs.
- OIDC implementation: adapted from intersecato/solidtime, which built the original generic OIDC support. This repository extracts only that work and rebases it onto the current upstream release.
Licensed AGPL-3.0, same as upstream.
Five new files plus small edits to eight existing ones:
| New | Purpose |
|---|---|
app/Service/Auth/OidcService.php |
The OIDC flow: authorization URL, callback, user resolution |
app/Http/Controllers/Auth/OidcController.php |
Two routes: redirect and callback |
app/Http/Middleware/PreventOidcProfilePhotoChanges.php |
Blocks local edits to OIDC-managed profile photos |
app/Extensions/Fortify/CustomLogoutResponse.php |
Federated (end-session) logout |
database/migrations/..._add_oidc_sub_to_users_table.php |
oidc_sub, nullable, UNIQUE |
tests/Feature/OidcAuthenticationTest.php |
Feature tests |
Edited: Kernel.php, User.php, FortifyServiceProvider.php, routes/web.php,
config/auth.php, config/services.php, composer.json, plus Login.vue and
AuthenticationCard.vue.
Dependency added: facile-it/php-openid-client.
The flow implements state (compared with hash_equals), nonce, and PKCE S256. The sub
claim from the ID token is cross-checked against the userinfo response. Users are matched by
oidc_sub first and only then by e-mail, and an account already bound to a different sub is
refused rather than re-linked.
Recommended hardening (see configuration below):
OIDC_SYNC_PROFILE_PHOTO=false— the profile-photo sync fetches the URL from thepictureclaim server-side, following redirects, with no scheme or address allowlist. On a container platform that is a blind SSRF vector. Disabling it removes the code path entirely.OIDC_REQUIRE_VERIFIED_EMAIL=true— without it, e-mail auto-linking can take over an existing account if your IdP does not guarantee verified addresses.OIDC_AUTO_REGISTER=false— otherwise anyone who can authenticate at your IdP gets an account, which silently overridesAPP_ENABLE_REGISTRATION.
Only the OIDC work was taken. MySQL support, billing toggles, invoicing, daily activities, custom login branding and the clients toggle were deliberately left out.
Four corrections were made while rebasing onto v0.20.0:
- Invitation matching. The original queried
lower(name)onorganization_invitations. That column does not exist upstream andemailisNOT NULL, so it would fail at runtime. Invitations are now matched by e-mail only. canRegister. The original used(bool) config('app.enable_registration'). Upstream stores that as a string (on/off/invite-only), and(bool) 'off'istruein PHP — the register link would appear with registration disabled. Now uses upstream'sRegistrationModeenum.loginViewlocation. The original registers it inJetstreamServiceProvider; upstreamv0.20.0registers it inFortifyServiceProvider. Adding a second registration would silently shadow the first. The existing one is modified instead.- Dead config removed. Login-logo settings were dropped along with the custom logo component, which is branding rather than OIDC.
OIDC_ENABLED=true
OIDC_ISSUER=https://idp.example.com/realms/your-realm
OIDC_CLIENT_ID=solidtime
OIDC_CLIENT_SECRET=...
OIDC_REDIRECT_URI=https://timetracker.example.com/auth/oidc/callback
OIDC_SCOPES="openid profile email"
OIDC_REQUIRE_VERIFIED_EMAIL=true
OIDC_SYNC_PROFILE_PHOTO=false
OIDC_AUTO_REGISTER=false
OIDC_AUTO_LINK=true
OIDC_LOGOUT_ENABLED=true
# Set to false only after verifying SSO works - it locks out password login.
AUTH_PASSWORD_LOGIN_ENABLED=trueRegister <APP_URL>/auth/oidc/callback as the redirect URI at your provider.
Same recipe as upstream's build-public.yml:
cp .env.production .env
composer install --no-dev --prefer-dist --ignore-platform-reqs --classmap-authoritative
npm ci && npm run build
docker build -f docker/prod/Dockerfile --build-arg DOCKER_FILES_BASE_PATH=docker/prod/ .The production Dockerfile expects an already-built tree — composer install and npm run build
are commented out inside it.
Note on flaky builds: upstream installs 20 PHP extensions in a single
RUN. Whenpecl.php.netthrottles, a random extension fails and the whole layer is rebuilt. Splitting thatRUNinto groups lets Docker cache the ones that succeeded. Not included here, since it is unrelated to OIDC.
git remote add upstream https://github.com/solidtime-io/solidtime.git
git fetch upstream --tags
git rebase v0.X.YRe-check FortifyServiceProvider (where loginView lives), the organization_invitations
schema, and RegistrationMode.
This patch has an expiry date. When upstream merges #1131 and ships OIDC, drop it and move the configuration over.