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

Add new --oidc-use-access-token flag to get-token #1084

Merged
merged 4 commits into from
Aug 16, 2024

Conversation

adkafka
Copy link
Contributor

@adkafka adkafka commented Apr 25, 2024

Implements #1083. See description there for context.

In its current form, this PR is bare bones functionality. I have not yet added any tests to confirm this behavior. Additionally, we could consider updating some of the naming. It is confusing to return a TokenSet where IDToken actually has an accessToken. I'm open to feedback on how best to improve this. However, changes to this may effect which token is cached and validated. So I'm hesitant to make this change unless we think it is necessary.

However, this PR is functional. I have validated it locally. Without adding --oidc-use-access-token, and id_token is successfully returned. Adding --oidc-use-access-token results in an access_token being successfully returned.

Implements int128#1083. See
description there for context.

In its current form, this PR is bare bones functionality. I have not yet
added any tests to confirm this behavior. Additionally, we could
consider updtating some of the naming. It is confusing to return a
`TokenSet` where `IDToken` actually has an `accessToken`. I'm open to
feedback on how best to improve this.

However, this PR is functional. I have validated it locally. Without
adding `--oidc-use-access-token`, and `id_token` is successfully
returned. Adding `--oidc-use-access-token` results in an `access_token`
being successfully returned.
IDToken: accessToken,
RefreshToken: token.RefreshToken,
}, nil
}
Copy link

Choose a reason for hiding this comment

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

Don't you think it should be enough to select the other token here?

idToken, ok := token.Extra("id_token").(string)

The way how it's implemented now would actually lead to a verification of both tokens if --oidc-use-access-token is set, which is in my opinion not necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is a valid alternative approach. I chose not do that initially here because the id_token has a nonce while the access_token does not. So if we only validate the access_token, we will lose the nonce check. It seemed preferred to me to maintain this nonce check but use the access_token.

@adkafka
Copy link
Contributor Author

adkafka commented May 21, 2024

I realize I have some tests failing because I haven't updated the mocks (mockery --all --inpackage --with-expecter). Fixing that and adding at least one test shortly.

EDIT: This actually isn't so easy. The version of mockery used previously (v2.13.1) seems incompatible with our current go version. We are running into this issue: https://vektra.github.io/mockery/v2.35/notes/#internal-error-package-without-types-was-imported

Adam kafka added 2 commits May 21, 2024 15:07
Needed to plumb through our new parameter `UseAccessToken` to the mocks
as well.
// New provides a mock function with given fields: ctx, p, tlsClientConfig
func (_m *MockFactoryInterface) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error) {
// New provides a mock function with given fields: ctx, p, tlsClientConfig, useAccessToken
func (_m *MockFactoryInterface) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config, useAccessToken bool) (Interface, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that I had to manually make these changes, which is not great. I couldn't get mockery v2.13.1 to work on my machine. I believe we have to upgrade to a newer version of mockery to work with this version of Go. I was getting this error:

$ ~/Downloads/mockery_2.13.1_Darwin_x86_64/mockery --all --inpackage --with-expecter
21 May 24 15:11 PDT INF Starting mockery dry-run=false version=v2.13.1
21 May 24 15:11 PDT INF Walking dry-run=false version=v2.13.1
2024/05/21 15:11:45 internal error: package "context" without types was imported from "github.com/int128/kubelogin/integration_test/httpdriver"

Which is mentioned here: https://vektra.github.io/mockery/v2.35/notes/#error-no-go-files-found-in-root-search-path. I decided not to tackle this in this PR. We can discuss doing so in a follow-up PR if that makes sense.

@adkafka
Copy link
Contributor Author

adkafka commented May 21, 2024

@int128 Are you able to take a look at this PR when you have a chance? I'm happy to update if you have suggestions. I wasn't sure if there are any better unit tests to add than the ones I added. And I'm unsure if it is worth updating TokenSet fields such that we actually store the AccessToken as opposed to overloading the IDToken field. Thanks!

if !ok {
return nil, fmt.Errorf("access_token is missing in the token response: %#v", accessToken)
}
_, err := verifier.Verify(ctx, accessToken)
Copy link

Choose a reason for hiding this comment

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

I think there is no need to verify the token on the client. We're not granting the user any access based on the content of the token. Instead we're just passing the token on to the k8s API, where it has to be verified.

Copy link

Choose a reason for hiding this comment

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

If we stick with verifying the token, the correct audience (client_id) would have to be passed to the verifier in line 201. The aud of the access_token is not necessarily the client_id requesting the token.

Copy link

@jsphpl jsphpl Jun 1, 2024

Choose a reason for hiding this comment

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

Addition: The OIDC spec does not mandate the format of an access token. So my comment about the audience when verifying the access token is irrelevant.

https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation describes the verification of the access token using the at_hash claim. This of course requires a valid signature of the id token. Which brings us back to the question whether we should do any verification of the tokens at all, given we're the client and not the resource server.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think there is no need to verify the token on the client. We're not granting the user any access based on the content of the token. Instead we're just passing the token on to the k8s API, where it has to be verified.

I see your point here. Kubelogin is only passing the token to kubernetes, so we don't gain much by validating it. However, we currently validate the id_token, so my inclination is to follow that precedent in this PR. If we want to add a CLI flag to disable local token verification, I think that could make sense. But I'd argue that is out of scope of this particular issue / PR.

If we stick with verifying the token, the correct audience (client_id) would have to be passed to the verifier in line 201. The aud of the access_token is not necessarily the client_id requesting the token.

Interesting. In my case, the client_id is the aud for both the id_token and the access_token. Is this a common case worth supporting? Or do we think it is sufficient to (in a separate PR) add a flag that disables local token validation?

Addition: The OIDC spec does not mandate the format of an access token. So my comment about the audience when verifying the access token is irrelevant.

https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation describes the verification of the access token using the at_hash claim. This of course requires a valid signature of the id token. Which brings us back to the question whether we should do any verification of the tokens at all, given we're the client and not the resource server.

Correct me if I'm wrong, but that validation only applies to access tokens acquired from the "authorization" endpoint. In my flow at least, we are using the "Authentication Code" flow. First, we make a call to /authorize to optian an authorization code, then we make a call to /token to obtain the id_token and access_token. In my case, the id_token will only populate the at_hash claim if the token is obtained using the "implicit" flow, where the token is returned directly from the /authorize call.

Copy link

Choose a reason for hiding this comment

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

Is this a common case worth supporting?

Many OIDC IdP will issue an access token with the same structure as an OIDC ID Token. But as far as my understanding goes, this is incidental and not mandated by the spec. So validating an access token the same way one would validate an ID token might or might not work. The access token could be an opaque string that only has meaning to the resource server.

If the access token is a JWT after the OIDC spec (as with Azure, Auth0, etc.), the only reasonable value for aud, in my opinion, would be the client ID of the resource server because that's who the access token is meant for.

So I would vote for either a) not verify the access token at all or b) only verify it using the at_hash, if present. Tendency towards a) because b) implies verifying the ID token, which, in my opinion, is pointless on the client side.

