128 lines
3.3 KiB
Go
128 lines
3.3 KiB
Go
package spotifyAuth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"git.akpain.net/codemicro/backseat-music/backseat/config"
|
|
"git.akpain.net/codemicro/backseat-music/backseat/data"
|
|
"git.akpain.net/codemicro/backseat-music/backseat/httpUtil"
|
|
"golang.org/x/oauth2"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
)
|
|
|
|
var oauthConf = &oauth2.Config{
|
|
ClientID: config.SpotifyClientID,
|
|
ClientSecret: config.SpotifyClientSecret,
|
|
Scopes: []string{"playlist-read-private", "playlist-modify-public", "playlist-modify-private", "ugc-image-upload", "user-read-recently-played"},
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: "https://accounts.spotify.com/authorize",
|
|
TokenURL: "https://accounts.spotify.com/api/token",
|
|
},
|
|
}
|
|
|
|
type SpotifyAuth struct {
|
|
mux *http.ServeMux
|
|
datastore *data.Store
|
|
}
|
|
|
|
var _ http.Handler = (*SpotifyAuth)(nil)
|
|
|
|
func New(datastore *data.Store) *SpotifyAuth {
|
|
s := &SpotifyAuth{
|
|
mux: http.NewServeMux(),
|
|
datastore: datastore,
|
|
}
|
|
|
|
s.mux.Handle("GET /spotify/oauth/outbound", httpUtil.HandleErrors(s.oauthOutbound))
|
|
s.mux.Handle("GET /spotify/oauth/inbound", httpUtil.HandleErrors(s.oauthInbound))
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *SpotifyAuth) ServeHTTP(rw http.ResponseWriter, rq *http.Request) {
|
|
s.mux.ServeHTTP(rw, rq)
|
|
}
|
|
|
|
const oauthStateCookie = "backseat-oauth"
|
|
|
|
func (s *SpotifyAuth) oauthOutbound(rw http.ResponseWriter, rq *http.Request) error {
|
|
state, err := generateState()
|
|
if err != nil {
|
|
return fmt.Errorf("generate state: %w", err)
|
|
}
|
|
|
|
http.SetCookie(rw, &http.Cookie{
|
|
Name: oauthStateCookie,
|
|
Value: state,
|
|
HttpOnly: true,
|
|
})
|
|
|
|
rw.Header().Set("Location", oauthConf.AuthCodeURL(
|
|
state,
|
|
oauth2.SetAuthURLParam("redirect_uri", deriveOauthRedirectURL(rq)),
|
|
oauth2.SetAuthURLParam("show_dialog", "true"),
|
|
))
|
|
rw.WriteHeader(301)
|
|
return nil
|
|
}
|
|
|
|
func generateState() (string, error) {
|
|
randomData := make([]byte, 64)
|
|
_, err := rand.Read(randomData)
|
|
if err != nil {
|
|
return "", nil
|
|
}
|
|
return hex.EncodeToString(randomData), nil
|
|
}
|
|
|
|
func (s *SpotifyAuth) oauthInbound(rw http.ResponseWriter, rq *http.Request) error {
|
|
code := rq.URL.Query().Get("code")
|
|
if code == "" {
|
|
_, _ = rw.Write([]byte("missing code"))
|
|
rw.WriteHeader(400)
|
|
return nil
|
|
}
|
|
|
|
state := rq.URL.Query().Get("state")
|
|
stateCookie, err := rq.Cookie(oauthStateCookie) // the only reason we might have an err is if the cookie doesn't exist
|
|
if state == "" || err != nil || subtle.ConstantTimeCompare([]byte(state), []byte(stateCookie.Value)) == 0 {
|
|
_, _ = rw.Write([]byte("bad state"))
|
|
rw.WriteHeader(400)
|
|
return nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
|
defer cancel()
|
|
|
|
tok, err := oauthConf.Exchange(ctx, code,
|
|
oauth2.SetAuthURLParam("grant_type", "authorization_code"),
|
|
oauth2.SetAuthURLParam("redirect_uri", deriveOauthRedirectURL(rq)))
|
|
if err != nil {
|
|
return fmt.Errorf("oauth2 exchange: %w", err)
|
|
}
|
|
|
|
if err := s.datastore.SetSpotifyOAuthToken(tok); err != nil {
|
|
return fmt.Errorf("save oauth2 token: %w", err)
|
|
}
|
|
|
|
rw.Header().Set("Location", httpUtil.GetNextURL(rq))
|
|
rw.WriteHeader(http.StatusFound)
|
|
return nil
|
|
}
|
|
|
|
func deriveOauthRedirectURL(rq *http.Request) string {
|
|
proto := "https"
|
|
if rq.TLS == nil {
|
|
proto = "http"
|
|
}
|
|
return (&url.URL{
|
|
Scheme: proto,
|
|
Host: rq.Host,
|
|
Path: "/spotify/oauth/inbound",
|
|
}).String()
|
|
}
|