Implement generic OIDC-based authentication #1298
Implement generic OIDC-based authentication #1298lheckemann wants to merge 2 commits intoNixOS:masterfrom
Conversation
Co-Authored-By: Maximilian Bosch <maximilian@mbosch.me>
dasJ
left a comment
There was a problem hiding this comment.
We actually have a patch for our idam that is less generic than yours (which is why it's not upstreamed). The comments probably reflect that ;)
I do like your approach which using proper libraries a lot! Also, how testable is this?
|
|
||
| my $after = "/" . $c->req->params->{after}; | ||
|
|
||
| $c->res->cookies->{'after_oidc'} = { |
There was a problem hiding this comment.
I saw this in GitHub as well and I really don't know why we would do this. We already have a session so why not do:
$c->session->{oidc_after} = $after;
You already do this for the state (which is what GitHub is not doing right now afaik 👀)
There was a problem hiding this comment.
Yes, this is just taken directly from the other auth methods. We did discuss that it should also probably be a single session variable regardless of the auth method. I'd prefer to fix this later though.
| $c->session->{oidc_state} = $state; | ||
| my $redirect_url = $oidc_client->uri_to_redirect( | ||
| redirect_uri => $c->uri_for('/oidc-login'), | ||
| scope => q{openid}, |
There was a problem hiding this comment.
Would be nice to have this configurable
There was a problem hiding this comment.
I'm not sure that's desirable: we care about the name, email and sub fields which are standard in openid, and I would very much like to avoid going further than that and requiring mapping logic.
|
|
||
| # Be paranoid about the email address format, since we do use it | ||
| # in URLs. | ||
| die "Illegal email address.\n" unless $email =~ /^[a-zA-Z0-9\.\-\_]+@[a-zA-Z0-9\.\-\_]+$/; |
There was a problem hiding this comment.
Unrelated: this is the absolute worst because it excludes plus-emails
| error($c, "Email address must be verified.", 401) unless $claims->{email_verified}; | ||
|
|
||
| doEmailLogin($self, $c, "oidc", $claims->{email}, $claims->{name} // undef, sprintf("oidc:$claims->{sub}")); | ||
|
|
There was a problem hiding this comment.
I think it'd be nice to have some role mapping as well because otherwise everyone has no permissions. We use:
+ my $role_mapping = $c->config->{oidc}->{role_mapping} or ();
+ # Give out roles
+ $c->user->userroles->delete;
+ push(@{$user_data->{groups}}, ("default"));
+ foreach my $group (@{$user_data->{groups}}) {
+ if (defined($role_mapping->{$group})) {
+ my $roles = $role_mapping->{$group};
+ if (ref($roles) eq 'ARRAY') {
+ for my $mapped_role (@{$roles}) {
+ $c->user->userroles->create({ role => $mapped_role });
+ }
+ } else {
+ $c->user->userroles->create({ role => $roles });
+ }
+ }
+ }There was a problem hiding this comment.
We discussed this at length (this was what took longest by far), and came to the conclusion that this is a real can of worms, because OIDC is about authentication and identity but not authorisation. For now, our recommended approach is to manage permissions manually.
| sub oidc_login :Path('/oidc-login') Args(0) { | ||
| my ($self, $c) = @_; | ||
|
|
||
| error($c, "Logging in via OIDC is not enabled.", 404) unless $c->config->{enable_oidc_login}; |
There was a problem hiding this comment.
Why not scope these? Something like $c>config->{oidc}->{enable}? Or even better $c->config->{oidc}->{client_id} which can be undef or defined and enable/disable oidc auth that way
|
|
||
| doEmailLogin($self, $c, "oidc", $claims->{email}, $claims->{name} // undef, sprintf("oidc:$claims->{sub}")); | ||
|
|
||
| $c->res->redirect($c->uri_for($c->res->cookies->{'after_oidc'})); |
There was a problem hiding this comment.
Same issue as with the GitHub auth: Doesn't work. If you use the cookie (and not the session which I recommended in another comment), you need $c->req->cookies, not res
There was a problem hiding this comment.
Ergh, you're right. I think it did work for some reason though.
| <div class="dropdown-divider"></div> | ||
| [% END %] | ||
| [% IF c.config.enable_oidc_login %] | ||
| <a class="dropdown-item" href="/oidc-redirect?after=[% c.req.path %]">Sign in with OIDC</a> |
There was a problem hiding this comment.
Would also be nice to be able to change the name of the provider so it can be consistent in an organization.
| $c->session->{access_token} = $token_response->access_token; | ||
| $c->session->{expires_at} = time() + $token_response->expires_in; | ||
| $c->session->{refresh_token} = $token_response->refresh_token; | ||
|
|
There was a problem hiding this comment.
We really ought to check the id_token and its signature here and make sure that the id_token is bound to the audience (client_id).. And then save the sub and compare it to the sub received in userinfo.
Otherwise we are vulnerable against token substitution attacks as the access token is not audience-bound.
NOTE: Due to the possibility of token substitution attacks, the UserInfo Response is not guaranteed to be about the End-User identified by the sub (subject) element of the ID Token. The sub Claim in the UserInfo Response MUST be verified to exactly match the sub Claim in the ID Token; if they do not match, the UserInfo Response values MUST NOT be used.
https://openid.net/specs/openid-connect-basic-1_0.html#UserInfo
We're not reusing the access_token long-term but still. we should use the id_token as the source of truth about the user identity. Not the userinfo response. the userinfo endpoint is there to augment the id_token given that you know and verified the sub. Not as a replacement for the id_token
|
I would like to host Hydra in an environment where not having custom SSO is a blocker, so I am quite keen on this change. What can we do to push it through? @lheckemann if you're not working on this anymore, can I take it over? I've rebased against master here: https://github.com/ners/hydra/tree/oidc |
|
It's on my "one day" pile; if you have a bit more time pressure on it, you're very welcome to take over :) |
|
i tried using @ners's rebased patch but couldn't get role mapping to work (with kanidm)... not quite sure if its my configuration issue or the support isn't there yet?
also, kanidm is complaining about PKCE: so i had to set |
|
apparently i don't know how oidc role mapping work :-0 example with kanidm: services.hydra.extraConfig = ''
enable_hydra_login = 0
enable_oidc_login = 1
oidc_client_id = "hydra"
oidc_scope = "openid email profile groups"
oidc_auth_uri = "https://kanidm.example.com/ui/oauth2"
oidc_token_uri = "https://kanidm.example.com/oauth2/token"
oidc_userinfo_uri = "https://kanidm.example.com/oauth2/openid/hydra/userinfo"
include ${config.sops.secrets.hydra.path} # oidc_client_secret = ...
<oidc_role_mapping>
<my hydra admin role>@<kanidm tenant domain> = admin
<my hydra admin role>@<kanidm tenant domain> = bump-to-front
<my hydra user role>@<kanidm tenant domain> = cancel-build
<my hydra user role>@<kanidm tenant domain> = eval-jobset
<my hydra user role>@<kanidm tenant domain> = create-projects
<my hydra user role>@<kanidm tenant domain> = restart-jobs
</oidc_role_mapping>
'';the only drawback of the current implementation is that we are missing PKCE support, otherwise patch lgtm (i can add docs if needed) maybe this is the wrong place to approve pr since i'm testing against https://github.com/ners/hydra/tree/oidc, but i'll do it anyways |
|
I had a bit of a go at this in #1568 this weekend as well, building on this work (which was indispensable for working out how this all works - thank you!) |
No description provided.