diff --git a/cmd/token.go b/cmd/token.go index 42e97aaa..bebb5f45 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -5,7 +5,9 @@ package cmd import ( "fmt" "strconv" + "time" + "github.com/fatih/color" "github.com/twitchdev/twitch-cli/internal/login" "github.com/spf13/cobra" @@ -15,6 +17,7 @@ import ( var isUserToken bool var userScopes string var revokeToken string +var validateToken string var overrideClientId string var tokenServerPort int var tokenServerIP string @@ -32,6 +35,7 @@ func init() { loginCmd.Flags().BoolVarP(&isUserToken, "user-token", "u", false, "Whether to login as a user or getting an app access token.") loginCmd.Flags().StringVarP(&userScopes, "scopes", "s", "", "Space separated list of scopes to request with your user token.") loginCmd.Flags().StringVarP(&revokeToken, "revoke", "r", "", "Instead of generating a new token, revoke the one passed to this parameter.") + loginCmd.Flags().StringVarP(&validateToken, "validate", "v", "", "Instead of generating a new token, validate the one passed to this parameter.") loginCmd.Flags().StringVar(&overrideClientId, "client-id", "", "Override/manually set client ID for token actions. By default client ID from CLI config will be used.") loginCmd.Flags().StringVar(&tokenServerIP, "ip", "localhost", "Manually set the IP address to be binded to for the User Token web server.") loginCmd.Flags().IntVarP(&tokenServerPort, "port", "p", 3000, "Manually set the port to be used for the User Token web server.") @@ -67,6 +71,40 @@ func loginCmdRun(cmd *cobra.Command, args []string) error { p.Token = revokeToken p.URL = login.RevokeTokenURL login.CredentialsLogout(p) + } else if validateToken != "" { + p.Token = validateToken + p.URL = login.ValidateTokenURL + r, err := login.ValidateCredentials(p) + if err != nil { + return fmt.Errorf("Failed to validate: %v", err.Error()) + } + + tokenType := "App Access Token" + if r.UserID != "" { + tokenType = "User Access Token" + } + + expiresInTimestamp := time.Now().Add(time.Duration(r.ExpiresIn) * time.Second).UTC().Format(time.RFC1123) + + lightYellow := color.New(color.FgHiYellow).PrintfFunc() + white := color.New(color.FgWhite).SprintfFunc() + + lightYellow("Client ID: %v\n", white(r.ClientID)) + lightYellow("Token Type: %v\n", white(tokenType)) + if r.UserID != "" { + lightYellow("User ID: %v\n", white(r.UserID)) + lightYellow("User Login: %v\n", white(r.UserLogin)) + } + lightYellow("Expires In: %v\n", white("%v (%v)", strconv.FormatInt(r.ExpiresIn, 10), expiresInTimestamp)) + + if len(r.Scopes) == 0 { + lightYellow("User ID: %v\n", white("None")) + } else { + lightYellow("Scopes:\n") + for _, s := range r.Scopes { + fmt.Println(white("- %v\n", s)) + } + } } else if isUserToken == true { p.URL = login.UserCredentialsURL login.UserCredentialsLogin(p, tokenServerIP, webserverPort) diff --git a/internal/login/login.go b/internal/login/login.go index 3b4a58db..08de0825 100644 --- a/internal/login/login.go +++ b/internal/login/login.go @@ -57,6 +57,14 @@ type LoginResponse struct { ExpiresAt time.Time } +type ValidateResponse struct { + ClientID string `json:"client_id"` + UserLogin string `json:"login"` + UserID string `json:"user_id"` + Scopes []string `json:"scopes"` + ExpiresIn int64 `json:"expires_in"` +} + const ClientCredentialsURL = "https://id.twitch.tv/oauth2/token?grant_type=client_credentials" const UserCredentialsURL = "https://id.twitch.tv/oauth2/token?grant_type=authorization_code" @@ -66,6 +74,8 @@ const RefreshTokenURL = "https://id.twitch.tv/oauth2/token?grant_type=refresh_to const RevokeTokenURL = "https://id.twitch.tv/oauth2/revoke" +const ValidateTokenURL = "https://id.twitch.tv/oauth2/validate" + func ClientCredentialsLogin(p LoginParameters) (LoginResponse, error) { u, err := url.Parse(p.URL) if err != nil { @@ -216,6 +226,31 @@ func RefreshUserToken(p RefreshParameters) (LoginResponse, error) { return r, nil } +func ValidateCredentials(p LoginParameters) (ValidateResponse, error) { + u, err := url.Parse(p.URL) + if err != nil { + log.Fatal(err) + } + + resp, err := loginRequestWithHeaders(http.MethodGet, u.String(), nil, []loginHeader{ + loginHeader{ + Key: "Authorization", + Value: "OAuth " + p.Token, + }, + }) + if err != nil { + return ValidateResponse{}, err + } + + // Handle validate response body + var r ValidateResponse + if err = json.Unmarshal(resp.Body, &r); err != nil { + return ValidateResponse{}, err + } + + return r, nil +} + func handleLoginResponse(body []byte) (LoginResponse, error) { var r AuthorizationResponse if err := json.Unmarshal(body, &r); err != nil { diff --git a/internal/login/login_request.go b/internal/login/login_request.go index 7f2a1c35..99e19589 100644 --- a/internal/login/login_request.go +++ b/internal/login/login_request.go @@ -16,9 +16,22 @@ type loginRequestResponse struct { Body []byte } +type loginHeader struct { + Key string + Value string +} + func loginRequest(method string, url string, payload io.Reader) (loginRequestResponse, error) { + return loginRequestWithHeaders(method, url, payload, []loginHeader{}) +} + +func loginRequestWithHeaders(method string, url string, payload io.Reader, headers []loginHeader) (loginRequestResponse, error) { req, err := request.NewRequest(method, url, payload) + for _, header := range headers { + req.Header.Add(header.Key, header.Value) + } + client := &http.Client{ Timeout: time.Second * 10, }