Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added support for saml login #1408

Merged
merged 21 commits into from Jan 31, 2020
Merged

added support for saml login #1408

merged 21 commits into from Jan 31, 2020

Conversation

kevinpapst
Copy link
Member

@kevinpapst kevinpapst commented Jan 23, 2020

Description

Only tested with Google apps SAML implementation

In order to test this branch, do a checkout and then:

  • composer install
  • edit config/packages/local.yaml with the adjusted contents below
  • bin/console cache:clear --env=prod
  • bin/console cache:warmup --env=prod
  • Test the new login button ...

YOU FIND THE REQUIRED CONFIGURATION IN THE DOCUMENTATION
See https://www.kimai.org/documentation/saml.html

Fixes #1227

Links:

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

  • I verified that my code applies to the guidelines (composer kimai:code-check)
  • I updated the documentation (see here)
  • I agree that this code is used in Kimai and will be published under the MIT license

@kevinpapst kevinpapst changed the title added first draft for saml support added support for saml login Jan 23, 2020
@timlegge
Copy link

Hi

I get an error on successful saml response:

bash-5.0$ tail -f var/log/prod.log
[2020-01-24 04:02:57] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"secured_area","authenticators":1} []
[2020-01-24 04:02:57] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"secured_area","authenticator":"App\Security\TokenAuthenticator"} []
[2020-01-24 04:02:57] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"secured_area","authenticator":"App\Security\TokenAuthenticator"} []
[2020-01-24 04:02:57] request.CRITICAL: Uncaught PHP Exception Doctrine\DBAL\Exception\NotNullConstraintViolationException: "An exception occurred while executing 'INSERT INTO kimai2_users (username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar, api_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' with params [null, null, null, null, 1, null, "", null, null, null, "a:0:{}", null, "2020-01-24 04:02:57", null, null, null]: SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: kimai2_users.username" at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractSQLiteDriver.php line 39 {"exception":"[object] (Doctrine\DBAL\Exception\NotNullConstraintViolationException(code: 0): An exception occurred while executing 'INSERT INTO kimai2_users (username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar, api_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' with params [null, null, null, null, 1, null, "", null, null, null, "a:0:{}", null, "2020-01-24 04:02:57", null, null, null]:\n\nSQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: kimai2_users.username at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractSQLiteDriver.php:39, Doctrine\DBAL\Driver\PDOException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: kimai2_users.username at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:123, PDOException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: kimai2_users.username at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:121)"} []
[2020-01-24 04:10:22] console.ERROR: Error thrown while running command "'kimai:create-user' superadmin 'admin@kimai.local' ROLE_SUPER_ADMIN changemeplease". Message: "The Translator does not support the following options: 'cache_vary'." {"exception":"[object] (Symfony\Component\Translation\Exception\InvalidArgumentException(code: 0): The Translator does not support the following options: 'cache_vary'. at /opt/kimai/vendor/symfony/framework-bundle/Translation/Translator.php:77)","command":"'kimai:create-user' superadmin 'admin@kimai.local' ROLE_SUPER_ADMIN changemeplease","message":"The Translator does not support the following options: 'cache_vary'."} []
[2020-01-24 04:13:42] request.INFO: Matched route "saml_acs". {"route":"saml_acs","route_parameters":{"_route":"saml_acs","_controller":"App\Auth\Controller\SamlController::assertionConsumerServiceAction"},"request_uri":"https://kimai-saml.alc.ca/auth/saml/acs","method":"POST"} []
[2020-01-24 04:13:42] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"secured_area","authenticators":1} []
[2020-01-24 04:13:42] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"secured_area","authenticator":"App\Security\TokenAuthenticator"} []
[2020-01-24 04:13:42] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"secured_area","authenticator":"App\Security\TokenAuthenticator"} []
[2020-01-24 04:13:43] request.CRITICAL: Uncaught PHP Exception Doctrine\DBAL\Exception\NotNullConstraintViolationException: "An exception occurred while executing 'INSERT INTO kimai2_users (username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar, api_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' with params [null, null, null, null, 1, null, "", null, null, null, "a:0:{}", null, "2020-01-24 04:13:42", null, null, null]: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'username' cannot be null" at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php line 103 {"exception":"[object] (Doctrine\DBAL\Exception\NotNullConstraintViolationException(code: 0): An exception occurred while executing 'INSERT INTO kimai2_users (username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar, api_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' with params [null, null, null, null, 1, null, "", null, null, null, "a:0:{}", null, "2020-01-24 04:13:42", null, null, null]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1048 Column 'username' cannot be null at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php:103, Doctrine\DBAL\Driver\PDOException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'username' cannot be null at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:123, PDOException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'username' cannot be null at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:121)"} []
^C

