spotify icon indicating copy to clipboard operation
spotify copied to clipboard

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

Open strideynet opened this issue 3 years ago • 7 comments

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

strideynet avatar Aug 16 '21 17:08 strideynet

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

campbelljlowman avatar Oct 24 '22 16:10 campbelljlowman

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

timbrammer910 avatar Nov 21 '22 20:11 timbrammer910

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.

strideynet avatar Nov 21 '22 22:11 strideynet

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.

timbrammer910 avatar Nov 22 '22 01:11 timbrammer910

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

seankhliao avatar Apr 20 '23 18:04 seankhliao

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

Instruction

  • Add http://localhost:8080/callback" to your Spotify's Applications "Redirect URL"
  • Run this code.
  • Navigate to http://localhost:8080/auth in your web browser.
  • Log In

Code

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)
}

Ed-Mar avatar Aug 08 '23 07:08 Ed-Mar