Skip to content

Commit

Permalink
refactor: use stateless state-machine for orchestrating oauth login
Browse files Browse the repository at this point in the history
  • Loading branch information
HandOfGod94 committed Oct 15, 2023
1 parent 12984bd commit 5d7c14f
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 24 deletions.
4 changes: 3 additions & 1 deletion cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ as Atlassian currently doesn't support PKCE verification for oauth flow.`,
switch args[0] {
case "login":
a := jira.NewAuthenticator()
a.Login(context.Background())
if err := a.Login(context.Background()); err != nil {
return err
}
case "logout":
panic("To be implemented")
default:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/handofgod94/gh-jira-changelog
go 1.20

require (
github.com/qmuntal/stateless v1.6.7
github.com/samber/lo v1.38.1
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cobra v1.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/qmuntal/stateless v1.6.7 h1:Dy54xezXfKnH/udv607vFKOQGMqe8wD5/JsHk5dfgC4=
github.com/qmuntal/stateless v1.6.7/go.mod h1:n1HjRBM/cq4uCr3rfUjaMkgeGcd+ykAZwkjLje6jGBM=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down
120 changes: 97 additions & 23 deletions pkg/jira_changelog/jira/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,85 @@ import (
"net/http"
"time"

"github.com/qmuntal/stateless"
"github.com/skratchdot/open-golang/open"
"golang.org/x/exp/slog"
"golang.org/x/oauth2"
)

const (
// states
stateInit = "Init"
stateOauthConfigSetup = "OauthConfigSetup"
stateTokenObtained = "TokenObtained"
stateAccessibleResourcesObtained = "AccessibleResourcesObtained"

// events
triggerSetupOauthConfig = "SetupOauthConfig"
triggerCodeExchange = "CodeExchange"
triggerFetchAccessibleResources = "FetchAccessibleResources"
)

type Authenticator struct {
loginWorkflow *stateless.StateMachine

oauthToken *oauth2.Token
conf *oauth2.Config
verifier string
ctx context.Context
callback chan *oauth2.Token
}

func NewAuthenticator() *Authenticator {
conf := &oauth2.Config{
a := &Authenticator{}

loginWorkflow := stateless.NewStateMachine(stateInit)
loginWorkflow.Configure(stateInit).
Permit(triggerSetupOauthConfig, stateOauthConfigSetup)

loginWorkflow.Configure(stateOauthConfigSetup).
OnEntry(a.setupOauthConfig).
Permit(triggerCodeExchange, stateTokenObtained, a.isVerifierPresent)

loginWorkflow.Configure(stateTokenObtained).
OnEntry(a.exchangeCode).
Permit(triggerFetchAccessibleResources, stateAccessibleResourcesObtained, a.isTokenValid, a.isOauthContextPresent)

loginWorkflow.Configure(stateAccessibleResourcesObtained).
OnEntry(a.fetchAccessibleResources)

a.loginWorkflow = loginWorkflow

return a
}

func (a *Authenticator) Login(ctx context.Context) error {
a.loginWorkflow.Fire(triggerSetupOauthConfig)

if err := a.loginWorkflow.FireCtx(ctx, triggerCodeExchange); err != nil {
return err
}

if err := a.loginWorkflow.FireCtx(ctx, triggerFetchAccessibleResources); err != nil {
return err
}
return nil
}

func (a *Authenticator) isVerifierPresent(ctx context.Context, args ...any) bool {
return a.verifier != ""
}

func (a *Authenticator) isTokenValid(ctx context.Context, args ...any) bool {
return a.oauthToken != nil && a.oauthToken.Valid()
}

func (a *Authenticator) isOauthContextPresent(ctx context.Context, args ...any) bool {
return a.ctx != nil
}

func (a *Authenticator) setupOauthConfig(ctx context.Context, args ...any) error {
a.conf = &oauth2.Config{
ClientID: "OOGf9PTJL0hGGC5hWD17G6OkiGKjO0FG",
ClientSecret: "ATOAhihA9MN3TOWAJEC4DxxPZMxGyjmA_mH8rUtSGXRIoUP6WQ3UvjCk5Mtx9TUBH6JF089B37D6",
Endpoint: oauth2.Endpoint{
Expand All @@ -33,13 +98,13 @@ func NewAuthenticator() *Authenticator {
Scopes: []string{"read:jira-work"},
}

return &Authenticator{
conf: conf,
}
a.verifier = oauth2.GenerateVerifier()

slog.Info("Configured oauth")
return nil
}

func (a *Authenticator) Login(ctx context.Context) error {
a.verifier = oauth2.GenerateVerifier()
func (a *Authenticator) exchangeCode(ctx context.Context, args ...any) error {
url := a.conf.AuthCodeURL("state", oauth2.S256ChallengeOption(a.verifier),
oauth2.SetAuthURLParam("response_type", "code"),
oauth2.SetAuthURLParam("prompt", "consent"),
Expand All @@ -59,41 +124,50 @@ func (a *Authenticator) Login(ctx context.Context) error {
},
})

http.HandleFunc("/gh-jira-changelog/oauth/callback", http.HandlerFunc(a.callbackHandler))
http.ListenAndServe(":9999", nil)
// sping up server for callback from RedirectURL and shut it down once we get response
a.callback = make(chan *oauth2.Token)
mux := http.NewServeMux()
mux.HandleFunc("/gh-jira-changelog/oauth/callback", http.HandlerFunc(a.callbackHandler))
svr := http.Server{Addr: "127.0.0.1:9999", Handler: mux}
go func() { svr.ListenAndServe() }()

a.oauthToken = <-a.callback
svr.Shutdown(a.ctx)
return nil
}

func (a *Authenticator) Client() *http.Client {
return a.conf.Client(a.ctx, a.oauthToken)
}

func (a *Authenticator) callbackHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
tok, err := a.conf.Exchange(a.ctx, code, oauth2.VerifierOption(a.verifier))
if err != nil {
log.Fatal(err)
}

a.oauthToken = tok

func (a *Authenticator) fetchAccessibleResources(ctx context.Context, args ...any) error {
resp, err := a.Client().Get("https://api.atlassian.com/oauth/token/accessible-resources")
if err != nil || resp.StatusCode != http.StatusOK {
slog.Error("Failed to fetch accessible-resource from jira", "error", err)
slog.Error("Response information", "response_status", resp.Status)
panic(err)
return err
}

accessibleResources, err := io.ReadAll(resp.Body)
if err != nil {
slog.Error("failed to read accessibleResources", "error", err)
panic(err)
return err
}
slog.Debug("Retrieved accessible resources successfully", "resources", string(accessibleResources))

return nil
}

func (a *Authenticator) Client() *http.Client {
return a.conf.Client(a.ctx, a.oauthToken)
}

func (a *Authenticator) callbackHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
token, err := a.conf.Exchange(a.ctx, code, oauth2.VerifierOption(a.verifier))
if err != nil {
log.Fatal(err)
}

msg := "<h1>Authentication successful!</h1>"
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"
fmt.Fprintln(w, msg)

a.callback <- token
}

0 comments on commit 5d7c14f

Please sign in to comment.