Possible I am doing something wrong...

@timlegge
Copy link

Ah. It requires me to create the username first. It should default to creating the user like ldap.

@kevinpapst
Copy link
Member Author

Accepted: email is now the only required field, username will fallback to email if not set.
Did you try groups already?

@timlegge
Copy link

timlegge commented Jan 24, 2020 via email

@timlegge
Copy link

Hi

I successfully tested a login with a Microsoft Azure IDp however upon the second login it attempts to create a duplicate account.

[2020-01-24 14:25:06] request.CRITICAL: Uncaught PHP Exception Doctrine\DBAL\Exception\UniqueConstraintViolationException: "An exception occurred while executing 'INSERT INTO kimai2_users (username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar, api_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' with params ["tl@example.com", "tl@example.com", "tl@example.com", "tl@example.com", 1, null, "", null, null, null, "a:0:{}", null, "2020-01-24 14:25:06", null, null, null]: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'tl@example.com' for key 'UNIQ_B9AC5BCE92FC23A8'" at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php line 55 {"exception":"[object] (Doctrine\DBAL\Exception\UniqueConstraintViolationException(code: 0): An exception occurred while executing 'INSERT INTO kimai2_users (username, username_canonical, email, email_canonical, enabled, salt, password, last_login, confirmation_token, password_requested_at, roles, alias, registration_date, title, avatar, api_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' with params ["tl@example.com", "tl@example.com", "tl@example.com", "tl@example.com", 1, null, "", null, null, null, "a:0:{}", null, "2020-01-24 14:25:06", null, null, null]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'tl@example.com' for key 'UNIQ_B9AC5BCE92FC23A8' at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php:55, Doctrine\DBAL\Driver\PDOException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'tl@example.com' for key 'UNIQ_B9AC5BCE92FC23A8' at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:123, PDOException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'tl@example.com' for key 'UNIQ_B9AC5BCE92FC23A8' at /opt/kimai/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:121)"} []

@kevinpapst
Copy link
Member Author

kevinpapst commented Jan 24, 2020

Maybe because of your previous tests? Check your kimai2_users database existing accounts. The unique index is on the username_canonical column.

Users are persisted and re-used if found in the database.

@timlegge
Copy link

No, there were only 3 users; My superadmin, google account, and the microsoft account

I deleted the microsoft account and was able to log in. I closed the browser and tried again and I was not able to login

@kevinpapst
Copy link
Member Author

Please add a var_dump in retrieveUser the file: vendor/kevinpapst/oneloginsaml-bundle/Security/Authentication/Provider/SamlProvider.php in line 73, so the message looks like this:

        try {
            var_dump($token);exit;
            return $this->userProvider->loadUserByUsername($token->getUsername());
        } catch (UsernameNotFoundException $e) {
            if ($this->userFactory instanceof SamlUserFactoryInterface) {
                return $this->generateUser($token);
            }
            
            throw $e;
        }

and let me know what you see. Maybe the user has the wrong email?

@timlegge
Copy link

Sorted the duplicate I changed the local.yaml mapping from

username: $Email

to

username: $http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name

How do the groups mapping work? An can I map the full name Full Name?

@kevinpapst
Copy link
Member Author

I think you should be able to remove the username from the mapping completely, as it will default to that field - but I am not entirely sure.

