Skip to content

Commit

Permalink
Refreshing access tokens on demand (#308)
Browse files Browse the repository at this point in the history
* Added --refresh and --secret to token sub-command
* Moved token error handling and messaging upstream
* Updated token docs
  • Loading branch information
Xemdo authored Jan 15, 2024
1 parent 12b9bb8 commit 2893a58
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 58 deletions.
82 changes: 77 additions & 5 deletions cmd/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package cmd

import (
"fmt"
"log"
"strconv"
"time"

Expand All @@ -18,7 +19,9 @@ var isUserToken bool
var userScopes string
var revokeToken string
var validateToken string
var refreshToken string
var overrideClientId string
var overrideClientSecret string
var tokenServerPort int
var tokenServerIP string
var redirectHost string
Expand All @@ -37,7 +40,9 @@ func init() {
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().StringVarP(&refreshToken, "refresh", "R", "", "Instead of generating a new token, refresh the token associated with the Refresh Token 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(&overrideClientSecret, "secret", "", "Override/manually set Client Secret for token actions. By default Client Secret from CLI config will be used.")
loginCmd.Flags().StringVar(&tokenServerIP, "ip", "", "Manually set the IP address to be bound 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.")
loginCmd.Flags().StringVar(&redirectHost, "redirect-host", "localhost", "Manually set the host to be used for the redirect URL")
Expand Down Expand Up @@ -65,6 +70,10 @@ func loginCmdRun(cmd *cobra.Command, args []string) error {
clientID = overrideClientId
}

if overrideClientSecret != "" {
clientSecret = overrideClientSecret
}

var p = login.LoginParameters{
ClientID: clientID,
ClientSecret: clientSecret,
Expand All @@ -76,13 +85,20 @@ func loginCmdRun(cmd *cobra.Command, args []string) error {
if revokeToken != "" {
p.Token = revokeToken
p.URL = login.RevokeTokenURL
login.CredentialsLogout(p)
_, err := login.CredentialsLogout(p)

if err != nil {
return err
}

log.Printf("Token %s has been successfully revoked", p.Token)

} 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())
return err
}

tokenType := "App Access Token"
Expand Down Expand Up @@ -111,12 +127,68 @@ func loginCmdRun(cmd *cobra.Command, args []string) error {
fmt.Println(white("- %v\n", s))
}
}

} else if refreshToken != "" {
p.URL = login.RefreshTokenURL

// If we are overriding the Client ID then we shouldn't store this in the config.
shouldStoreInConfig := (overrideClientId == "")

resp, err := login.RefreshUserToken(login.RefreshParameters{
RefreshToken: refreshToken,
ClientID: clientID,
ClientSecret: clientSecret,
URL: login.RefreshTokenURL,
}, shouldStoreInConfig)

if err != nil {
errDescription := ""
if overrideClientId == "" {
errDescription = "Check `--refresh` flag to ensure the provided Refresh Token is valid for the Client ID set with `twitch config`."
} else {
errDescription = "Check `--refresh` and `--client-id` flags to ensure the provided Refresh Token is valid for the provided Client ID."
}

return fmt.Errorf("%v\n%v", err.Error(), errDescription)
}

lightYellow := color.New(color.FgHiYellow).SprintfFunc()

log.Println("Successfully refreshed Access Token.")
log.Println(lightYellow("Access Token: ") + resp.Response.AccessToken)
log.Println(lightYellow("Refresh Token: ") + resp.Response.RefreshToken)
log.Println(lightYellow("Expires At: ") + resp.ExpiresAt.String())

} else if isUserToken {
p.URL = login.UserCredentialsURL
login.UserCredentialsLogin(p, tokenServerIP, webserverPort)
resp, err := login.UserCredentialsLogin(p, tokenServerIP, webserverPort)

if err != nil {
return err
}

lightYellow := color.New(color.FgHiYellow).SprintfFunc()

log.Println("Successfully generated User Access Token.")
log.Println(lightYellow("User Access Token: ") + resp.Response.AccessToken)
log.Println(lightYellow("Refresh Token: ") + resp.Response.RefreshToken)
log.Println(lightYellow("Expires At: ") + resp.ExpiresAt.String())
log.Println(lightYellow("Scopes: ") + fmt.Sprintf("%v", resp.Response.Scope))

} else {
p.URL = login.ClientCredentialsURL
login.ClientCredentialsLogin(p)
resp, err := login.ClientCredentialsLogin(p)

if err != nil {
return err
}

lightYellow := color.New(color.FgHiYellow).SprintfFunc()

log.Println("Successfully generated App Access Token.")
log.Println(lightYellow("App Access Token: ") + resp.Response.AccessToken)
log.Println(lightYellow("Expires At: ") + resp.ExpiresAt.String())
log.Println(lightYellow("Scopes: ") + fmt.Sprintf("%v", resp.Response.Scope))
}

return nil
Expand Down
37 changes: 27 additions & 10 deletions docs/token.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@ Access tokens can be revoked with:
twitch token -r 0123456789abcdefghijABCDEFGHIJ
```

## Refreshing Access Tokens

Access tokens can be refreshed using a refresh token:

```
twitch token --refresh ABCDEfghij0123456789abcdefghijABCDEFGHIJ
```

By default, this uses the Client ID and Client Secret stored in your config file. You can override this with `--client-id` and `--secret`, as such:

```
twitch token --refresh ABCDEfghij0123456789abcdefghijABCDEFGHIJ --client-id uo6dggojyb8d6soh92zknwmi5ej1q2 --secret yigv8zib6nuczcoy08u8g1nxh6wjgu
```
When overriding the Client ID, your config file will **not** be updated with the new access token, client ID, or secret.

## Alternate IP for User Token Webserver

If you'd like to bind the webserver used for user tokens (`-u` flag), you can override it with the `--ip` flag. For example:
Expand Down Expand Up @@ -147,16 +162,18 @@ None.

**Flags**

| Flag | Shorthand | Description | Example | Required? (Y/N) |
|-------------------|-----------|----------------------------------------------------------------------------------------------------------------|----------------------------------------------|-----------------|
| `--user-token` | `-u` | Whether to fetch a user token or not. Default is false. | `token -u` | N |
| `--scopes` | `-s` | The space separated scopes to use when getting a user token. | `-s "user:read:email user_read"` | N |
| `--revoke` | `-r` | Instead of generating a new token, revoke the one passed to this parameter. | `-r 0123456789abcdefghijABCDEFGHIJ` | N |
| `--validate` | `-v` | Instead of generating a new token, validate the one passed to this parameter. | `-v 0123456789abcdefghijABCDEFGHIJ` | N |
| `--ip` | | Manually set the port to be used for the User Token web server. The default binds to all interfaces. (0.0.0.0) | `--ip 127.0.0.1` | N |
| `--port` | `-p` | Override/manually set the port for token actions. (The default is 3000) | `-p 3030` | N |
| `--client-id` | | Override/manually set client ID for token actions. By default client ID from CLI config will be used. | `--client-id uo6dggojyb8d6soh92zknwmi5ej1q2` | N |
| `--redirect-host` | | Override/manually set the redirect host token actions. The default is `localhost` | `--redirect-host contoso.com` | N |
| Flag | Shorthand | Description | Example | Required? (Y/N) |
|-------------------|-----------|------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|-----------------|
| `--user-token` | `-u` | Whether to fetch a user token or not. Default is false. | `token -u` | N |
| `--scopes` | `-s` | The space separated scopes to use when getting a user token. | `-s "user:read:email user_read"` | N |
| `--revoke` | `-r` | Instead of generating a new token, revoke the one passed to this parameter. | `-r 0123456789abcdefghijABCDEFGHIJ` | N |
| `--validate` | `-v` | Instead of generating a new token, validate the one passed to this parameter. | `-v 0123456789abcdefghijABCDEFGHIJ` | N |
| `--refresh` | `-R` | Instead of generating a new token, refresh the token associated with the Refresh Token passed to this parameter. | `-R ABCDEfghij0123456789abcdefghijABCDEFGHIJ` | N |
| `--ip` | | Manually set the port to be used for the User Token web server. The default binds to all interfaces. (0.0.0.0) | `--ip 127.0.0.1` | N |
| `--port` | `-p` | Override/manually set the port for token actions. (The default is 3000) | `-p 3030` | N |
| `--client-id` | | Override/manually set Client ID for token actions. By default Client ID from CLI config will be used. | `--client-id uo6dggojyb8d6soh92zknwmi5ej1q2` | N |
| `--secret` | | Override/manually set Client Secret for token actions. By default Client Secret from CLI config will be used. | `--secret yigv8zib6nuczcoy08u8g1nxh6wjgu` | N |
| `--redirect-host` | | Override/manually set the redirect host token actions. The default is `localhost` | `--redirect-host contoso.com` | N |

## Notes

Expand Down
5 changes: 3 additions & 2 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package api

import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
Expand Down Expand Up @@ -313,9 +314,9 @@ func GetClientInformation() (clientInformation, error) {
ClientID: clientID,
ClientSecret: clientSecret,
URL: login.RefreshTokenURL,
})
}, true)
if err != nil {
return clientInformation{}, err
return clientInformation{}, errors.New(err.Error() + "\nPlease rerun `twitch configure`")
}
token = r.Response.AccessToken
}
Expand Down
Loading

0 comments on commit 2893a58

Please sign in to comment.