zmb3 / spotify

A Go wrapper for the Spotify Web API
Apache License 2.0
1.33k stars 284 forks source link

Ensure clear example exists for how to use the client with existing access/refresh tokens #167

Open strideynet opened 2 years ago

strideynet commented 2 years ago

As it stands, there's no clear example for the following flow:

  1. User goes through oauth2 grant, access/refresh tokens are produced
  2. These tokens are persisted somewhere
  3. At a later date, the user makes a request to the service
  4. These tokens are then pulled out of persistence and used
campbelljlowman commented 1 year ago

Any update on this? I'm trying to figure out how to implement this flow in my application. I use a frontend to retrieve the user token and add it to a database. Then I want to read this token from the database and create a client using this token. Struggling to figure out a way to do this

Update: I figured this out. I pieced together some code from other examples:

        spotifyToken := "tokenString"
    token := &oauth2.Token{
        AccessToken: spotifyToken,
    }
    httpClient := spotifyauth.New().Client(ctx, token)
    client := spotify.New(httpClient)

spotifyToken is my token string which I read from the database. I have this code in a handler so I already has a ctx, but I believe you could just create a new background context like in some of the examples (context.Background())

The docs for an oauth2.Token have other fields for the token struct, one of which is a refresh token. I haven't looked into this yet but I'd guess it could handle refreshing for you

timbrammer910 commented 1 year ago

Yeah I'm struggling with this as well. I'm trying to just run a small app on a schedule, persisting client_id, client_secret, auth token, and refresh token as GitHub secrets. I'm setting the Expiry on the token to time.Now() when building a client:

`tok := &oauth2.Token{ AccessToken: "token string pulled from env", RefreshToken: "token string pulled from env", Expiry: time.Now(), }

client := spotify.New(spotifyAuth.Client(context.Background(), tok))`

But receive oauth2: cannot fetch token: 400 Bad Request Response: {"error":"invalid_grant","error_description":"Invalid refresh token"}

Part of the trouble with this is that I'm struggling to understand the refresh token system in Spotify's API. It says things like "A new refresh token might be returned too." in the documentation, which isn't helpful. Can I keep generating new clients from a static set of access + refresh tokens? The OAuth2 docs seem to imply I can with this line "The token will auto-refresh as necessary" here - https://pkg.go.dev/golang.org/x/oauth2#Config.Client

strideynet commented 1 year ago

Yeah I'm struggling with this as well. I'm trying to just run a small app on a schedule, persisting client_id, client_secret, auth token, and refresh token as GitHub secrets. I'm setting the Expiry on the token to time.Now() when building a client:

`tok := &oauth2.Token{ AccessToken: "token string pulled from env", RefreshToken: "token string pulled from env", Expiry: time.Now(), }

client := spotify.New(spotifyAuth.Client(context.Background(), tok))`

But receive oauth2: cannot fetch token: 400 Bad Request Response: {"error":"invalid_grant","error_description":"Invalid refresh token"}

Part of the trouble with this is that I'm struggling to understand the refresh token system in Spotify's API. It says things like "A new refresh token might be returned too." in the documentation, which isn't helpful. Can I keep generating new clients from a static set of access + refresh tokens? The OAuth2 docs seem to imply I can with this line "The token will auto-refresh as necessary" here - https://pkg.go.dev/golang.org/x/oauth2#Config.Client

If I recall correctly, they'll return a new refresh token when you refresh, so you'll need to come up with a way to write that back into the GitHub Action secret.

timbrammer910 commented 1 year ago

It might, but that timeout isn't made available. I've reused a refresh token longer than the life of the initial access token that generated it.

I solved this by re-implementing the code described here - https://developer.spotify.com/documentation/general/guides/authorization/code-flow/ under "Request a refreshed Access Token" and just running it on each execution and collecting the authorization code it returns.

If there is a way to do this using this lib + the oauth2 lib, I couldn't figure it out.

seankhliao commented 1 year ago

fwiw i construct the oauth2.Config by hand and it seems to refresh properly if I explicitly set AuthStyle: oauth2.AuthStyleInHeader Which is what i understood the spotify docs to require for the refresh flow https://developer.spotify.com/documentation/web-api/tutorials/code-flow#request-a-refreshed-access-token

example:

https://github.com/seankhliao/earbug/blob/60fd546c490cb2518efa5851eb2eec0ef14a3ad0/subcommands/serve/auth.go#L99-L108

Ed-Mar commented 11 months ago

Simple example usage of "golang.org/x/oauth2" for Spotify

Instruction

package main

import (
    "context"
    "golang.org/x/oauth2"
    "log"
    "net/http"
)

// tokenSource is a global variable that will be responsible for automatically refreshing the token when needed.
var tokenSource oauth2.TokenSource

// oauth2Config is the configuration for the OAuth2 flow, including the client ID, client secret, scopes, and endpoints.
var oauth2Config = oauth2.Config{
    RedirectURL:  "http://localhost:8080/callback",
    ClientID:     "your-client-id",
    ClientSecret: "your-client-secret",
    Scopes:       []string{"user-read-private", "user-read-email"},
    Endpoint: oauth2.Endpoint{
        AuthURL:  "https://accounts.spotify.com/authorize",
        TokenURL: "https://accounts.spotify.com/api/token"},
}

func main() {
    // Registering the handlers for the authentication process.
    http.HandleFunc("/auth", handleAuth)
    http.HandleFunc("/callback", handleCallback)
    // Starting the HTTP server on port 8080.
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// handleAuth initiates the OAuth2 authorization flow by redirecting the user to the provider's consent page.
func handleAuth(w http.ResponseWriter, r *http.Request) {
    authURL := oauth2Config.AuthCodeURL("", oauth2.AccessTypeOffline)
    http.Redirect(w, r, authURL, http.StatusFound)
}

// requestAccessToken exchanges the authorization code for an access token.
func requestAccessToken(code string) (*oauth2.Token, error) {
    return oauth2Config.Exchange(context.Background(), code)
}

// handleCallback handles the callback from the OAuth2 provider. It extracts the authorization code from the request,
// exchanges it for an access token, and sets up a token source for automatic token refresh.
func handleCallback(w http.ResponseWriter, r *http.Request) {
    code := r.URL.Query().Get("code")
    if code == "" {
        http.Error(w, "Missing code", http.StatusBadRequest)
        return
    }

    token, err := requestAccessToken(code)
    log.Printf("Token: %v", token) // Logging the token (for debugging purposes; be cautious in production).
    if err != nil {
        http.Error(w, "Failed to exchange token", http.StatusInternalServerError)
        return
    }

    // Create a token source that will automatically refresh the token as needed.
    tokenSource = oauth2Config.TokenSource(context.Background(), token)
}