You can try to set the alias for the full name.

@timlegge
Copy link

Working on the groups. I have
groups:
attribute: groups
mapping:
Kimai - Administrator: ROLE_ADMIN
Kimai - Team Lead: ROLE_TEAMLEAD

Which as you may notice have spaces :-)

when /opt/kimai/src/Auth/User/SamlUserFactory.php reads them ( var_dump($this->groupMapping); at line 52

I get:
array(2) { ["Kimai _ Administrator"]=> string(10) "ROLE_ADMIN" ["Kimai _ Team Lead"]=> string(13) "ROLE_TEAMLEAD" }

For some reason it is converting the - to an _

Any ideas?

@kevinpapst
Copy link
Member Author

Oh please not 😬 okay right on, I will convert that "array like" syntax to the same that is used for LDAP. Now I can remember the reason ^^

@kevinpapst
Copy link
Member Author

@timlegge I updated the configuration syntax, you need to change the block in kimai.

The attribute mapping from:

        mapping:
            email: $Email
            username: $Email
            alias: $FullName

to

        mapping:
            - { saml: $Email, kimai: email }
            - { saml: $Email, kimai: username }
            - { saml: $FullName, kimai: alias }

Then groups was renamed to roles and the array syntax under roles was also changed like the mapping above.

See https://www.kimai.org/documentation/saml.html

Full example:

kimai:
    saml:
        activate: true
        title: Login with Google
        mapping:
            - { saml: $Email, kimai: email }
            - { saml: $Email, kimai: username }
            - { saml: $FullName, kimai: alias }
        roles:
            attribute: Roles
            mapping:
                - { saml: Admins, kimai: ROLE_ADMIN }
                - { saml: Management, kimai: ROLE_TEAMLEAD }

@kevinpapst kevinpapst added this to the 1.8 milestone Jan 24, 2020
@timlegge
Copy link

timlegge commented Jan 24, 2020 via email

@timlegge
Copy link

@kevinpapst Initial testing looks promising. Nice!

I have tested with one user (static group) against a Microsoft Azure Saml Identity Provider.

With the change to the ldap style arrays my groups complete with spaces and dashes (I know, I know) works and the user is created.

I intercepted the response from the server and tampered with the xml to attempt to grant extra permissions and as expected OneLogin caught the tampering with the signed Message - great

I have not changed the group that the user is in yet but I did set the roles in the user record to ''. As I was logged in that changed my user to a normal user on page refresh

I tried to log in again and the permissions were that of a normal user. It would be expected to refresh the user data on login. Especially the title, roles and possibly alias

So far that is the only issue I have found.

@kevinpapst
Copy link
Member Author

It would be expected to refresh the user data on login. Especially the title, roles and possibly alias

Things like that should be discussed upfront. The necessary change was really big, but it works now.

When updating, you need to remove these two lines from the config (see updates docs):

                user_factory: App\Auth\User\SamlUserFactory
                persist_user: true

@timlegge
Copy link

The role information is not updateing in the latest version. Looking for the issue now

@timlegge
Copy link

never mind. My issue - reading the documentation caused me an issue:

roles:
attribute: Roles

The attribute has to match the attribute name of the groups/roles being returned in the Saml Response

@timlegge
Copy link

timlegge commented Jan 29, 2020

Found one issue with running behind a nginx proxy:

I will look at if thats a nginx resolution.

[2020-01-29 16:06:40] request.CRITICAL: Uncaught PHP Exception RuntimeException: "The response was received at http://kimai-test.example.com:8001/auth/saml/acs instead of https://kimai-test.example.com/auth/saml/acs" at /opt/kimai/src/Saml/Controller/SamlController.php line 52 {"exception":"[object] (RuntimeException(code: 0): The response was received at http://kimai-test.example.com:8001/auth/saml/acs instead of https://kimai-test.example.com/auth/saml/acs at /opt/kimai/src/Saml/Controller/SamlController.php:52)"} []

@kevinpapst
Copy link
Member Author

My assumption is, that this is a general "problem" with the Symfony setup and not with the SAML module.
When you look at this: https://github.com/kevinpapst/kimai2/blob/master/public/index.php#L27 then you see, that the env variable TRUSTED_PROXIES tells Symfony to evaluate the X-FORWARDED-* headers and take them into account when generating URLs.

Maybe you have to set the TRUSTED_PROXIES ENV.

How can I test that on my end? Can you share a snippet of your proxy config, so I can setup one here?

@kevinpapst
Copy link
Member Author

Ok, I did set it up and found a solution, which works at a first glance. Need more testing...

Here is my nginx proxy config (nothing special I would say):

server {
    listen 443 ssl;

    server_name kimai2.local;

    ssl_certificate      /Users/kevin/Sites/_certs/kimai2.local.crt;
    ssl_certificate_key  /Users/kevin/Sites/_certs/kimai2.local.key;
    ssl_session_timeout  5m;
    ssl_protocols  SSLv2 SSLv3 TLSv1;
    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers   on;

    access_log  /Users/kevin/Sites/_logs/kimai2.local.log;

    location / {
          proxy_pass http://127.0.0.1:8010/;

          proxy_set_header  Host $http_host;
          proxy_set_header  X-Forwarded-Host $host:$server_port;
          proxy_set_header  X-Forwarded-Server $host;
          proxy_set_header  X-Real-IP $remote_addr;
          proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header  X-Forwarded-Proto $scheme;
          proxy_set_header  X-Forwarded-Port $server_port;
      }
}

This is my .env setting:

TRUSTED_PROXIES=127.0.0.1,kimai2.local,localhost

And this is a saml specific part of my local.yaml (I think the baseurl is the important setting here):

            sp:
                entityId: 'https://kimai2.local/auth/saml/metadata'
                assertionConsumerService:
                    url: 'https://kimai2.local/auth/saml/acs'
                    binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
                singleLogoutService:
                    url: 'https://kimai2.local/auth/saml/logout'
                    binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
                #privateKey: ''
            # Optional settings
            baseurl: 'https://kimai2.local/auth/saml/'

@timlegge
Copy link

Looking, my proxy is ... complex ...

@kevinpapst
Copy link
Member Author

kevinpapst commented Jan 29, 2020

Yes, but it is not about the proxy, but about the two kimai configs (the forwarded for headers are pretty standard, aren't they?).

Edit: I added a chapter to the documentation about proxies: https://www.kimai.org/documentation/webserver-configuration.html#reverse-proxy

@kevinpapst
Copy link
Member Author

@timlegge I found one more piece of code, which might help ... probably we can even skip the usage of the baseurl with that change. see my latest push, especially this line: 04223e0#diff-918f42d3001f48647a833788daea0e66R23

@timlegge
Copy link

Hi

Its possible I have my proxies misconfigured but I still need the base enabled. Works fine with that enabled.

Tim

@kevinpapst
Copy link
Member Author

kevinpapst commented Jan 30, 2020

LOL, for the other customer the baseurl didn't work, but this setting was required.
I have no plans for further changes, unless you find issues. So whenever you are done with testing: please let me know.

@timlegge
Copy link

likely done tomorrow.

@urinal-cake
Copy link

urinal-cake commented Jan 31, 2020

I had my team test SAML auth for about 3 hours yesterday, and we cannot find any issues as of today.

We did see an issue where the user would never be allowed to login if they first attempted to login using a non-SAML authenticated user:

  1. Click Login button
  2. Select Gmail account, or G Suite account for a different organization other than target.
  3. Notice app_not_configured_for_user error
  4. Go back in browser
  5. Utilize G Suite account for target organization
  6. Notice same error as Add filter to Timesheet admin #3, even though the account is now valid.

We're now no longer able to reproduce that issue, but will keep an eye out for it nonetheless. It could have been resolved by browser cache and Kimai app cache. However, we did that prior to starting our testing.

For the moment, I do agree with an earlier comment about the need to concatenate the 'first name' and 'last name' attribute. Google and idP do not provide 'Full Name' as an attribute. It is possible to create a 'Custom Attribute,' but for an organization with many users, this requires administrator access to do so, and can be time consuming.

It would be incredibly powerful for us to be able to add the following:

         mapping:
            - { saml: $Email, kimai: email }
            - { saml: $Email, kimai: username }
            - { saml: $FirstName ~ $LastName, kimai: alias }

Or something similar.

# Conflicts:
#	composer.json
#	composer.lock
@timlegge
Copy link

timlegge commented Jan 31, 2020 via email

@kevinpapst
Copy link
Member Author

kevinpapst commented Jan 31, 2020

At that point in time kimai did not register the user, as no information about him is available - so I guess this problem won't reoccur.

I added the concatenation of attributes, configure it like this:

        mapping:
            - { saml: $Email, kimai: email }
            - { saml: $FirstName $LastName, kimai: alias }

Its pure string replacement magic and each attribute in the saml key needs to be delimited by a space, otherwise it won't work, so you can use something like $FirstName-$LastName.

Please test if it works and let me know.
I am keen on merging this and stop stacking more features on top.

@codecov
Copy link

codecov bot commented Jan 31, 2020

Codecov Report

Merging #1408 into master will decrease coverage by <.01%.
The diff coverage is 92.66%.

@@             Coverage Diff              @@
##             master    #1408      +/-   ##
============================================
- Coverage     92.58%   92.57%   -0.01%     
- Complexity     4161     4169       +8     
============================================
  Files           394      396       +2     
  Lines         12799    12878      +79     
============================================
+ Hits          11850    11922      +72     
- Misses          949      956       +7
Impacted Files Coverage Δ Complexity Δ
src/Entity/RatesTrait.php 100% <ø> (ø) 8 <0> (ø) ⬇️
src/Entity/Timesheet.php 95.53% <ø> (ø) 55 <0> (ø) ⬇️
src/Entity/BudgetTrait.php 100% <ø> (ø) 6 <0> (ø) ⬇️
src/Controller/DoctorController.php 71.92% <0%> (ø) 38 <0> (ø) ⬇️
src/Controller/WidgetController.php 0% <0%> (ø) 1 <1> (?)
src/Timesheet/TrackingMode/DurationOnlyMode.php 100% <100%> (ø) 9 <0> (ø) ⬇️
src/Timesheet/Rounding/CeilRounding.php 100% <100%> (ø) 10 <0> (ø) ⬇️
src/Timesheet/Rounding/DefaultRounding.php 100% <100%> (ø) 10 <0> (ø) ⬇️
src/Repository/TimesheetRepository.php 81.63% <100%> (ø) 115 <0> (ø) ⬇️
src/Timesheet/Rounding/ClosestRounding.php 97.43% <100%> (+0.29%) 13 <0> (ø) ⬇️
... and 9 more

@urinal-cake
Copy link

Kevin, this worked flawlessly. We did some more testing with the combined fullname attribute, but haven't turned up any more issues. Good to go from our end. Let me know how else I can help.

@kevinpapst
Copy link
Member Author

@timlegge done as well or do you want some more testing time?

@timlegge
Copy link

@kevinpapst have not pulled the latest commit.

Copy link

@timlegge timlegge left a comment

Choose a reason for hiding this comment

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

I merged the latest and it seems fine with the concat (does not break me)

@kevinpapst
Copy link
Member Author

Thanks @timlegge and @urinal-cake for all the testing!!!!

Changes can still be done, when we find any issue.

@kevinpapst kevinpapst merged commit 6a53357 into master Jan 31, 2020
@kevinpapst kevinpapst deleted the saml branch January 31, 2020 18:47
@lock
Copy link

lock bot commented Apr 2, 2020

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. If you use Kimai on a daily basis, please consider donating to support further development of Kimai.

@lock lock bot locked and limited conversation to collaborators Apr 2, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

Successfully merging this pull request may close these issues.

SAML Support for Authentication
3 participants