1

Clearly by the negative score, I haven't provided enough information - sorry about that. However, perhaps add comments to explain why rather than just marking it down?

2nd attempt at a description:
I would like to be able to connect to Spotify's web API interface (https://developer.spotify.com/web-api/) on a headless embedded platform (Arm based simple MCU with WiFi). The username and password would be hardcoded into the system, probably added at setup time with the help of a mobile device (providing a temporary user interface).

I want to be able to add tracks to a playlist, which requires an authentication token. Spotify's usual flow requires the embedded platform to host their webpage login, as described here (https://developer.spotify.com/web-api/authorization-guide/).

Is this possible to authenticate without the webpage?

I have seen here (https://developer.spotify.com/technologies/spotify-ios-sdk/token-swap-refresh/) that Spotify recommend mobile apps use a remote server to handle refreshing of tokens - perhaps that's a route?

Any pointers would be appreciated.

Martin Harrison
  • 145
  • 2
  • 11

3 Answers3

0

I don't think it is bad question. I am also working on a headless player that runs on a local network which makes the authorization flow a bit awkward. So this is not much of an answer, but let me explain how it can be done.

Your headless system needs to have a web interface that can redirect to the spotify authorization url and handle the callback. The problem is that you have to register the callback url on your spotify app. Say you register http://server1/spotify/auth/callback. Now the server1 needs to be accessible from the device doing the authorization, f.ex by adding it to /etc/hosts.

The good news is that refresh can be done without user intervention, so if you store the access token the user will only need to do this one time after installing.

0

I know that this is really late, but for anyone having the same issue...

I am working on something similar was mentioned above so I'll share what I know. I am creating a music player that could act as another device on my Spotify (using: https://developer.spotify.com/documentation/web-playback-sdk/) account as well be controlled by my custom webpage.

I have 3 parts to this: backend server, the SDK player webpage (for me: http://localhost:8080/#/pup/player), the frontend UI webpage

(all the code snippets are a part of a class)

The only way I was able to get it running was like so:

  1. Start the backend server and initialize puppeteer
    async initPup(){
        this.browser = await puppeteer.launch({
            headless: false, // This is important, because spotify SDK doesn't create the device when using headless
            devtools: true,
            executablePath: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", //I also have to use Chrome and not Chromium, because Chromium is missing support for EME keySystems (yes, I've tried bruteforcing chromium versions or getting Firefox to work using createBrowserFetcher())
            ignoreDefaultArgs: ['--mute-audio'],
            args: ['--autoplay-policy=no-user-gesture-required']
        });

        this.page = (await this.browser.pages())[0]; // create one page
        if(this.page == undefined){
            this.page = await this.browser.newPage();
        }

        this.pup_ready = true;

        console.log(await this.page.browser().version())
    }
  1. Open your SDK player page with puppeteer and pass the ClientID and ClientSecret of your Spotify project (https://developer.spotify.com/dashboard/):
    async openPlayer(){
        // const player_page = "http://localhost:8080/#/pup/player"
        if(this.pup_ready){
            await this.page.goto(player_page + "/?&cid=" + this.client_id + "&csec=" + this.client_secret);
        }
    }
  1. On the SDK player webpage save the cid and csec URL params to LocalStorage. This should be done when no ULR parameter named "code" has been given, because that's the authorizations code which will be generated in the next step.

Something like:

    var auth_code = url_params_array.find(x=>x.param.includes("code")); // try to get the auth code
    var c_id = url_params_array.find(x=>x.param.includes("cid")); //get cid
    var c_sec = url_params_array.find(x=>x.param.includes("csec")); //get csec

    var token = undefined;
    if(auth_code == undefined){ // the auth code is not defined yet and it has to be created
        //SAVING CLIENT ID and CLIENT SECRET
        c_id = c_id.value;
        c_sec = c_sec.value;
        window.localStorage.setItem("__cid", c_id)
        window.localStorage.setItem("__csec", c_sec)
        
        //GETTING THE AUTH CODE
        var scope = "streaming \
           user-read-email \
           user-read-private"
        var state = "";
        var auth_query_parameters = new URLSearchParams({
            response_type: "code",
            client_id: c_id,
            scope: scope,
            redirect_uri: "http://localhost:8080/#/pup/player/",
            state: state
        })
        window.open('https://accounts.spotify.com/authorize/?' + auth_query_parameters.toString()); // tak the puppeteer to the spotify login page
    }
  1. Login on the spotify page using your credential to create the auth token. I had to use https://www.npmjs.com/package/puppeteer-extra-plugin-stealth to bypass CAPTCHAS
    async spotifyLogin(mail="<YOUR_SPOTIFY_MAIL>", pass = "<YOUR_SPOTIFY_PASSWORD") {
        var p = this.page = (await this.browser.pages())[1] // get the newly opened page with the spotify

        //await p.waitForNavigation({waitUntil: 'networkidle2'})
        await p.focus("#login-username"); // put in the credentials
        await p.keyboard.type(mail);
        await p.focus("#login-password");
        await p.keyboard.type(pass);
        await p.$eval("#login-button", el => el.click());

        (await this.browser.pages())[0].close(); // close the old SDK page

        await sleep(1000) // wait to be redirected back to your SDK page
        //

        this.page = (await this.browser.pages())[0];
        this.auth_code = await this.page.evaluate( (varName) => window.localStorage.getItem(varName), ["__auth"] ) // here is ave the auth token as a property of the class instance as well
    }
  1. Once you're redirected to SDK page again you already have cid and csec and now also the auth token.
    if(auth_code == undefined)
        //... (this is already in step 3)
    }else{
        // GETTING CID and C SECRET AGAIN
        c_id = window.localStorage.getItem("__cid")
        c_sec = window.localStorage.getItem("__csec")

        // SAVING THE AUTH CODE
        auth_code = auth_code.value;
        window.localStorage.setItem("__auth", auth_code)
    }
  1. Generate a token on the backend.
async genToken():Promise<void>{
        //Pretty much coppied from: https://developer.spotify.com/documentation/web-playback-sdk/guide/
        var authOptions = {
            url: 'https://accounts.spotify.com/api/token',
            headers: {
            'Authorization': 'Basic ' + (Buffer.from(this.client_id + ':' + this.client_secret).toString("base64"))
            },
            form: {
                code: this.auth_code,
                redirect_uri: "http://localhost:8080/#/pup/player/",
                grant_type: 'authorization_code'
            },
            json: true
        };

        var token;
        var refresh_token;
        await request.post(authOptions, function(error, response, body) { // also get the refresh token
            if (!error && response.statusCode === 200) {
                token = body.access_token;
                refresh_token = body.refresh_token;
            }
        });

        while (!token && !refresh_token){ // wait for both of them
            await sleep(100)
        }

        this.token = token; // save them in the class instance properties
        this.refresh_token = refresh_token;
    }
  1. Lastly the puppeteer fills in a html field with the token generated in step 6 on the SDK site and presses a button to start the SDK player.
    // this function gets called after the button gets pressed
    async function main(){
        console.log(window.localStorage.getItem("__cid")) // print out all the data
        console.log(window.localStorage.getItem("__csec"))
        console.log(window.localStorage.getItem("__auth"))
        console.log(getToken())
        
        const player = new Spotify.Player({ // start the sporify player
            name: 'Home Spotify Player',
            getOAuthToken: cb => cb(getToken())
        });

        player.connect().then(()=>{ // connect the player
            console.log(player)
        });
        window.player = player;
    }

    function getToken(){
        return document.getElementById("token_input").value;
    }
  1. You are done. Next step for me at least was communicating using another UI page to the backend puppeteer to control the SDK page (play/pause/skip etc.) This process is pretty "hacky" and not pretty at all but if you just have a little personal project it should do the job fine.

If anyone would be interested in the whole code I might even upload it somewhere, but I think this read is long-enough and overly detailed anyway.

rudytak
  • 1
  • 1
0

The proper way for this would be to use the device authorization grant flow - Spotify does this already for its TV applications, but they seem to block other applications from using it. It is possible to find clientIds online that are working with this, but it is not supported by Spotify.

I explained how this works and requested that they enable it in a supported way for custom applications in this feature request - please upvote the idea there if you find it useful.

That said, it is also possible to implement your own device authorization grant flow by hosting an extra server between your device and Spotify. That server should

  • host an authorize and a token API endpoint
  • host a user-facing page where the user can enter the user code
  • a callback page for Spotify to redirect the user after login

I believe this is how https://github.com/antscode/MacAuth implements it:

When the device calls the authorize, the server should generate a record containing the device_code and user_code and send them back in the response. The server should keep the record for later.

When the user enters the user_code in the user-facing page, the server should redirect the user to Spotify to login, and after login the user should be redirected to the server's callback page. At that moment the server can fetch credentials from Spotify's token endpoint using the data it received in the callback. The server should store the credentials it received in the record of the user_code.

The device can poll the server using the device_code for the availability of the tokens using the token endpoint.

hansmbakker
  • 1,108
  • 14
  • 29