// Package client provides a client of OpenID Connect. package client import ( "context" "fmt" "net/http" "slices" gooidc "github.com/coreos/go-oidc/v3/oidc" "github.com/google/wire" "github.com/int128/kubelogin/pkg/infrastructure/clock" "github.com/int128/kubelogin/pkg/infrastructure/logger" "github.com/int128/kubelogin/pkg/oidc" "github.com/int128/kubelogin/pkg/oidc/client/logging" "github.com/int128/kubelogin/pkg/pkce" "github.com/int128/kubelogin/pkg/tlsclientconfig" "github.com/int128/kubelogin/pkg/tlsclientconfig/loader" "golang.org/x/oauth2" ) var Set = wire.NewSet( wire.Struct(new(Factory), "*"), wire.Bind(new(FactoryInterface), new(*Factory)), ) type FactoryInterface interface { New(ctx context.Context, prov oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error) } type Factory struct { Loader loader.Loader Clock clock.Interface Logger logger.Interface } // New returns an instance of infrastructure.Interface with the given configuration. func (f *Factory) New(ctx context.Context, prov oidc.Provider, tlsClientConfig tlsclientconfig.Config) (Interface, error) { rawTLSClientConfig, err := f.Loader.Load(tlsClientConfig) if err != nil { return nil, fmt.Errorf("could not load the TLS client config: %w", err) } baseTransport := &http.Transport{ TLSClientConfig: rawTLSClientConfig, Proxy: http.ProxyFromEnvironment, } loggingTransport := &logging.Transport{ Base: baseTransport, Logger: f.Logger, } httpClient := &http.Client{ Transport: loggingTransport, } ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) provider, err := gooidc.NewProvider(ctx, prov.IssuerURL) if err != nil { return nil, fmt.Errorf("oidc discovery error: %w", err) } supportedPKCEMethods, err := extractSupportedPKCEMethods(provider) if err != nil { return nil, fmt.Errorf("could not determine supported PKCE methods: %w", err) } deviceAuthorizationEndpoint, err := extractDeviceAuthorizationEndpoint(provider) if err != nil { return nil, fmt.Errorf("could not determine device authorization endpoint: %w", err) } return &client{ httpClient: httpClient, provider: provider, oauth2Config: oauth2.Config{ Endpoint: provider.Endpoint(), ClientID: prov.ClientID, ClientSecret: prov.ClientSecret, Scopes: append(prov.ExtraScopes, gooidc.ScopeOpenID), }, clock: f.Clock, logger: f.Logger, negotiatedPKCEMethod: determinePKCEMethod(supportedPKCEMethods, prov.PKCEMethod), deviceAuthorizationEndpoint: deviceAuthorizationEndpoint, useAccessToken: prov.UseAccessToken, }, nil } func determinePKCEMethod(supportedMethods []string, preferredMethod oidc.PKCEMethod) pkce.Method { switch preferredMethod { case oidc.PKCEMethodNo: return pkce.NoMethod case oidc.PKCEMethodS256: return pkce.MethodS256 default: if slices.Contains(supportedMethods, "S256") { return pkce.MethodS256 } return pkce.NoMethod } } func extractSupportedPKCEMethods(provider *gooidc.Provider) ([]string, error) { var claims struct { CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` } if err := provider.Claims(&claims); err != nil { return nil, fmt.Errorf("invalid discovery document: %w", err) } return claims.CodeChallengeMethodsSupported, nil } func extractDeviceAuthorizationEndpoint(provider *gooidc.Provider) (string, error) { var claims struct { DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"` } if err := provider.Claims(&claims); err != nil { return "", fmt.Errorf("invalid discovery document: %w", err) } return claims.DeviceAuthorizationEndpoint, nil }