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

Verify main vs extra JWT bearers differently #596

Merged
merged 3 commits into from
Jun 25, 2020

Conversation

NickMeves
Copy link
Member

@NickMeves NickMeves commented May 30, 2020

When using the configured provider JWT Verifier, it makes
sense to use the provider CreateSessionStateFromBearerToken
method. For any extra JWT Issuers, they should use a generic
default verifier.

#592

How Has This Been Tested?

Unit tests added for legacy/generic bearer IDToken -> SessionState.

TODO: deploy to a local kubernetes cluster and validate standard human OIDC with k8s service account tokens as an extra jwt issuer. UPDATE: DONE

Checklist:

  • My change requires a change to the documentation or CHANGELOG.
  • I have updated the documentation/CHANGELOG accordingly.
  • I have created a feature (non-master) branch for my PR.

if err != nil {
logger.Printf("failed to verify bearer token: %v", err)
Copy link
Member Author

Choose a reason for hiding this comment

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

I removed this and defaulted to the logging happening only if all verifiers failed. My users have been confused with error messages when 1st or 2nd verifiers on the array fail and the 3rd extraJwt Verifier succeeded (seeing the failed words even though it succeeded was confusing)

assert.Equal(t, rawIDToken, session.AccessToken)
assert.Equal(t, rawIDToken, session.IDToken)
assert.Equal(t, "", session.RefreshToken)
assert.Equal(t, "123456789", session.Email)
Copy link
Member Author

Choose a reason for hiding this comment

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

What is the overall architectural rationale for forcing a session.Email to a claim.Subject when an email claim is missing? And not using the claim.Subject at all in the session and setting both the session.Email and session.User to the Email?

if claims.Email == "" {

I left the existing logic untouched -- but shouldn't session.User == claim.Subject (That's what my OIDC provider results in going the normal route).

User: claims.Email,

And should we allow session.Email to be "" if the IDToken is missing the claim? And adjust the email domains validator to accept it if its set to * (if set to anything else it would fail since it is missing a @ character)

Copy link
Member

Choose a reason for hiding this comment

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

User ID claim was added so that we could make sure that a particular claim was added, to be considered the user, but the work hasn't been completed so it's only half way through

As for falling back, I'm not sure why we are doing that, i think we currently require all sessions to have a user and email field that's not empty, so I guess that's why? I think I need to do some archaeology on this

@NickMeves
Copy link
Member Author

On further reflection, I think this is definitely a bug?

User: claims.Email,

I had a user make a web app that used this proxy for use with CLI tools that did OIDC via PKSE and passed the IDToken as the bearer. He got the X-Forwarded-User as the email claim, vs all the other normal webapps made with our OneLogin OIDC provider had the subject claim as the X-Forwarded-User.

I was very confused at the time, but this is definitely why.

@JoelSpeed JoelSpeed added the bug label Jun 2, 2020
Copy link
Member

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

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

Had a read through, looks pretty good so far, left a couple of comments

Please fixup the conflicts and add a changelog entry

providers/provider_default.go Show resolved Hide resolved
assert.Equal(t, rawIDToken, session.AccessToken)
assert.Equal(t, rawIDToken, session.IDToken)
assert.Equal(t, "", session.RefreshToken)
assert.Equal(t, "123456789", session.Email)
Copy link
Member

Choose a reason for hiding this comment

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

User ID claim was added so that we could make sure that a particular claim was added, to be considered the user, but the work hasn't been completed so it's only half way through

As for falling back, I'm not sure why we are doing that, i think we currently require all sessions to have a user and email field that's not empty, so I guess that's why? I think I need to do some archaeology on this

@NickMeves
Copy link
Member Author

I still owe you an interactive test on this, I haven't dockerized this branch yet to test how it all works deployed as a sidecar in a local kubernetes environment that is setup for OIDC projected service account tokens.

@NickMeves
Copy link
Member Author

Should I make the default_provider aware of the --user-id-claim flag when mapping claims to a session.Email just the same as the OIDC provider?

@NickMeves NickMeves force-pushed the extra-jwt-token-session branch from b08147b to f2bf5a3 Compare June 3, 2020 00:36
@NickMeves
Copy link
Member Author

NickMeves commented Jun 3, 2020

@JoelSpeed Let me know what you find out as far as the history of the forced session.Email and not allowing it to be "".

This commit is my take on allowing it: 1dd6ba5

I can ditch it if needed (or cherry pick it to a separate PR to keep this PR purely about the bearer bugs).

@NickMeves NickMeves force-pushed the extra-jwt-token-session branch from f2bf5a3 to 1dd6ba5 Compare June 3, 2020 00:44
@NickMeves
Copy link
Member Author

I still owe you an interactive test on this, I haven't dockerized this branch yet to test how it all works deployed as a sidecar in a local kubernetes environment that is setup for OIDC projected service account tokens.

This is done, looks good in a kubernetes environment allowing both OneLogin IDTokens (configured provider) OR kubernetes projected service account tokens (extra bearer JWT):

[2020/06/03 17:15:09] [oauthproxy.go:283] Skipping JWT tokens from configured OIDC issuer: "https://greenhouse.onelogin.com/oidc/2"
[2020/06/03 17:15:09] [oauthproxy.go:285] Skipping JWT tokens from extra JWT issuer: "http://oidc.default.svc.cluster.local=sample-app"
...
192.168.144.128 - nick.meves@greenhouse.io [2020/06/03 17:21:27] sample-app.local GET 127.0.0.1:5000 "/sso" HTTP/1.1 "Ruby" 200 1336 0.003
192.168.144.128 - system:serviceaccount:default:dummy [2020/06/03 17:23:46] sample-app.local GET 127.0.0.1:5000 "/sso" HTTP/1.1 "Ruby" 200 1174 0.005

@NickMeves
Copy link
Member Author

I had the /sso endpoint just spit back the X-Forwarded-* auth headers, you can see the behavior allowing empty session.Email in the service account token output (the experimental commit we can remove from this this PR if desired):

OneLogin OIDC Human IDToken:

{
  "data": {
     "username":  "[REDACTED]",
     "prefered_username": "nick.meves@greenhouse.io",
     "email":  "nick.meves@greenhouse.io"
  }
}

Kubernetes Projected Service Account IDToken:

{
  "data": {
    "username": "system:serviceaccount:default:dummy",
    "prefered_username": null,
    "email": null
  }
}

@JoelSpeed
Copy link
Member

So I did some archaeology today, my conclusion is that, if the scope contained email, then the response and the userinfo endpoint should have the email value, but if email is not in the scope, then we don't need for it to fallback to anything I don't think

As far as I can tell, the OAuth2 Proxy doesn't require the email to be returned from the provider anywhere, so I think we can get away with not setting it. It's probably better that I rework this into #560

This will likely be a breaking change, though, technically better than the existing behaviour.
I think we should probably look into the user-id-claim work and work out how we move forward with that.

@NickMeves
Copy link
Member Author

So I did some archaeology today, my conclusion is that, if the scope contained email, then the response and the userinfo endpoint should have the email value, but if email is not in the scope, then we don't need for it to fallback to anything I don't think

As far as I can tell, the OAuth2 Proxy doesn't require the email to be returned from the provider anywhere, so I think we can get away with not setting it. It's probably better that I rework this into #560

This will likely be a breaking change, though, technically better than the existing behaviour.
I think we should probably look into the user-id-claim work and work out how we move forward with that.

I can remove that commit, isn't the end of the world to have a session.Email that isn't an email in cases where the subject claim isn't an email (with it as blank or a non-email from the subject -- both routes require --emails-domains=* to validate appropriately).

I'm not sure this is best handled in the #560 -- its that weird middle ground for how do we agnostically treat extra JWT bearers that aren't from the main provider in a generic way.

IE my case, I like my OIDC provider settings have the user-id claim to email like the default, but I don't want to change that to something just to appease compatibility with whatever extra JWT Bearer Verifiers I add since their IDToken structures could feasibly be all over the place (subject/audience is likely the only thing we can rely on it having).

@NickMeves NickMeves force-pushed the extra-jwt-token-session branch 3 times, most recently from 09df48d to 7d5c8ff Compare June 4, 2020 19:16
JoelSpeed
JoelSpeed previously approved these changes Jun 7, 2020
Copy link
Member

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

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

@NickMeves Can you just confirm this is good to go from your perspective, I believe it is and have no further comments

@steakunderscore I'd appreciate if you could also review this one before merging

rawIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, minimalIDToken).SignedString(key)
assert.NoError(t, err)
// Pass to a dummy Verifier to get an oidc.IDToken from the rawIDToken for our actual test below
idToken, err := verifier.Verify(context.Background(), rawIDToken)
Copy link
Member Author

