Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
brianstrauch committed Jun 9, 2021
0 parents commit e6a0701
Show file tree
Hide file tree
Showing 19 changed files with 582 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/spotify-api.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 123 additions & 0 deletions accounts_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package spotify

import (
secure "crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"math/rand"
"net/http"
"net/url"
"strings"

"github.com/brianstrauch/spotify/model"
)

const (
AccountsAPIBaseURL = "https://accounts.spotify.com"
ClientID = "81dddfee3e8d47d89b7902ba616f3357"
)

func StartProof() (string, string, error) {
verifier, err := generateRandomVerifier()
if err != nil {
return "", "", err
}

hash := sha256.Sum256(verifier)
challenge := base64.URLEncoding.EncodeToString(hash[:])
challenge = strings.TrimRight(challenge, "=")

return string(verifier), challenge, nil
}

func generateRandomVerifier() ([]byte, error) {
seed, err := generateSecureSeed()
if err != nil {
return nil, err
}
rand.Seed(seed)

chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-~"

verifier := make([]byte, 128)
for i := 0; i < len(verifier); i++ {
idx := rand.Intn(len(chars))
verifier[i] = chars[idx]
}

return verifier, nil
}

func generateSecureSeed() (int64, error) {
buf := make([]byte, 8)
_, err := secure.Read(buf)
if err != nil {
return 0, err
}

seed := int64(binary.BigEndian.Uint64(buf))
return seed, nil
}

func BuildAuthURI(redirectURI, challenge, state string, scope string) string {
q := url.Values{}
q.Add("client_id", ClientID)
q.Add("response_type", "code")
q.Add("redirect_uri", redirectURI)
q.Add("code_challenge_method", "S256")
q.Add("code_challenge", challenge)
q.Add("state", state)
q.Add("scope", scope)

return AccountsAPIBaseURL + "/authorize?" + q.Encode()
}

func RequestToken(code, redirectURI, verifier string) (*model.Token, error) {
v := url.Values{}
v.Set("client_id", ClientID)
v.Set("grant_type", "authorization_code")
v.Set("code", code)
v.Set("redirect_uri", redirectURI)
v.Set("code_verifier", verifier)
body := strings.NewReader(v.Encode())

res, err := http.Post(AccountsAPIBaseURL+"/api/token", "application/x-www-form-urlencoded", body)
if err != nil {
return nil, err
}
defer res.Body.Close()

// TODO: Handle errors

token := new(model.Token)
if err := json.NewDecoder(res.Body).Decode(token); err != nil {
return nil, err
}

return token, nil
}

func RefreshToken(refreshToken string) (*model.Token, error) {
v := url.Values{}
v.Set("grant_type", "refresh_token")
v.Set("refresh_token", refreshToken)
v.Set("client_id", ClientID)
body := strings.NewReader(v.Encode())

res, err := http.Post(AccountsAPIBaseURL+"/api/token", "application/x-www-form-urlencoded", body)
if err != nil {
return nil, err
}
defer res.Body.Close()

// TODO: Handle errors

token := new(model.Token)
if err := json.NewDecoder(res.Body).Decode(token); err != nil {
return nil, err
}

return token, nil
}
53 changes: 53 additions & 0 deletions accounts_api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package spotify

import (
"net/url"
"regexp"
"strings"
"testing"
)

func TestStartProof(t *testing.T) {
verifier, challenge, err := StartProof()
if err != nil {
t.Fatal(err)
}

if !regexp.MustCompile(`^[[:alnum:]_.\-~]{128}$`).MatchString(verifier) {
t.Fatal("Verifier string does not match")
}

// Hash with SHA-256 (64 chars)
// Convert to Base64 (44 chars)
// Remove trailing = (43 chars)
if !regexp.MustCompile(`^[[:alnum:]\-_]{43}$`).MatchString(challenge) {
t.Fatal("Challenge string does not match")
}
}

func TestBuildAuthURI(t *testing.T) {
var (
redirectURI = "http://localhost:1024"
challenge = "challenge"
state = "state"
scope = "user-modify-playback-state"
)

uri := BuildAuthURI(redirectURI, challenge, state, scope)

substrings := []string{
"client_id=" + ClientID,
"response_type=code",
"redirect_uri=" + url.QueryEscape(redirectURI),
"code_challenge_method=S256",
"code_challenge=" + challenge,
"state=" + state,
"scope=" + url.QueryEscape(scope),
}

for _, substring := range substrings {
if !strings.Contains(uri, substring) {
t.Fatalf("URI %s does not contain substring %s", uri, substring)
}
}
}
Loading

0 comments on commit e6a0701

Please sign in to comment.