getting access token url from popup in javascript [spotify auth]
Asked Answered
E

2

7

I am trying to make a spotify auth flow in pure javascript, so a user can sign in, and i can then add a new playlist for their account. From the instructions I've read, I use an auth popup that once they sign in, has the access token in the URL. I have a popup right now that the user can auth with, and once they do it will have the access token in the url.

I need to get the url from my popup and save it as a global var, but I'm having trouble figuring out how to do this in javascript.

https://codepen.io/martin-barker/pen/YzPwXaz

My codepen opens a popup with let popup = window.open( , can I run a function in my popup to detect when the user successfully authenticates and the url changes? In which case I would want to save the url for parsing and close my popup

My javascript code is as follows:

async function spotifyAuth() {
let result = spotifyLogin()
}

//open popup
function spotifyLogin() {
console.log("inside spotifyLogin, opening popup")

let popup = window.open(`https://accounts.spotify.com/authorize?client_id=5a576333cfb1417fbffbfa3931b00478&response_type=token&redirect_uri=https://codepen.io/martin-barker/pen/YzPwXaz&show_dialog=true&scope=playlist-modify-public`, 'Login with Spotify', 'width=800,height=600')

}

//get url from popup and parse access token????
window.spotifyCallback = (payload) => {
console.log("inside window? ") //this line never appears in console
popup.close()
fetch('https://api.spotify.com/v1/me', {
headers: {
'Authorization': `Bearer ${payload}`
}
}).then(response => {
return response.json()
}).then(data => {
// do something with data
})
}
Entomostracan answered 8/12, 2019 at 22:42 Comment(0)
B
9

Here is how I did it in JavaScript. Global variable like you mentioned:

var access_token = null;

My url looks something like this for example: https://...home.jsp#access_token=BQAXe5JQOV_xZmAukmw6G430lreF......rQByzZMcOIF2q2aszujN0wzV7pIxA4viMbQD6s&token_type=Bearer&expires_in=3600&state=vURQeVAoZqwYm4dC

After Spotify redirects the user to the uri you specified on the dashboard, I parse the url for the hash containing the access token like so:

var hash = window.location.hash.substring(1);
var accessString = hash.indexOf("&");

/* 13 because that bypasses 'access_token' string */
access_token = hash.substring(13, accessString);
console.log("Access Token: " + access_token);

Which the output is:

Access Token: BQAXe5JQOV_xZmAukmw6G430lreF...........rQByzZMcOIF2q2aszujN0wzV7pIxA4viMbQD6s

I save this access token in the sessionStorage just in case the user navigates away from the page and the url does not contain the access_token. I am assuming this is the implicit grant flow since you want to use pure JavaScript. Just make sure to re obtain a access token every hour since they expire.

Addendum

I can show you how to obtain the token and use it in an example.

I have a button on a .html page that once clicked calls a function called implicitGrantFlow() in a JavaScript file called

Test.js

function implicitGrantFlow() {

/* If access token has been assigned in the past and is not expired, no request required. */
if (sessionStorage.getItem("accessToken") !== null &&
    sessionStorage.getItem("tokenTimeStamp") !== null &&
    upTokenTime < tokenExpireSec) {
        var timeLeft = (tokenExpireSec - upTokenTime);
        console.log("Token still valid: " + Math.floor(timeLeft / 60) + " minutes left.");

        /* Navigate to the home page. */
        $(location).attr('href', "home.jsp");
} else {
    console.log("Token expired or never found, getting new token.");
    $.ajax({
        url: auth_url,
        type: 'GET',
        contentType: 'application/json',
        data: {
            client_id: client_id,
            redirect_uri: redirect_uri,
            scope: scopes,
            response_type: response_type_token,
            state: state
        }
    }).done(function callback(response) {
        /* Redirect user to home page */
        console.log("COULD THIS BE A SUCCESS?");
        $(location).attr('href', this.url);

    }).fail(function (error) {
        /* Since we cannot modify the server, we will always fail. */
        console.log("ERROR HAPPENED: " + error.status);
        console.log(this.url);
        $(location).attr('href', this.url);
    });
}

What I am doing is checking if the access_token info I stored in the sessionStorage is null. I used time since Epoch to generate when the token was created and when it ideally should expire. If these parameters are satisfied then I do not make another call.

Else, I make a call to get an access token, which on success will redirect me to my uri as I mentioned in my previous write up (you'll see I have the redirect in the .fail section. This is due to me not having permission on my school server to setup settings to bypass the CORS related issues preventing my call from being successful even though the redirect url I create is fine.).

Then when my whitelist uri gets loaded (which redirects to my home page) I utilize my <body> tag.

home.jsp

<body onload="getAccessToken()">

Here in my tag I have it call this function once the page loads. This calls the function getAccessTokens().

/**
 * The bread and butter to calling the API. This function will be called once the
 * user is redirected to the home page on success and without rejecting the terms
 * we are demanding. Once through, this function parses the url for the access token
 * and then stores it to be used later or when navigating away from the home page.
 */
function getAccessToken() {

    access_token = sessionStorage.getItem("accessToken");

    if (access_token === null) {
        if (window.location.hash) {
            console.log('Getting Access Token');

            var hash = window.location.hash.substring(1);
            var accessString = hash.indexOf("&");

            /* 13 because that bypasses 'access_token' string */
            access_token = hash.substring(13, accessString);
            console.log("Access Token: " + access_token);

            /* If first visit or regaining token, store it in session. */    
            if (typeof(Storage) !== "undefined") {
                /* Store the access token */
                sessionStorage.setItem("accessToken", access_token); // store token.

                /* To see if we need a new token later. */
                sessionStorage.setItem("tokenTimeStamp", secondsSinceEpoch);

                /* Token expire time */
                sessionStorage.setItem("tokenExpireStamp", secondsSinceEpoch + 3600);
                console.log("Access Token Time Stamp: "
                + sessionStorage.getItem("tokenTimeStamp")
                + " seconds\nOR: " + dateNowMS + "\nToken expires at: "
                + sessionStorage.getItem("tokenExpireStamp"));
            } else {
                alert("Your browser does not support web storage...\nPlease try another browser.");
            }
        } else {
            console.log('URL has no hash; no access token');
        }
    } else if (upTokenTime >= tokenExpireSec) {
        console.log("Getting a new acess token...Redirecting");

        /* Remove session vars so we dont have to check in implicitGrantFlow */
        sessionStorage.clear();

        $(location).attr('href', 'index.html'); // Get another access token, redirect back.

    } else {
        var timeLeft = (tokenExpireSec - upTokenTime);
        console.log("Token still valid: " + Math.floor(timeLeft / 60) + " minutes left.");
    }

Here I am storing the token in session storage once I obtain the access token from the url. I use the process mentioned in my earlier post but here is the full JavaScript. If it is still unclear after the comments please let me know.

Now that we have our access token obtained and stored we can now make an api call. Here is how I do it (and have been using qQuery, an example of getting a user's top tracks).

Example api call

/**
 * Function will get the user's top tracks depending on the limit and offset
 * specified in addition to the time_range specified in JSON format.
 * @param time_range short/medium/long range the specifies how long ago.
 * @param offset Where the indexing of top tracks starts.
 * @param limit How many tracks at a time we can fetch (50 max.)
 */
function getUserTopTracks(time_range, offset, limit) {

$.get({
    url: 'https://api.spotify.com/v1/me/top/tracks',
    headers: {
        'Authorization': 'Bearer ' + access_token,
    },
    data: {
        limit: limit, // This is how many tracks to show (50 max @ a time).
        offset: offset, // 0 = top of list, increase to get more tracks.
        time_range: time_range // short/medium/long_term time ranges.
    },
    success: function (response) {

        /* Get the items from the response (The limit) tracks. */
        res = JSON.parse(JSON.stringify(response.items));

        /* Get all the track details in the json */
        for (i = 0; i < res.length; i++) {
            console.log("Track: " + res[i]);
        }
    },
    fail: function () {
        console.log("getUserTopTracks(): api call failed!");
    }
});

The parameter time_range is specified as "long_term" to get the user's top tracks since the beginning (read more on Spotify's docs for more info) in addition to offset being 0 to start at the beginning and limit being equal to 50 since that is the max fetch per call.

On success I have my response variable 'response' and I then want the root of parsing to start from the 'items' section to make parsing easier (you do not have to do this, you can simply just use response.xxx.items.xxx). I then print to the console the response.

This is the basic things you can do and how you decide to handle the data or store it is up to you. I am not an expert, I only start learning web programming this past semester and a lot of the practices I am doing might be wrong or incorrect.

Bowen answered 8/12, 2019 at 22:56 Comment(12)
do you have an exmaple of the js code you used? The part I'm not understanding is how I get the token. My popup opens, the user signs in, then I have it redirect back to codepen, the popup doesnt automtiacally close right now, but as soon as the user signs in i want to close the popup. should i send the popup window its own javascript code for parsing the url? or is there some way i can add like an event listener function for when the user signs in, so that once the redirect url with the token is accessed i run the string parsing stuff to get just the access tokenEntomostracan
thanks for all your advice, ive been going over it and am working on getting past the initial ajax request to the auth endpoint, but am trying to fix a CORS error restricting my url from redirecting: #59420913Entomostracan
@Entomostracan I know what is happening. Refer to this link: #20035601 for a proper explanation. Basically, what I did to get around this to test on my local machine before it was done on a server was installing the 'CORS Everywhere' add on Firefox. If you are not using Firefox then I believe Chrome has a similar add on as well. When enabled it will bypass CORS policies and allow you to check if your request is actually working. Try this out and see if it works.Bowen
I declare upTokenTime as a global variable using: const tokenSecOld = sessionStorage.getItem("tokenTimeStamp"); const tokenExpireSec = sessionStorage.getItem("tokenExpireStamp"); const upTokenTime = parseInt(tokenSecOld) + (secondsSinceEpoch - parseInt(tokenSecOld)); Which I am just basically utilizing time since Epoch to constrain the hour the token is valid and make sure that the token still has time left to be used.Bowen
thanks for the help, I got the access token being generated / saved on my page succesfully. I'm trying to make the api call for getting top tracks by calling your function 'getUserTopTracks('long_term', 0, 15)'. I checked that my access token string is in the get request, but I'm getting a 403 console error: GET api.spotify.com/v1/me/top/… 403Entomostracan
it could be my scope, currently set to playlist-modify-publicEntomostracan
Yes, update your scopes. You need to include 'user-library-modify'Bowen
im trying to get the ''get user's top tracks" example api call you posted working, for which I think I only need scope=user-top-read correct? as soon as I change the scope (from 'playlist-modify-public' to 'user-top-read') used in my auth-url and the initial spotify auth ajax request , when I authorize I get an 'illigal redirect uri ' spotify webpage errorEntomostracan
You need to combine your scopes as some end points require multiple scope permissions. For example var scopes = 'playlist-modify-public user-top-read user-library-modify' and etc. You cannot just swap out a scope for another. Utilizing these scopes is what tells the user what you are accessing whenever they click accept on their first time visiting your app.Bowen
thanks, its interesting that if i change my scope, i get redirected to a page saying 'illigal_redirect_uri' instead of back to my site. wrote more about it here #59474301Entomostracan
again, thank you for the extensive posts. I plan on making a guide on my blog for this very topic soon to help other people like myself. turns out the spotify api incorrectly labels the 'create a playlist' endpoint. In their OFFICIAL DOCUMENTATION developer.spotify.com/console/post-playlists its labeled as "api.spotify.com/v1/playlists", when in reality it needs to be "api.spotify.com/v1/me/playlists" to work/. im going to report this to them. after fixing this my auth / create playlist action worksEntomostracan
Hey fantastic! Glad I could help, the api documentation for the most part is great but there are a few grey areas and inconsistencies. However, I am glad you got it to work.Bowen
J
0

You were on the right track.

The popup will redirect you to the website you added under redirect_uri=.... It will add the code and state query parameter to that url.

So on the webpage which is serving as your redirect_uri host, you can parse the complete URL.

It isn't possible to do it on a single page.

Justificatory answered 18/12, 2019 at 19:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.