mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-11-09 16:50:01 +00:00
[spotify] Switch to PKCE flow, remove need for clientSecret
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
a9984b2df2
commit
dc8f645f24
2 changed files with 30 additions and 15 deletions
|
@ -21,23 +21,25 @@ Start with going to the [Spotify for Developers Dashboard](https://developer.spo
|
||||||
- Select "Web API" for the "API/SDKs you are planning to use"
|
- Select "Web API" for the "API/SDKs you are planning to use"
|
||||||
- Check the ToS box (of course after reading those!)
|
- Check the ToS box (of course after reading those!)
|
||||||
- Click "Save"
|
- Click "Save"
|
||||||
- From the "Settings" button of your app get the "Client ID" and "Client secret" and note them down
|
- From the "Settings" button of your app get the "Client ID" and note it down
|
||||||
- Optional: If you need to authorize multiple channels (i.e. for multiple users of the bot instance) you can edit the "Redirect URIs" on the "Settings" page and add more.
|
- Optional: If you need to authorize multiple channels (i.e. for multiple users of the bot instance) you can edit the "Redirect URIs" on the "Settings" page and add more.
|
||||||
|
|
||||||
{{< alert style="info" >}}If you are managing a bot instance for multiple persons having their own Spotify accounts you need to invite them to the Spotify app as long as it is in development-mode. You can do that in the Spotify Developer Dashboard under "User Management" (up to 25 users). As an alternative every person can create an own Spotify app and you can enter their `clientId` / `clientSecret` into the config for their respective channel.{{< /alert >}}
|
{{< alert style="info" >}}If you are managing a bot instance for multiple persons having their own Spotify accounts you need to invite them to the Spotify app as long as it is in development-mode. You can do that in the Spotify Developer Dashboard under "User Management" (up to 25 users). As an alternative every person can create an own Spotify app and you can enter their `clientId` into the config for their respective channel.{{< /alert >}}
|
||||||
|
|
||||||
Now head into the configuration file and configure the Spotify module:
|
Now head into the configuration file and configure the Spotify module:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Module configuration by channel or defining bot-wide defaults. See
|
# Module configuration by channel or defining bot-wide defaults. See
|
||||||
# module specific documentation for options to configure in this
|
# module specific documentation for options to configure in this
|
||||||
# section. All modules come with internal defaults so there is no
|
# section.
|
||||||
# need to configure this but you can overwrite the internal defaults.
|
|
||||||
module_config:
|
module_config:
|
||||||
spotify:
|
spotify:
|
||||||
|
# Use one client-id for all channels (invite users)
|
||||||
default:
|
default:
|
||||||
clientId: 'put the client ID you noted down here'
|
clientId: 'put the client ID you noted down here'
|
||||||
clientSecret: 'put the secret here'
|
# Use one client-id per channel (have each user create an app)
|
||||||
|
anotherttvuser:
|
||||||
|
clientId: 'put the client ID they sent you here'
|
||||||
```
|
```
|
||||||
|
|
||||||
Now send the user which currently playing track should be displayed to the `https://example.com/spotify/<channel>` URL. So I for example would visit `https://example.com/spotify/luziferus`. They are redirected to Spotify, need to authorize the app and if everything went well the bot tells them "Spotify is now authorized for this channel, you can close this page".
|
Now send the user which currently playing track should be displayed to the `https://example.com/spotify/<channel>` URL. So I for example would visit `https://example.com/spotify/luziferus`. They are redirected to Spotify, need to authorize the app and if everything went well the bot tells them "Spotify is now authorized for this channel, you can close this page".
|
||||||
|
|
|
@ -2,21 +2,33 @@ package spotify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const spotifyRequestTimeout = 2 * time.Second
|
const (
|
||||||
|
spotifyRequestTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
pkcePBKDFIter = 210000
|
||||||
|
pkcePBKDFLen = 64
|
||||||
|
)
|
||||||
|
|
||||||
|
var instanceSalt = uuid.Must(uuid.NewV4()).String()
|
||||||
|
|
||||||
func handleStartAuth(w http.ResponseWriter, r *http.Request) {
|
func handleStartAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
channel := mux.Vars(r)["channel"]
|
channel := mux.Vars(r)["channel"]
|
||||||
|
pkceVerifier := hex.EncodeToString(pbkdf2.Key([]byte(channel), []byte(instanceSalt), pkcePBKDFIter, pkcePBKDFLen, sha512.New))
|
||||||
|
|
||||||
redirURL := baseURL.ResolveReference(&url.URL{Path: r.URL.Path})
|
redirURL := baseURL.ResolveReference(&url.URL{Path: r.URL.Path})
|
||||||
conf, err := oauthConfig(channel, strings.Split(redirURL.String(), "?")[0])
|
conf, err := oauthConfig(channel, strings.Split(redirURL.String(), "?")[0])
|
||||||
|
@ -30,13 +42,20 @@ func handleStartAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
if code == "" {
|
if code == "" {
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r,
|
w, r,
|
||||||
conf.AuthCodeURL(fmt.Sprintf("%x", sha256.Sum256(append([]byte(conf.ClientID), []byte(channel)...)))),
|
conf.AuthCodeURL(
|
||||||
|
fmt.Sprintf("%x", sha256.Sum256(append([]byte(conf.ClientID), []byte(channel)...))),
|
||||||
|
oauth2.S256ChallengeOption(pkceVerifier),
|
||||||
|
),
|
||||||
http.StatusFound,
|
http.StatusFound,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := conf.Exchange(r.Context(), r.URL.Query().Get("code"))
|
token, err := conf.Exchange(
|
||||||
|
r.Context(),
|
||||||
|
r.URL.Query().Get("code"),
|
||||||
|
oauth2.VerifierOption(pkceVerifier),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("getting Spotify oauth token")
|
logrus.WithError(err).Error("getting Spotify oauth token")
|
||||||
http.Error(w, "unable to get Spotify auth token", http.StatusInternalServerError)
|
http.Error(w, "unable to get Spotify auth token", http.StatusInternalServerError)
|
||||||
|
@ -58,14 +77,8 @@ func oauthConfig(channel, redirectURL string) (conf *oauth2.Config, err error) {
|
||||||
return nil, fmt.Errorf("getting clientId for channel: %w", err)
|
return nil, fmt.Errorf("getting clientId for channel: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clientSecret, err := getModuleConfig(actorName, channel).String("clientSecret")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("getting clientSecret for channel: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &oauth2.Config{
|
return &oauth2.Config{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ClientSecret: clientSecret,
|
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
AuthURL: "https://accounts.spotify.com/authorize",
|
AuthURL: "https://accounts.spotify.com/authorize",
|
||||||
TokenURL: "https://accounts.spotify.com/api/token",
|
TokenURL: "https://accounts.spotify.com/api/token",
|
||||||
|
|
Loading…
Reference in a new issue