In my flow at least, we are using the "Authentication Code" flow

Section 3.1.3.8. of the same Document describes the same procedure for the auth code flow. The at_hash claim is optional in that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I would vote for either a) not verify the access token at all or b) only verify it using the at_hash, if present. Tendency towards a) because b) implies verifying the ID token, which, in my opinion, is pointless on the client side.

Given that we currently verify the ID token in this application and changing that is out of scope of this PR, I don't think option a) fits well in here. As I mentioned earlier, I'd like this change to match the existing precedence set by this application, at least until the maintainer indicates otherwise. Perhaps a good in-between we can consider is to check the signature of the access_token but not verify the aud claim matches the client_id. I think we can accomplish this by creating a different verifier for the access_token. Here is the one we create for the id_token:

https://github.com/int128/kubelogin/blob/f9c3c8aee74ff81784c5d5e1b8c2cbcc76aff8a5/pkg/oidc/client/client.go#L201C25-L201C33

We can instead set ClientID to a blank string and set SkipClientIDCheck to true. See config parameters here. As the docs for go-oidc state, there are some cases where the aud will not match the client_id. This use of an access_token seems to fit that use case

Section 3.1.3.8. of the same Document describes the same procedure for the auth code flow. The at_hash claim is optional in that case.

Agreed. Using the at_hash route for validation is a possibility. But that field is optional, so it won't work to validate the token in all cases. If I understand correctly, this validation route (using at_hash) is primarily useful when the access_token is NOT a signed JWT. If the access_token is a signed JWT (which it will have to be to successfully use this code path for kubernetes), then it is preferable to me to validate the JWT the way we are currently (with the potential caveat above about intentionally not checking the aud claim).

Copy link

Choose a reason for hiding this comment

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

Sounds good to me.

which it will have to be to successfully use this code path for kubernetes

Very good point!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jsphpl I just pushed 1add63b which should resolve this issue.

@jsphpl
Copy link

jsphpl commented May 31, 2024

I can confirm that the --oidc-use-access-token works as intended.

However, I had to disable verification of the access_token because I passed --oidc-auth-request-extra-params="audience=CLUSTER_CLIENT_ID". Verifying the access_token is not necessary anyway, see my comments above.

Btw. verifying the id_token isn't necessay either, because we're just the client. Verification has to happen on the resource server (k8s API). We're not granting the user any access – we're merely passing the token to the resource server. Or are we?

As noted in the PR, there are some cases where the access token `aud`
field will not be the `client_id`. To allow for these, we use a
different token verifier that will not verify that claim.
@adkafka
Copy link
Contributor Author

