Add support for OIDC authentication#5865
Conversation
|
Thanks for your effort on this PR. I am not going to details yet, becuase I'd like to discuss one topic first: Did you try to use/integrate the Symfony OidcHandler https://symfony.com/doc/current/security/access_token.html#using-openid-connect-oidc ? I am not really open to integrate new dependencies, especially for security related topics. Even more so, as that library didn't receive any commit in 10 months and has open topics for PHP 8.5. The last release was in September 2024. That is not really a reliable dependency. |
|
Personally I would expect Kimai to only provide the interfaces and possibilities to add new providers but the providers should come from plugins. |
|
Completely agree on the dependency point. I wasn’t aware that Symfony had OIDC support built-in. A plugin could make sense, but it has it be tied pretty deeply into the Symfony auth system to work. |
|
What I mean is that in my opinion this login authentication provider should not be part of Kimai Core but be provided as a plugin (if possible). |
|
The problem with everything auth related is that it needs changes in core config files. So shipping it as plugin would be ideal, but I am not sure how easy that actually is to achieve. Yes, I'd like to include the option for OIDC, but as said, with Symfony dependencies only. @matt-oakes are you willing to have a look at the Symfony own provider? |
I can only tell you what TYPO3 uses. TYPO3 has a Auth service chain (https://docs.typo3.org/permalink/t3coreapi:authentication-service-chain). It basically looks like this: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Services/UsingServices/ServiceChain.html#services-using-services-service-chain So you have a registry where you can register your own auth service provider and TYPO3 will iterate over them and check if the auth service provider can handle the request and if the auth was successful or not. For Kimai this could be an event listener where you can register your own service provider and Kimai then calls them. Each auth service provider needs to implement an interface (https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Services/Developer/ServiceApi.html) |
|
Sorry for the long delay on this! I have made the changes now to get rid of the third-party dependency and instead using the OIDC functions from Symfony. The only one we actually need is the I had to re-implement the "discovery" part of the OIDC spec which uses the base URL of the server and the "well known" URL to discover all of the other paths it needs to autheticate with that particular server. This is fairly straightforward and is implemented in All of the other changes are around generating the URLs to redirect to ourselves and the scafolding required to wire up the new classes. Hopefully this all makes sence and you can review this fully. All of the same configuration and testing steps for the initial post will still work with this PR with these changes. |
|
Just a gentle nudge on this. Is there anything you need from me or any other details you'd like? |
|
This is quite a lot of security related code that you want me to maintain in the long term. |
There was a problem hiding this comment.
Pull request overview
This PR introduces OpenID Connect (OIDC) authentication as a first-class alternative login mechanism alongside existing SAML support, including configuration, routing, and user hydration/role mapping.
Changes:
- Adds a new OIDC authentication flow (controller routes, authenticator, discovery, and user provisioning/hydration).
- Extends configuration to support OIDC provider settings and role mapping, plus wires new services into Symfony.
- Updates login UI and adjusts tests to account for the new
AUTH_OIDCuser auth type.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Voter/UserVoterTest.php | Adds OIDC to auth-type test cases. |
| tests/Controller/Security/SecurityControllerTest.php | Updates controller construction to include OIDC configuration. |
| templates/security/login.html.twig | Adds an OIDC login button to the login page. |
| src/Oidc/Security/OidcAuthenticationSuccessHandler.php | Adds a success handler for OIDC logins (redirect handling). |
| src/Oidc/Security/OidcAuthenticationFailureHandler.php | Adds a failure handler for OIDC logins. |
| src/Oidc/OidcUserInfoTokenHandlerFactory.php | Creates the Symfony OIDC userinfo token handler using discovery metadata. |
| src/Oidc/OidcToken.php | Adds an OIDC security token type. |
| src/Oidc/OidcProvider.php | Implements OIDC user lookup, hydration, and role mapping. |
| src/Oidc/OidcLoginAttributes.php | Adds a container for OIDC claims/user identifier. |
| src/Oidc/OidcDiscovery.php | Implements OIDC discovery document fetching + caching. |
| src/Oidc/OidcBadge.php | Adds a Passport badge to carry OIDC attributes. |
| src/Oidc/OidcAuthenticator.php | Adds the main Symfony authenticator for OIDC callback handling. |
| src/Entity/User.php | Introduces AUTH_OIDC constant. |
| src/DependencyInjection/Configuration.php | Adds kimai.oidc configuration tree and validation. |
| src/Controller/Security/SecurityController.php | Passes OIDC config into the login template. |
| src/Controller/Auth/OidcController.php | Adds OIDC login/callback routes (redirect to provider, callback placeholder). |
| src/Configuration/SystemConfiguration.php | Adds OIDC config accessors and adjusts login form activation logic. |
| src/Configuration/OidcConfigurationInterface.php | Adds OIDC configuration interface. |
| src/Configuration/OidcConfiguration.php | Implements OIDC configuration adapter over SystemConfiguration. |
| phpstan.neon | Adds PHPStan ignores for new OIDC handler properties. |
| config/services.yaml | Registers OIDC services and wiring overrides. |
| config/packages/security.yaml | Registers the OIDC authenticator in the firewall. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {% if oidc_config.isActivated() %} | ||
| {% if kimai_config.loginFormActive %} | ||
| <div class="hr-text">{{ 'or'|trans({}, 'TablerBundle') }}</div> | ||
| {% endif %} | ||
| <div class="card-body"> | ||
| <div class="row"> | ||
| {% if not kimai_config.loginFormActive %} | ||
| <h2 class="card-title text-center mb-4">{{ block('login_box_msg') }}</h2> | ||
| {% endif %} |
| // if OIDC is active, the login form cannot be deactivated | ||
| if (!$this->isOidcActive()) { | ||
| return true; | ||
| } | ||
|
|
||
| // if SAML is active, the login form cannot be deactivated | ||
| if (!$this->isSamlActive()) { | ||
| return true; | ||
| } |
| protected function determineTargetUrl(Request $request): string | ||
| { | ||
| $relayState = $request->request->get('RelayState', $request->query->get('RelayState')); | ||
| if (\is_scalar($relayState)) { | ||
| $relayState = (string) $relayState; | ||
| if ($relayState !== $this->httpUtils->generateUri($request, (string) $this->options['login_path'])) { | ||
| return $relayState; | ||
| } | ||
| } | ||
|
|
||
| return parent::determineTargetUrl($request); | ||
| } |
| // map the user attributes onto the user | ||
| if ($token->hasAttribute('display_name')) { | ||
| $user->setAlias($token->getAttribute('display_name')); | ||
| } | ||
| if ($token->hasAttribute('email')) { | ||
| $user->setEmail($token->getAttribute('email')); | ||
| } | ||
| if ($token->hasAttribute('picture')) { | ||
| $user->setAvatar($token->getAttribute('picture')); | ||
| } |
| $roles = []; | ||
| $oidcGroups = $token->getAttribute('groups'); | ||
| foreach ($oidcGroups as $groupName) { | ||
| if (\array_key_exists($groupName, $groupMap)) { | ||
| $roles[] = $groupMap[$groupName]; | ||
| } | ||
| } |
| } | ||
| } | ||
| }) | ||
| ->thenInvalid('You need to configure a OIDC provider_url, client_id, and client_secret.') |
| <h2 class="card-title text-center mb-4">{{ block('login_box_msg') }}</h2> | ||
| {% endif %} | ||
| <div class="col text-center"> | ||
| <a href="{{ path('oidc_login') }}" id="social-login-button" tabindex="50" class="btn btn-white w-50"> |
| private function hydrateUser(User $user, OidcLoginAttributes $token): void | ||
| { | ||
| // extract user roles from the oidc "groups" attribute | ||
| $groupMapping = $this->configuration->getRolesMapping(); | ||
| if ($token->hasAttribute('groups')) { | ||
| $groupMap = []; | ||
| foreach ($groupMapping as $mapping) { |
|
Thanks. That's fair and completely understand about the lack of time. Just let me know if you have any questions or need me to do anything to help with this. |
Description
This PR adds support for OpenID Connect (OIDC) authentication. It resolves #2469.
The code is heavily based on the existing SAML authentication implementation using:
OidcAuthenticatorto hook into Symfony authenticating usingPassport.OidcProviderto create and hydrate users.OidcControllerto supply the login routes to redirect to the configured OIDC provider.The implementation of OIDC in this PR is handled by
jumbojett/openid-connect-phpwhich does all of the heavy lifting.Configuration is very similar to SAML too, but much simplier because ODIC is more standardized and has sensible defaults. Basic configuration would be:
These are the only configuration values which need to be provided because OIDC is able to automatically detect the others using the "well known" URL. In the example above, it is able to access
https://auth.example.com/.well-known/openid-configuration. The response to this will include all of the URLs and other configuration needed to setup OIDC with that specific server.I have also gone a little further and implemented the same "role mapping" system which SAML has. You can configure this in a very similar way by adding this to the above config:
This works example the same as with SAML, however, the
attributeconfiguration value isn't needed as it is standarized in OIDC.Demo video
kimai_oidc.mp4
How to test this
To test this, you will need to have an OIDC authentication provider. There are many available such as Pocket ID, Ory Hydra, Authentik, VoidAuth, Keycloak, and Forgejo.
However, if you don't have any setup, then by far the easiest is to start a "demo instance" of Pocket ID (they last for 30 minutes before being deleted).
https://demo.pocket-id.org
When you have an instance you can follow these steps to create the required enviornment for testing:
To test role mapping:
admins.Types of changes
Checklist
composer code-check)