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

* Add new `--oidc-use-access-token` flag to `get-token`

Implements https://github.com/int128/kubelogin/issues/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.

* Fix failing tests

Needed to plumb through our new parameter `UseAccessToken` to the mocks
as well.

* Add a test to make sure new flag is plumbed through

* Support Access Tokens whose audience differ from the client_id

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.

---------

Co-authored-by: Adam kafka <akafka@tesla.com>
This commit is contained in:
Adam Kafka
2024-08-16 00:57:05 -07:00
committed by GitHub
parent 70ce255a8d
commit 905238ce07
10 changed files with 82 additions and 13 deletions

View File

@@ -123,6 +123,7 @@ func TestCmd_Run(t *testing.T) {
RedirectURLHostname: "localhost",
},
},
UseAccessToken: false,
},
},
"FullOptions": {
@@ -150,6 +151,30 @@ func TestCmd_Run(t *testing.T) {
RedirectURLHostname: "localhost",
},
},
UseAccessToken: false,
},
},
"AccessToken": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-use-access-token=true",
},
in: credentialplugin.Input{
TokenCacheDir: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
Provider: oidc.Provider{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
},
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
UseAccessToken: true,
},
},
"HomedirExpansion": {
@@ -180,6 +205,7 @@ func TestCmd_Run(t *testing.T) {
TLSClientConfig: tlsclientconfig.Config{
CACertFilename: []string{filepath.Join(userHomeDir, ".kube/ca.crt")},
},
UseAccessToken: false,
},
},
}

View File

