How to implement Spotify OAuth2 login flow in Spring Boot Java application

I’m building a web app that needs to connect with Spotify’s API. Users need to log into their Spotify accounts for my app to work properly.

The login flow works fine until the callback part. Users can click my “Connect with Spotify” button and see Spotify’s permission page. But when they approve access, I get a 403 error saying access is denied.

I think the problem is in my callback handler method handleSpotifyCallback. Maybe the tokens aren’t being set correctly after the user approves the connection.

Here’s my authentication controller:

import java.io.IOException;
import java.net.URI;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletResponse;
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.SpotifyHttpManager;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials;
import se.michaelthelin.spotify.requests.authorization.authorization_code.AuthorizationCodeRequest;
import se.michaelthelin.spotify.requests.authorization.authorization_code.AuthorizationCodeUriRequest;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.servlet.view.RedirectView;

@RestController
@RequestMapping("/auth")
public class SpotifyAuthController {
    
    private static final URI callbackUri = SpotifyHttpManager.makeUri("http://localhost:8080/auth/callback");
    private String authCode = "";
    private String userRefreshToken = "";
    private static final SpotifyApi spotifyClient = new SpotifyApi.Builder()
        .setClientId("abc123def456ghi789")
        .setClientSecret("xyz987uvw654rst321")
        .setRedirectUri(callbackUri)
        .build();

    @GetMapping("connect")
    @ResponseBody
    public RedirectView initiateSpotifyAuth() {
        AuthorizationCodeUriRequest authUriRequest = spotifyClient.authorizationCodeUri()
            .scope("playlist-modify-public, user-read-private, user-read-email, user-top-read")
            .show_dialog(true)
            .build();
        final URI authUri = authUriRequest.execute();
        RedirectView redirect = new RedirectView();
        redirect.setUrl(authUri.toString());
        return redirect;
    }

    @GetMapping(value = "callback")
    public String handleSpotifyCallback(@RequestParam("code") String authorizationCode, HttpServletResponse response) throws IOException {
        System.out.println("Got authorization code: " + authorizationCode);
    
        if (authorizationCode == null || authorizationCode.isEmpty()) {
            return "Missing authorization code.";
        }
    
        AuthorizationCodeRequest tokenRequest = spotifyClient.authorizationCode(authorizationCode).build();
    
        try {
            final AuthorizationCodeCredentials credentials = tokenRequest.execute();
    
            spotifyClient.setAccessToken(credentials.getAccessToken());
            spotifyClient.setRefreshToken(credentials.getRefreshToken());
            userRefreshToken = credentials.getRefreshToken();
    
            System.out.println("Got Access Token: " + credentials.getAccessToken());
            System.out.println("Got Refresh Token: " + credentials.getRefreshToken());
            System.out.println("Token expires in: " + credentials.getExpiresIn());
    
        } catch (IOException | SpotifyWebApiException | org.apache.hc.core5.http.ParseException e) {
            System.out.println("Token exchange failed: " + e.getMessage());
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Token exchange failed.");
            return "Token exchange failed.";
        }
    
        response.sendRedirect("http://localhost:8080/dashboard");
        return spotifyClient.getAccessToken();
    }
}

What could be causing the 403 error after successful authentication?

Your 403 error is likely coming from how you’re handling the Spotify callback. In your handleSpotifyCallback method, you’re using both response.sendRedirect() and returning a string - this creates conflicts in Spring Boot’s response handling. First, double-check that http://localhost:8080/auth/callback is added to your Spotify app’s redirect URIs in the dashboard. Then try switching your method to use RedirectView instead of sending the redirect directly. I’ve hit this same issue before when mixing response types, and this usually fixes it.

The problem’s in your callback method - you’re calling response.sendRedirect() AND returning a string. Spring Boot can’t handle both at once and gets confused about which response to send. I’ve hit this same issue with OAuth flows before. Drop the response.sendRedirect() call and return a RedirectView object instead, just like you did in initiateSpotifyAuth(). Also double-check that your callback URL in Spotify’s developer dashboard matches your code exactly - same port, trailing slash, HTTP vs HTTPS. Even tiny mismatches can break auth after the user approves access.