Choose a reason for hiding this comment

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

There's got to be a cleaner way to get an oidc.IDToken from a rawIDToken string...? Getting it through a dummy oidc.NewVerifier worked, but just feels sloppy to me -- but nothing stood out to me when I tried to dig through oidc 🤷

Copy link
Member

Choose a reason for hiding this comment

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

It is just a struct at the end of the day, you could theoretically construct it, but then you can't set the private methods. I think what you've done is probably the best we can do sadly

@NickMeves
Copy link
Member Author

@NickMeves Can you just confirm this is good to go from your perspective, I believe it is and have no further comments

@steakunderscore I'd appreciate if you could also review this one before merging

Yup, ready to go. I ran some manual tests in a local kubernetes environment and it looked good.

Only area I had a lingering question is the new unit test I added that feels a little sloppy.

JoelSpeed
JoelSpeed previously approved these changes Jun 14, 2020
JoelSpeed
JoelSpeed previously approved these changes Jun 19, 2020
@NickMeves
Copy link
Member Author

Give me the heads up when we're ready and I'll rebase before the merge. I think I keep dismissing your reviews when I push up rebases 😄

@JoelSpeed
Copy link
Member

I think let's just get this merged, can you rebase?

Nick Meves added 3 commits June 19, 2020 11:47
When using the configured provider JWT Verifier, it makes
sense to use the provider `CreateSessionStateFromBearerToken`
method. For any extra JWT Issuers, they should use a generic
default verifier.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants