package authcode import ( "context" "fmt" "time" "github.com/int128/kubelogin/pkg/infrastructure/browser" "github.com/int128/kubelogin/pkg/infrastructure/logger" "github.com/int128/kubelogin/pkg/oidc" "github.com/int128/kubelogin/pkg/oidc/client" "github.com/int128/kubelogin/pkg/pkce" "golang.org/x/sync/errgroup" ) type BrowserOption struct { SkipOpenBrowser bool BrowserCommand string BindAddress []string AuthenticationTimeout time.Duration OpenURLAfterAuthentication string RedirectURLHostname string AuthRequestExtraParams map[string]string LocalServerCertFile string LocalServerKeyFile string } // Browser provides the authentication code flow using the browser. type Browser struct { Browser browser.Interface Logger logger.Interface } func (u *Browser) Do(ctx context.Context, o *BrowserOption, oidcClient client.Interface) (*oidc.TokenSet, error) { u.Logger.V(1).Infof("starting the authentication code flow using the browser") state, err := oidc.NewState() if err != nil { return nil, fmt.Errorf("could not generate a state: %w", err) } nonce, err := oidc.NewNonce() if err != nil { return nil, fmt.Errorf("could not generate a nonce: %w", err) } pkceParams, err := pkce.New(oidcClient.NegotiatedPKCEMethod()) if err != nil { return nil, fmt.Errorf("could not generate the PKCE parameters: %w", err) } successHTML := BrowserSuccessHTML if o.OpenURLAfterAuthentication != "" { successHTML = BrowserRedirectHTML(o.OpenURLAfterAuthentication) } in := client.GetTokenByAuthCodeInput{ BindAddress: o.BindAddress, State: state, Nonce: nonce, PKCEParams: pkceParams, RedirectURLHostname: o.RedirectURLHostname, AuthRequestExtraParams: o.AuthRequestExtraParams, LocalServerSuccessHTML: successHTML, LocalServerCertFile: o.LocalServerCertFile, LocalServerKeyFile: o.LocalServerKeyFile, } ctx, cancel := context.WithTimeout(ctx, o.AuthenticationTimeout) defer cancel() readyChan := make(chan string, 1) var out *oidc.TokenSet var eg errgroup.Group eg.Go(func() error { select { case url, ok := <-readyChan: if !ok { return nil } u.openURL(ctx, o, url) return nil case <-ctx.Done(): return fmt.Errorf("context cancelled while waiting for the local server: %w", ctx.Err()) } }) eg.Go(func() error { defer close(readyChan) tokenSet, err := oidcClient.GetTokenByAuthCode(ctx, in, readyChan) if err != nil { return fmt.Errorf("authorization code flow error: %w", err) } out = tokenSet u.Logger.V(1).Infof("got a token set by the authorization code flow") return nil }) if err := eg.Wait(); err != nil { return nil, fmt.Errorf("authentication error: %w", err) } u.Logger.V(1).Infof("finished the authorization code flow via the browser") return out, nil } func (u *Browser) openURL(ctx context.Context, o *BrowserOption, url string) { if o.SkipOpenBrowser { u.Logger.Printf("Please visit the following URL in your browser: %s", url) return } u.Logger.V(1).Infof("opening %s in the browser", url) if o.BrowserCommand != "" { if err := u.Browser.OpenCommand(ctx, url, o.BrowserCommand); err != nil { u.Logger.Printf(`error: could not open the browser: %s Please visit the following URL in your browser manually: %s`, err, url) } return } if err := u.Browser.Open(url); err != nil { u.Logger.Printf(`error: could not open the browser: %s Please visit the following URL in your browser manually: %s`, err, url) } }