adkafka commented Jun 17, 2024

However, I had to disable verification of the access_token because I passed --oidc-auth-request-extra-params="audience=CLUSTER_CLIENT_ID". Verifying the access_token is not necessary anyway, see my comments above.

This should be resolved now, after I pushed 1add63b.

@adkafka adkafka requested a review from jsphpl June 17, 2024 23:14
@glaberge
Copy link

We use Okta and would also benefit from this as it would solve this issue as well (#863) because the id_token has a hard coded lifetime of 60 minutes where the access token is configurable.

@glaberge
Copy link

glaberge commented Jul 18, 2024

@adkafka I was not able to use your patch with Okta. Probably something wrong with my config but essentially I'm using this:

kind: Config
preferences: {}
users:
    - name: okta
      user:
        exec:
            apiVersion: client.authentication.k8s.io/v1beta1
            args:
                - oidc-login-custom
                - get-token
                - --oidc-use-access-token=true
                - --oidc-issuer-url=https://paramountcommerce.okta.com/oauth2/<redacted>
                - --oidc-client-id=<redacted>
                - --oidc-extra-scope=email
                - --oidc-extra-scope=offline_access
                - --oidc-extra-scope=profile
                - --oidc-extra-scope=openid
                - --v=5
                - --force-refresh
                #- --skip-open-browser
            command: kubectl

I'm able to get a token that I can see in the cache, also getting a message that my token is valid (for 8 hours now yeah!), but I get 2 browser windows opening/closing one after the other. Verbose logging seems to indicate is makes 8 requests, 6 200OK and 2 401Unauthorized, and still ends up with a message from kubectl that error: You must be logged in to the server (Unauthorized). Removing - --oidc-use-access-token=true works as usual with your patch.

Just wondering if you have an idea of what's happening.

I tried various combinations within kubelogin and Okta but can't seem to get it to work.

@adkafka
Copy link
Contributor Author

adkafka commented Jul 18, 2024

@adkafka I was not able to use your patch with Okta. Probably something wrong with my config but essentially I'm using this:

kind: Config
preferences: {}
users:
    - name: okta
      user:
        exec:
            apiVersion: client.authentication.k8s.io/v1beta1
            args:
                - oidc-login-custom
                - get-token
                - --oidc-use-access-token=true
                - --oidc-issuer-url=https://paramountcommerce.okta.com/oauth2/<redacted>
                - --oidc-client-id=<redacted>
                - --oidc-extra-scope=email
                - --oidc-extra-scope=offline_access
                - --oidc-extra-scope=profile
                - --oidc-extra-scope=openid
                - --v=5
                - --force-refresh
                #- --skip-open-browser
            command: kubectl

I'm able to get a token that I can see in the cache, also getting a message that my token is valid (for 8 hours now yeah!), but I get 2 browser windows opening/closing one after the other. Verbose logging seems to indicate is makes 8 requests, 6 200OK and 2 401Unauthorized, and still ends up with a message from kubectl that error: You must be logged in to the server (Unauthorized). Removing - --oidc-use-access-token=true works as usual with your patch.

Just wondering if you have an idea of what's happening.

I tried various combinations within kubelogin and Okta but can't seem to get it to work.

I don't know off hand what the issue is. When I run in debug logging, I also do see some 4xx errors on some calls. Seems like the underlying library does some probing on the endpoints during the flow which can result in errors, but the flow itself can still work.

The browser window opening twice is interesting, I'm not sure what would cause that. Are you able to determine at what part of the flow the window opens?

Can you independently check that the JWT used (logged in debug mode) is valid, signed by the expected issuer, and has the expected audience (and any other checks) that you have configured your kubernetes cluster to perform? Given that the kube-login tool is able to successfully grab the token and use it against your kubernetes cluster (but the usage of the token fails), I suspect that the issue lies in the server-side configuration you have for your kubernetes cluster. IME, kubectl returns an opaque error such as the one you mentioned when the token used is not valid based on the kubernetes server-side configuration.

@glaberge
Copy link

glaberge commented Jul 23, 2024

For anyone using Okta and EKS, here's how I managed to fix this:
In your custom auth server

  • Change your audience to your client ID (cid found in app)
  • Add a groups claim
  • Add/Modify sub claim with the value of user.getInternalProperty("id") for Access Tokens
  • Add an email claim of user.email for Access Tokens

@adkafka
Copy link
Contributor Author

adkafka commented Jul 30, 2024

@int128 Are you able to review this PR? It seems to have already helped three people in the community (myself, @jsphpl and @glaberge). It would be great to get your review so others can benefit as well. Thanks!

Copy link
Owner

@int128 int128 left a comment

Choose a reason for hiding this comment

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

LGTM. Thank you for the great contribution!

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.

5 participants