From b60bfcb1b079f8c5ad9f2589122bf5bee7b2faaf Mon Sep 17 00:00:00 2001 From: Aaron Tye Date: Tue, 13 Jul 2021 11:31:47 -0400 Subject: [PATCH] validate access token --- internal/auth/authentication.go | 55 ++++++++++++++++++++------------- internal/cmd/entrypoint.go | 37 ++++++++++++++++++++-- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/internal/auth/authentication.go b/internal/auth/authentication.go index affc10f..7382ef1 100644 --- a/internal/auth/authentication.go +++ b/internal/auth/authentication.go @@ -46,13 +46,18 @@ var ( errNoAccessToken = errors.New("access_token not found") ) -func GetOAuthToken(conf *oauth2.Config, verifier TokenVerifyier, u Utils) (string, error) { - state, err := u.NewUUID() +func GetOAuthToken(conf *oauth2.Config, verifier TokenVerifyier, util Utils) (string, error) { + state, err := util.NewUUID() if err != nil { return "", err } - nonce, err := u.NewUUID() + nonce, err := util.NewUUID() + if err != nil { + return "", err + } + + u, err := url.Parse(conf.RedirectURL) if err != nil { return "", err } @@ -62,12 +67,20 @@ func GetOAuthToken(conf *oauth2.Config, verifier TokenVerifyier, u Utils) (strin return "", err } - err = u.OpenURL(addr) + opts := redirectOpts{ + state: state, + nonce: nonce, + port: port{Port: u.Port()}, + t: template.Must(template.New("example").Parse(html)), + verifier: verifier, + } + + err = util.OpenURL(addr) if err != nil { return "", err } - token, err := listenForToken(state, nonce, conf, verifier) + token, err := listenForRedirect(opts) if err != nil { return "", err } @@ -86,24 +99,24 @@ func buildUserLoginURL(conf *oauth2.Config, state string, nonce string) (string, return strings.Replace(authURL.String(), "response_type=code", "response_type=token+id_token", 1), nil } -func listenForToken(state string, nonce string, conf *oauth2.Config, verifier TokenVerifyier) (accessToken string, err error) { - u, err := url.Parse(conf.RedirectURL) - if err != nil { - return "", err - } - - var port struct { - Port string - } - port.Port = u.Port() +type redirectOpts struct { + state string + nonce string + port port + t *template.Template + verifier TokenVerifyier +} - t := template.Must(template.New("example").Parse(html)) +type port struct { + Port string +} - svr := &http.Server{Addr: fmt.Sprintf(":%s", u.Port())} +func listenForRedirect(opts redirectOpts) (accessToken string, err error) { + svr := &http.Server{Addr: fmt.Sprintf(":%s", opts.port.Port)} svr.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": - err = t.Execute(w, &port) + err = opts.t.Execute(w, opts.port) if err != nil { err = fmt.Errorf("failed to execute template: %v", err) return @@ -118,7 +131,7 @@ func listenForToken(state string, nonce string, conf *oauth2.Config, verifier To data := strings.Split(r.URL.Query().Get("data"), "&") var idToken IDToken - if s := getValue(data, "state"); s != state { + if s := getValue(data, "state"); s != opts.state { err = errFailedStateValidation return } @@ -129,7 +142,7 @@ func listenForToken(state string, nonce string, conf *oauth2.Config, verifier To return } - idToken, err = verifier.Verify(context.Background(), tkn) + idToken, err = opts.verifier.Verify(context.Background(), tkn) if err != nil { err = fmt.Errorf("failed to verify access token: %v", err) return @@ -145,7 +158,7 @@ func listenForToken(state string, nonce string, conf *oauth2.Config, verifier To return } - if claims.Nonce != nonce { + if claims.Nonce != opts.nonce { err = errFailedNonceValidation return } diff --git a/internal/cmd/entrypoint.go b/internal/cmd/entrypoint.go index ffa64bc..477a372 100644 --- a/internal/cmd/entrypoint.go +++ b/internal/cmd/entrypoint.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "math/rand" + "net/http" "os" "path/filepath" "strings" @@ -51,9 +52,10 @@ const ( ) var ( - ErrNoChannel = errors.New("no channel provided") - ErrNoClientID = errors.New("no clientID in configuration file") - ErrNoUsername = errors.New("no username in configuration file") + ErrNoChannel = errors.New("no channel provided") + ErrNoClientID = errors.New("no clientID in configuration file") + ErrNoUsername = errors.New("no username in configuration file") + ErrInvalidAccessToken = errors.New("invalid access token") ) func NewRootCmd() *cobra.Command { @@ -111,6 +113,11 @@ ttchat --channel ludwig --lines 5 errExit(err) } + err = validateAccessToken(accessToken) + if err != nil { + errExit(err) + } + // Get user display name tc, err := helix.NewClient(&helix.Options{ ClientID: conf.ClientID, @@ -217,6 +224,30 @@ func getAccessToken(tokenFlagValue string, conf Config, verifier auth.TokenVerif return t, nil } +func validateAccessToken(accessToken string) error { + r, err := http.NewRequest("GET", "https://id.twitch.tv/oauth2/validate", nil) + if err != nil { + return err + } + + r.Header.Set("Authorization", fmt.Sprintf("OAuth %s", accessToken)) + resp, err := http.DefaultClient.Do(r) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusUnauthorized { + return ErrInvalidAccessToken + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("invaild access token: status code: %d", resp.StatusCode) + } + + return nil +} + type twitchAPI interface { GetUsers(params *helix.UsersParams) (*helix.UsersResponse, error) }