mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-14 16:39:51 +00:00
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:
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user