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

support for id_token_hint #1125

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

Conversation

srenatus
Copy link
Contributor

@srenatus srenatus commented Nov 13, 2017

The spec (see #990) is a bit vague on this. I suppose you could interpret it as follows: id_token_hint SHOULD be there when prompt=none. However, when prompt=none is passed, dex should error out when it cannot fulfill the request in a promptless way. Now, it's obvious that PasswordConnector can't do prompt=none -- for others, however, it's less obvious. It's perhaps possible to "forward" the prompt=none to OIDC connectors, but for SAML AuthnRequests, I'm not sure if that's a thing.

However, what this does is opportunistically select the login provider based on the id_token_hint. If this results in a prompt or not depends on the provider, and we don't promise anything. So, the case here is prompt=none is not set, id_token_hint is OPTIONAL. We make use of that optional hint.

To test-drive this, checkout the branch and start the example-app against a dex instance with the static passwordDB and example-connector, and follow along:

  1. After a Login using Example, there's a button "use ID token hint":

    screen shot 2017-11-13 at 11 19 25
  2. You'll be redirected to /auth/local, and it will log you in without any need for interaction, so you'll be redirected to the callback:
    screen shot 2017-11-13 at 11 19 34

  3. If you start a new process, by logging in from http://127.0.0.1:5555, and select the email provider, clicking "login with id token hint" gets you back straight away to the password prompt:

    screen shot 2017-11-13 at 11 21 31

Now, technically, we'd probably have enough information to pre-fill that username field.

However, there might also be ways to forward the "requested subject" to OIDC and SAML -- and I believe this is what we should do? The edge case I've got in mind is this:

  1. dex gets an auth request with an id_token_hint for alice (using SAML, for example)
  2. the browser is redirected to SAML (authn request, no constraints whatsoever)
  3. the users logs in as bob
  4. the browser returns to dex, it creates a session for bob, as per SAML Assertion.

While this isn't a security hazard, it's potentially a UX issue.

What do you think? I suppose there's two options here:

  1. clean up this PR, leave it as-is
  2. Do (1.), and fix the password connector username pre-fill.
  3. Do (1.), and proceed down the road of request-a-specific-user-to-login-using-any-supported-connector.

(3.) is too much for a single PR, and I cannot commit to getting this done in all connectors.
(2.) seems OK though, but I'd prefer to split this in two PRs.

Ok-ish way to proceed? If so, I'm going to add tests.

TODOs

  • Tests
  • Have I screwed up the vendored dependencies?! That change seems way to big for just additionally using go-jose.v2/jwt 🤔 ✅ make revendor, all good now.

@srenatus srenatus changed the title [WIP] support for id_token_hint [WIP+question] support for id_token_hint Nov 13, 2017
@srenatus srenatus force-pushed the sr/id_token_hint branch 2 times, most recently from 6d13ae3 to f3147c2 Compare November 13, 2017 12:54
Copy link
Contributor

@ericchiang ericchiang left a comment

Choose a reason for hiding this comment

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

Seems reasonable. What do we do if the key's been rotated? Do we really need to verify the signature?

server/oauth2.go Outdated
@@ -20,6 +20,7 @@ import (
"time"

jose "gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't need to use the jwt package for this. The jose.ParseSigned works fine https://godoc.org/gopkg.in/square/go-jose.v2#ParseSigned

server/oauth2.go Outdated
@@ -376,6 +377,8 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (req storage.AuthReq
scopes := strings.Fields(q.Get("scope"))
responseTypes := strings.Fields(q.Get("response_type"))

connectorID := s.connectorIDFromIDTokenHint(q.Get("id_token_hint"))
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we error if the token hint is incorrect?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OPTIONAL. ID Token previously issued by the Authorization Server being passed as a hint about the End-User's current or past authenticated session with the Client. If the End-User identified by the ID Token is logged in or is logged in by the request, then the Authorization Server returns a positive response; otherwise, it SHOULD return an error. When possible, an id_token_hint SHOULD be present when prompt=none is used and an invalid_request error MAY be returned if it is not; however, the server SHOULD respond successfully when possible, even if it is not present. The Authorization Server need not be listed as an audience of the ID Token when it is used as an id_token_hint value.

Hmm not sure if the spec is clear there. Anyways, I can make this error out if reading the connectorID from the token fails.

server/oauth2.go Outdated
// figure out key used for signing this
var usedKey string
for _, hdr := range tok.Headers {
if jose.SignatureAlgorithm(hdr.Algorithm) != jose.RS256 {
Copy link
Contributor

Choose a reason for hiding this comment

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

In some distant future, I'd like to not hard code jose.RS256 everywhere. Maybe we can take this from a centrally configured place?

return
}
if idTokenHint != "" {
// redirect to auth URL with the hint, using default scopes
scopes := []string{"openid", "profile", "email"}
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems odd that these aren't taken from the first page.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I was a bit lazy here. I'm going to try to fix this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's actually not that simple -- we don't know much of the granted scopes when retrieving the id_token 🤔 Since this is the example app, I haven't spent too much time on implementing this.

Copy link
Contributor

Choose a reason for hiding this comment

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

np. it's not critical.

@srenatus
Copy link
Contributor Author

srenatus commented Nov 14, 2017

Seems reasonable. What do we do if the key's been rotated? Do we really need to verify the signature?

@ericchiang Actually, when it comes to what jose exports, I'm having trouble extracting claims from the payload -- unless I'm verifying the token (since Verify() returns its payload). I'll go look for another way, though 👍

On the issue, I'm torn. I've had another look at the rotation logic (please correct me if I'm wrong): keys get dropped when there's no possibility to have a non-expired ID token signed with that key around.
With this in mind, I suppose checking the signatures is not what we should do.

This opens up the question of what could possibly go wrong? Basically, we're opening up a way for someone (Bob) to send someone else (Cathy) to a certain connector by tricking her into opening /dex/auth?id_token_hint=CRAFTEDIDTOKEN. Now, what would happen there is that Cathy would either be able to login or not. ⏩ I don't think this is too risky. After all, we're only allowing for connector IDs that dex knows (and serves), and we don't redirect anywhere else.

@srenatus
Copy link
Contributor Author

srenatus commented Nov 14, 2017

Ok, I've updated the branch. Remaining issues/questions:

  • It seems that jumping directly to the right connector, if that connector is a password connector, breaks the "back" link (from show "back" link for password connectors #1123), by going back to the example-app, not the page we've been 302-redirected from.
  • There's some potential for UX improvements from reading the UserID from the id_token_hint, too -- however, I wasn't sure how to put that into storage.AuthRequest. There's a Claims.UserID that might be usable, but I can't tell if this is a terrible thing to do. (Is it?)
    • Also, this would affect step (2.) in the list of the top post, so it could be tackled independently.

@srenatus srenatus changed the title [WIP+question] support for id_token_hint support for id_token_hint Nov 14, 2017
@ericchiang
Copy link
Contributor

It seems that jumping directly to the right connector, if that connector is a password connector, breaks the "back" link (from #1123), by going back to the example-app, not the page we've been 302-redirected from.

This seems like the biggest outstanding concern.

There's some potential for UX improvements from reading the UserID from the id_token_hint, too -- however, I wasn't sure how to put that into storage.AuthRequest. There's a Claims.UserID that might be usable, but I can't tell if this is a terrible thing to do. (Is it?)

We're not guaranteed that the UserID actually corresponds to the username that the user entered to login. In LDAP your username might be "jane" but your userID would be "cn=jane,dn=people,..."

@srenatus
Copy link
Contributor Author

srenatus commented Dec 4, 2017

This seems like the biggest outstanding concern.

Alright, I'll take care of this 😄

This way, we automatically get nicer output that has the test case name
prefixed:

    --- PASS: TestParseAuthorizationRequest (0.01s)
	--- PASS: TestParseAuthorizationRequest/normal_request (0.00s)
	--- PASS: TestParseAuthorizationRequest/POST_request (0.00s)
	--- PASS: TestParseAuthorizationRequest/invalid_client_id (0.00s)
	--- PASS: TestParseAuthorizationRequest/invalid_redirect_uri (0.00s)
	--- PASS: TestParseAuthorizationRequest/implicit_flow (0.00s)
	--- PASS: TestParseAuthorizationRequest/unsupported_response_type (0.00s)
	--- PASS: TestParseAuthorizationRequest/only_token_response_type (0.00s)

Signed-off-by: Stephan Renatus <[email protected]>
@srenatus
Copy link
Contributor Author

srenatus commented Dec 4, 2017

@ericchiang I'm afraid e8fe811 is a bit of a blunt solution -- but I haven't found a better one. I'd appreciate any input on this.

Also, I'd like to add tests. Currently, it seems like it might make most sense to add some unit tests for handleAuthorization to handlers_test.go? Do you agree? (I'm thinking of simple checks like "server with one connector, it forwards"; "server with two connectors, it doesn't", "server with two connectors + id_token_hint, it forwards".)

@deric
Copy link
Contributor

deric commented Jan 22, 2019

Any progress on this?

@srenatus
Copy link
Contributor Author

@deric I've afraid I've dropped the ball on this. I'll try to pick it up 😅 -- no promises that it'll happen this week, though.

@srenatus
Copy link
Contributor Author

@deric if you'd like to take over, be my guest!

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.

3 participants