@@ -18,6 +18,7 @@ type getTokenOptions struct {
ClientSecret string
ExtraScopes []string
UsePKCE bool
UseAccessToken bool
TokenCacheDir string
tlsOptions tlsOptions
authenticationOptions authenticationOptions
@@ -30,6 +31,7 @@ func (o *getTokenOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
f.BoolVar(&o.UsePKCE, "oidc-use-pkce", false, "Force PKCE usage")
f.BoolVar(&o.UseAccessToken, "oidc-use-access-token", false, "Instead of using the id_token, use the access_token to authenticate to Kubernetes")
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for token cache")
f.BoolVar(&o.ForceRefresh, "force-refresh", false, "If set, refresh the ID token regardless of its expiration time")
o.tlsOptions.addFlags(f)
@@ -85,6 +87,7 @@ func (cmd *GetToken) New() *cobra.Command {
GrantOptionSet: grantOptionSet,
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
ForceRefresh: o.ForceRefresh,
UseAccessToken: o.UseAccessToken,
}
if err := cmd.GetToken.Do(c.Context(), in); err != nil {
return fmt.Errorf("get-token: %w", err)

View File

@@ -15,6 +15,7 @@ type setupOptions struct {
ClientSecret string
ExtraScopes []string
UsePKCE bool
UseAccessToken bool
tlsOptions tlsOptions
authenticationOptions authenticationOptions
}
@@ -25,6 +26,7 @@ func (o *setupOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
f.BoolVar(&o.UsePKCE, "oidc-use-pkce", false, "Force PKCE usage")
f.BoolVar(&o.UseAccessToken, "oidc-use-access-token", false, "Instead of using the id_token, use the access_token to authenticate to Kubernetes")
o.tlsOptions.addFlags(f)
o.authenticationOptions.addFlags(f)
}
@@ -50,6 +52,7 @@ func (cmd *Setup) New() *cobra.Command {
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
UsePKCE: o.UsePKCE,
UseAccessToken: o.UseAccessToken,
GrantOptionSet: grantOptionSet,
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
}

View File

@@ -62,6 +62,7 @@ type client struct {
logger logger.Interface
supportedPKCEMethods []string
deviceAuthorizationEndpoint string
useAccessToken bool
}
func (c *client) wrapContext(ctx context.Context) context.Context {
@@ -205,6 +206,32 @@ func (c *client) verifyToken(ctx context.Context, token *oauth2.Token, nonce str
if nonce != "" && nonce != verifiedIDToken.Nonce {
return nil, fmt.Errorf("nonce did not match (wants %s but got %s)", nonce, verifiedIDToken.Nonce)
}
if c.useAccessToken {
accessToken, ok := token.Extra("access_token").(string)
if !ok {
return nil, fmt.Errorf("access_token is missing in the token response: %#v", accessToken)
}
// We intentionally do not perform a ClientID check here because there
// are some use cases in access_tokens where we *expect* the audience
// to differ. For example, one can explicitly set
// `audience=CLUSTER_CLIENT_ID` as an extra auth parameter.
verifier = c.provider.Verifier(&gooidc.Config{ClientID: "", Now: c.clock.Now, SkipClientIDCheck: true})
_, err := verifier.Verify(ctx, accessToken)
if err != nil {
return nil, fmt.Errorf("could not verify the access token: %w", err)
}
// There is no `nonce` to check on the `access_token`. We rely on the
// above `nonce` check on the `id_token`.
return &oidc.TokenSet{
IDToken: accessToken,
RefreshToken: token.RefreshToken,
}, nil
}
return &oidc.TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,

View File

@@ -24,7 +24,7 @@ var Set = wire.NewSet(
)
type FactoryInterface interface {
New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error)
New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config, useAccessToken bool) (Interface, error)
}
type Factory struct {
@@ -34,7 +34,7 @@ type Factory struct {
}
// New returns an instance of infrastructure.Interface with the given configuration.
func (f *Factory) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error) {
func (f *Factory) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsclientconfig.Config, useAccessToken bool) (Interface, error) {
rawTLSClientConfig, err := f.Loader.Load(tlsClientConfig)
if err != nil {
return nil, fmt.Errorf("could not load the TLS client config: %w", err)
@@ -80,6 +80,7 @@ func (f *Factory) New(ctx context.Context, p oidc.Provider, tlsClientConfig tlsc
logger: f.Logger,
supportedPKCEMethods: supportedPKCEMethods,
deviceAuthorizationEndpoint: deviceAuthorizationEndpoint,
useAccessToken: useAccessToken,
}, nil
}

View File

@@ -24,13 +24,13 @@ func (_m *MockFactoryInterface) EXPECT() *MockFactoryInterface_Expecter {
return &MockFactoryInterface_Expecter{mock: &_m.Mock}
}
// 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) {
ret := _m.Called(ctx, p, tlsClientConfig)
var r0 Interface
if rf, ok := ret.Get(0).(func(context.Context, oidc.Provider, tlsclientconfig.Config) Interface); ok {
r0 = rf(ctx, p, tlsClientConfig)
if rf, ok := ret.Get(0).(func(context.Context, oidc.Provider, tlsclientconfig.Config, bool) Interface); ok {
r0 = rf(ctx, p, tlsClientConfig, useAccessToken)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(Interface)
@@ -38,8 +38,8 @@ func (_m *MockFactoryInterface) New(ctx context.Context, p oidc.Provider, tlsCli
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, oidc.Provider, tlsclientconfig.Config) error); ok {
r1 = rf(ctx, p, tlsClientConfig)
if rf, ok := ret.Get(1).(func(context.Context, oidc.Provider, tlsclientconfig.Config, bool) error); ok {
r1 = rf(ctx, p, tlsClientConfig, useAccessToken)
} else {
r1 = ret.Error(1)
}
@@ -56,7 +56,8 @@ type MockFactoryInterface_New_Call struct {
// - ctx context.Context
// - p oidc.Provider
// - tlsClientConfig tlsclientconfig.Config
func (_e *MockFactoryInterface_Expecter) New(ctx interface{}, p interface{}, tlsClientConfig interface{}) *MockFactoryInterface_New_Call {
// - useAccessToken bool
func (_e *MockFactoryInterface_Expecter) New(ctx interface{}, p interface{}, tlsClientConfig interface{}, useAccessToken bool) *MockFactoryInterface_New_Call {
return &MockFactoryInterface_New_Call{Call: _e.mock.On("New", ctx, p, tlsClientConfig)}
}

View File

@@ -36,6 +36,7 @@ type Input struct {
CachedTokenSet *oidc.TokenSet // optional
TLSClientConfig tlsclientconfig.Config
ForceRefresh bool
UseAccessToken bool
}
type GrantOptionSet struct {
@@ -98,7 +99,7 @@ func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
}
u.Logger.V(1).Infof("initializing an OpenID Connect client")
oidcClient, err := u.ClientFactory.New(ctx, in.Provider, in.TLSClientConfig)
oidcClient, err := u.ClientFactory.New(ctx, in.Provider, in.TLSClientConfig, in.UseAccessToken)
if err != nil {
return nil, fmt.Errorf("oidc error: %w", err)
}

View File

@@ -85,7 +85,7 @@ func TestAuthentication_Do(t *testing.T) {
}, nil)
mockClientFactory := client.NewMockFactoryInterface(t)
mockClientFactory.EXPECT().
New(ctx, dummyProvider, dummyTLSClientConfig).
New(ctx, dummyProvider, dummyTLSClientConfig, false).
Return(mockClient, nil)
u := Authentication{
ClientFactory: mockClientFactory,
@@ -143,7 +143,7 @@ func TestAuthentication_Do(t *testing.T) {
}, nil)
mockClientFactory := client.NewMockFactoryInterface(t)
mockClientFactory.EXPECT().
New(ctx, dummyProvider, dummyTLSClientConfig).
New(ctx, dummyProvider, dummyTLSClientConfig, false).
Return(mockClient, nil)
u := Authentication{
ClientFactory: mockClientFactory,
@@ -190,7 +190,7 @@ func TestAuthentication_Do(t *testing.T) {
}, nil)
mockClientFactory := client.NewMockFactoryInterface(t)
mockClientFactory.EXPECT().
New(ctx, dummyProvider, dummyTLSClientConfig).
New(ctx, dummyProvider, dummyTLSClientConfig, false).
Return(mockClient, nil)
u := Authentication{
ClientFactory: mockClientFactory,

View File

@@ -38,6 +38,7 @@ type Input struct {
GrantOptionSet authentication.GrantOptionSet
TLSClientConfig tlsclientconfig.Config
ForceRefresh bool
UseAccessToken bool
}
type GetToken struct {
@@ -92,6 +93,7 @@ func (u *GetToken) Do(ctx context.Context, in Input) error {
CachedTokenSet: cachedTokenSet,
TLSClientConfig: in.TLSClientConfig,
ForceRefresh: in.ForceRefresh,
UseAccessToken: in.UseAccessToken,
}
authenticationOutput, err := u.Authentication.Do(ctx, authenticationInput)
if err != nil {

View File

@@ -74,6 +74,7 @@ type Stage2Input struct {
ClientSecret string
ExtraScopes []string // optional
UsePKCE bool // optional
UseAccessToken bool // optional
ListenAddressArgs []string // non-nil if set by the command arg
GrantOptionSet authentication.GrantOptionSet
TLSClientConfig tlsclientconfig.Config
@@ -91,6 +92,7 @@ func (u *Setup) DoStage2(ctx context.Context, in Stage2Input) error {
},
GrantOptionSet: in.GrantOptionSet,
TLSClientConfig: in.TLSClientConfig,
UseAccessToken: in.UseAccessToken,
})
if err != nil {
return fmt.Errorf("authentication error: %w", err)
@@ -128,6 +130,9 @@ func makeCredentialPluginArgs(in Stage2Input) []string {
if in.UsePKCE {
args = append(args, "--oidc-use-pkce")
}
if in.UseAccessToken {
args = append(args, "--oidc-use-access-token")
}
for _, f := range in.TLSClientConfig.CACertFilename {
args = append(args, "--certificate-authority="+f)
}