Skip to content

Add support for OIDC authentication#5865

Open
matt-oakes wants to merge 2 commits into
kimai:mainfrom
matt-oakes:oidc
Open

Add support for OIDC authentication#5865
matt-oakes wants to merge 2 commits into
kimai:mainfrom
matt-oakes:oidc

Conversation

@matt-oakes

@matt-oakes matt-oakes commented Mar 9, 2026

Copy link
Copy Markdown

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:

  • OidcAuthenticator to hook into Symfony authenticating using Passport.
  • OidcProvider to create and hydrate users.
  • OidcController to supply the login routes to redirect to the configured OIDC provider.
    The implementation of OIDC in this PR is handled by jumbojett/openid-connect-php which 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:

kimai:
    oidc:
        activate: true
        title: "Login with Custom OIDC"
        provider_url: "https://auth.example.com/"
        client_id: "EXAMPLE"
        client_secret: "EXAMPLE"

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:

          roles:
            resetOnLogin: true
            mapping:
                # Insert your role-mapping here (ROLE_USER is added automatically)
                - { oidc: Admin, kimai: ROLE_SUPER_ADMIN }
                - { oidc: Manager, kimai: ROLE_ADMIN }
                - { oidc: Teamlead, kimai: ROLE_TEAMLEAD }

This works example the same as with SAML, however, the attribute configuration 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:

  1. Follow the setup instructions to create a new user.
  2. Create a passkey when requested, or skip in this case and it will keep you logged in until the demo instance is deleted.
  3. Go to "Administration" -> "OIDC Clients" and then click "Add OIDC client"
  4. Fill in the name as "Kamai" (or whatever you like) and leave the rest of the settings blank. Press "Save"
  5. Scroll down to the "Allowed User Groups", expand it and press "Unrestrict". This will allow all users in the auth provider (just you) to access the client.
  6. At the top of the screen there will be the settings. Expand it and then use "client ID", "client secret", and "issuer URL" to fill in this Kamai config:
kimai:
    oidc:
        activate: true
        title: "Login with Pocket ID"
        provider_url: "https://EXAMPLE.demo.podkcet-id.org"
        client_id: "EXAMPLE"
        client_secret: "EXAMPLE"
  1. Open the login page of your Kimai instance and there should be a "Login with Pocket ID" button. Pressing that should take you to the Pocket ID login page.
  2. Press sign in and then accept the information which will be shared.
  3. You will be redirected back to Kimai and logged in. If this was a new user your name, email, and profile picture will be set from Pocket ID.

To test role mapping:

  1. In your Pocket ID demo instance, go to "Administration" -> "User groups" and then click "Add Group".
  2. Give it a friendly name and optionally change the name which will be sent to Kimai. This example will use admins.
  3. On the next page, scroll down to the user list, tick your user and then press "Save". This assigns your user to that group.
  4. Modify your Kimai config to include:
          roles:
            resetOnLogin: true
            mapping:
                - { oidc: admins, kimai: ROLE_ADMIN }
  1. Sign out of Kimai and then sign in again with the "Login with Pocket ID" button. You will be asked for permissions again and this time it will be requesting "groups" access.
  2. When you accept and sign in your user will be assigned to the admin role.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

@CLAassistant

CLAassistant commented Mar 9, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@kevinpapst

Copy link
Copy Markdown
Member

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.

@simonschaufi

Copy link
Copy Markdown
Contributor

Personally I would expect Kimai to only provide the interfaces and possibilities to add new providers but the providers should come from plugins.

@matt-oakes

Copy link
Copy Markdown
Author

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.

@simonschaufi

Copy link
Copy Markdown
Contributor

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).

@kevinpapst

Copy link
Copy Markdown
Member

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.
I used a library for SAML in the past and had massive issues with that, so moved away from it.

@matt-oakes are you willing to have a look at the Symfony own provider?

@simonschaufi

simonschaufi commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

I am not sure how easy that actually is to achieve.

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)

@matt-oakes

Copy link
Copy Markdown
Author

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 OidcUserInfoTokenHandler which validates the token and retreives the user identifier. This is the security critical part of the OIDC spec, so that is handled by Symfony.

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 OidcDiscovery. It caches this with an expiry of 1 hour. This can be removed if you would prefer to fetch this every time instead, but I think it's useful as most servers won't change this very often, if at all.

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.

@matt-oakes

Copy link
Copy Markdown
Author

Just a gentle nudge on this. Is there anything you need from me or any other details you'd like?

@kevinpapst

Copy link
Copy Markdown
Member

This is quite a lot of security related code that you want me to maintain in the long term.
This needs time that I don't have right now.
Also I'd like to investigate the SecurityChain that @simonschaufi mentioned.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_OIDC user 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.

Comment on lines +50 to +58
{% 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 %}
Comment on lines +161 to 169
// 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;
}
Comment on lines +25 to +36
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);
}
Comment thread src/Oidc/OidcProvider.php
Comment on lines +94 to +103
// 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'));
}
Comment thread src/Oidc/OidcProvider.php
Comment on lines +78 to +84
$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">
Comment thread src/Oidc/OidcProvider.php
Comment on lines +66 to +72
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) {
@matt-oakes

Copy link
Copy Markdown
Author

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.

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.

Support OpenID Connect

5 participants