Files
kubelogin/pkg/usecases/authentication/authcode/browser.go
2025-01-12 21:41:20 +09:00

119 lines
3.5 KiB
Go

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)
}
}