Spotify node web api - trouble with multiple users - node.js

I am working on an app that uses Spotify Node web API and having trouble when multiple users login into my application. I am successfully able to go through authentication flow and get the tokens and user ID after a user logs in. I am using the Authorization Code to authorize user (since I would like to get refresh tokens after expiration). However, the current problem is that getUserPlaylists function described here (FYI, if the first argument is undefined, it will return the playlists of the authenticated user) returns playlists of the most recently authenticated user instead of the user currently using the app.
Example 1: if user A logins in to the application, it will get its playlists fine. If user B logins in to the application, it also sees its own playlists. BUT, if user A refreshes the page, user A sees the playlists of the user B (instead of its own, user A playlists).
Example 2: user A logs in, user B can see user A's playlists just by going to the app/myplaylists route.
My guess is, the problem is with this section of the code
spotifyApi.setAccessToken(access_token);
spotifyApi.setRefreshToken(refresh_token);
The latest user tokens override whatever user was before it and hence the previous user is losing grants to do actions such as viewing its own playlists.
Expected behavior: user A sees own playlists after user B logs in event after refreshing the page.
Actual behavior: user A sees user B's playlists after user B logged in and user A refreshes the page.
I am aware that I could use the tokens without using the Spotify Node API
and just use the tokens to make requests and it should probably be fine, however, it would be great to still be able to use the Node API and to handle multiple users.
Here is the portion of code that most likely has problems:
export const createAuthorizeURL = (
scopes = SCOPE_LIST,
state = 'spotify-auth'
) => {
const authUrl = spotifyApi.createAuthorizeURL(scopes, state);
return {
authUrl,
...arguments
};
};
export async function authorizationCodeGrant(code) {
let params = {
clientAppURL: `${APP_CLIENT_URL || DEV_HOST}/app`
};
try {
const payload = await spotifyApi.authorizationCodeGrant(code);
const { body: { expires_in, access_token, refresh_token } } = payload;
spotifyApi.setAccessToken(access_token);
spotifyApi.setRefreshToken(refresh_token);
params['accessToken'] = access_token;
params['refreshToken'] = refresh_token;
return params;
} catch (error) {
return error;
}
return params;
}
export async function getMyPlaylists(options = {}) {
try {
// if undefined, should return currently authenticated user
return await spotifyApi.getUserPlaylists(undefined, options);
} catch (error) {
return error;
}
}
Would appreciate any help on this. I am really excited about what I am making so it would mean a LOT if someone could help me find the issue...

You're on the right track. When you set your access token and refresh token, though, you're setting it for your entire application, and all users who call your server will use it. Not ideal.
Here's a working example of the Authorization Code Flow in Node: https://glitch.com/edit/#!/spotify-authorization-code
As you can see, it uses a general instance of SpotifyWebApi to handle authentication, but it instantiates a new loggedInSpotifyApi for every request to user data, so you get the data for the user who's asking for it.
If you want to use the above example, you can just start editing to "remix" and create your own copy of the project.
Happy hacking!

Related

Edit User's Custom Claims from Firebase

I am using firebase to generate JWT tokens to authorize access to a hasura graphql server.
I want an end user to have a callable firebase function that they can call from the app so they can change the x-hasura-role in their claims without changing other parts of their claims. I am guessing the best way to do this is to export the old custom user claims and set a new role inputted by the user.
PseudoCode:
exports.changeUserType = functions.https.onCall( async (data, context) => {
var userType = data.usertype;
// get the old user claims somehow
// check if user should be able to change their userType via a graphql query
...
// edit the user claims
return admin.auth().setCustomUserClaims(userType, {
'https://hasura.io/jwt/claims': {
'x-hasura-role': userType,
'x-hasura-default-role': 'orgdriver',
'x-hasura-allowed-roles': ['orgauditor', 'orgdriver', 'orgmanager', 'orgadmin', 'orgdirector'],
'x-hasura-user-id': user.uid // <-- from the old claims so user can't edit
}
});
If there is a better way to do this, maybe by grabbing a user's id from the auth database by checking who ran the function please tell me. Thank you in advance.
When a Firebase Authenticated user hits a Firebase Function, their uid is passed in through context. I would ensure they are authenticated first:
if (context.auth == undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The user must be authenticated.',
);
}
Then I would grab their uid:
const uuid = context?.auth?.uid as string;
Then you can get their user using the firebase-admin library's getAuth():
// get user
const user = await getAuth().getUser(uuid);
Now finally you can set your new custom claim property:
// set the hasura role
return await getAuth().setCustomUserClaims(uuid, {
...user.customClaims,
'x-hasura-role': userType,
});
Be sure to import:
import { getAuth } from 'firebase-admin/auth';
In this way you can safely know the user is authenticated and a uid exists, then you can simply grab the user and all their existing claims, then when you go to update destructure all existing claims values, and update the one value you want.
In this way get all the user's old claims, ensure they are authenticated, retain all old claim properties, and update the one thing you want to update.
I hope that helps out!

Are fcmTokens and ID Tokens the same and how to verify them with Node.js as a cloud function?

My app uses fcmTokens assigned to a user and stored in a Firestore document to keep track of app installations and logins. When a user logs out of the app I delete the fcmToken from the Firestore document and run InstanceID.instanceID().deleteID.
However when the user has bad internet 'InstanceID.instanceID().deleteID' is run again when the app starts the next time. The fcmToken in the Firestore document is not deleted in this case.
Theoretically I could also run a query in the app and search for this token in all of the Firestore user documents and delete it there but I rather would like to use cloud functions to check if the fcmTokens of a user are still valid. If not I want to delete them. I started writing the following cloud function but I am getting an error saying
Decoding Firebase ID token failed. Make sure you passed the entire string JWT which represents an ID token. See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
I assume I am using the wrong function and fcmTokens are not the same as ID Tokens?
Is there a way to check for the validity of the fcmToken similar to how I check here for the (non existent) ID Token.
Or should I somehow use ID Tokens in general for managing device specific login? ( I'm using a Snapshot listener that listens for fcmToken changes and I am logging the user out when a specific fcmToken is deleted.)
Here is my cloud function:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
var userA_UID = ""
exports.checkFcmToken = functions.firestore.document('registeredUsers/{userA_UID}').onUpdate(async (snapshot, context) => {
userA_UID = context.params.userA_UID;
const userInfo = await admin.firestore().collection('registeredUsers').doc(userA_UID).get();
const fcmTokens = userInfo.data()['fcmTokens'];
if (fcmTokens !== undefined) {
if (fcmTokens.length > 0) {
for (let fcmToken of fcmTokens) {
checkToken(fcmToken)
}
}
}
function checkToken(fcmToken) {
//will delete token from document array if invalid
admin.auth().verifyIdToken(fcmToken)
.then((decodedToken) => {
let uid = decodedToken.uid;
console.log(uid)
throw new Error('Error!')
}).catch((error) => {
console.log(error)
});
}
})
FCM tokens and ID tokens are quite different, and cannot be used interchangeably.
Firebase Authentication ID tokens identify a user. This means that if the same user is signed in on two different devices, they have ID tokens that identify the same user.
FCM tokens (also called Instance ID tokens) identify an application installation. If you have two tokens from two different devices, there is nothing that is shared between those tokens.
FCM tokens are opaque strings, and cannot be be verified without calling the FCM API.
When you send a message to an outdated token, the FCM API responds with a clear error message. The idiomatic way to keep your list of tokens clean is to handle this error message and remove the outdated token, as shown in this example from the Cloud Functions repo.

Sign out different user

I would like to be able to sign out some user that is logged in. By this I do not mean current user but some other user.
I, as an administrator, can deactivate or ban a user. At this point I would like to get this user and sign him out so when he makes his next request he gets redirected to log in page. After login attempt he would get a message why he cannot log in.
I am aware I can do it by writing my own authorization filter and go and fetch user from database each time but I would, if possible, like to avoid that.
Setting up authentication as follows hits mz OnValidatePrincipal each time a request is made but I do not know how to update exactly this user.
services.AddAuthentication("CookieAuthentication")
.AddCookie("CookieAuthentication", options => {
options.LoginPath = "/Login";
options.Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = new Func<CookieValidatePrincipalContext, Task>(async (a) =>
{
var expired = a.Principal.FindFirst(System.Security.Claims.ClaimTypes.Expired);
if(expired != null && bool.Parse(expired.Value))
{
a.RejectPrincipal();
return;
}
await Task.CompletedTask;
})
};
});
I am also aware that I could write my own middleware and keep some kind of list of all the users that have logged in. That list could than be easily updated by an administrator and checked by the previous event.
What I would like to know is, can I get an instance of other logged user and can I than alter its claims?

Authorising a Spotify session on a headless system

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.
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.
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:
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())
}
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);
}
}
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
}
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
}
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)
}
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;
}
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;
}
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.
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.

Retrieve role in jhipster immediately after Auth.login

I want to send a user to a particular view after login based on a role that person has. For example, I want to send a user with ROLE_STUDENT to a student page, and a person with ROLE_TEACHER to a teacher page. Unfortunately, if I call isInRole in the controller immediately after Auth.login, that fails. Specifically, in the login function (which I moved to main.controller.js so that the login dialog appears on the main page), I have code like this:
$scope.login = function () {
Auth.login({
username: $scope.username,
password: $scope.password
}).then(function (account, $state) {
Principal.identity(true);
$scope.authenticationError = false;
$scope.account = account;
$scope.isAuthenticated = Principal.isAuthenticated;
$scope.isInRole = Principal.isInRole;
if ($scope.isInRole('ROLE_STUDENT')) {
$scope.state.go('student_dashboard');
}
}).catch(function () {
$scope.authenticationError = true;
});
};
However, the isInRole method always returns false. If I debug it, I can see in principal.service.js shows that at this point, _authenticated is false and _identity is undefined.
Now, if I comment out the isInRole conditional in the controller, so that the user always goes to the student_dashboard page, I can put isInRole code on the student_dashboard page itself, and it works great. So, it appears that something is happening between the time of the redirect, and the time the target page loads, and I want to know what that is so that I can cause it to happen and thus determine if a user has a particular role and then redirect appropriately.
I believe your problem is with
Principal.identity(true);
This actually returns a promise which does an ajax call to update the principal user, so to use the principal functions you will need to do something like this
Principal.identity(true).then(function(profile) {
$scope.authenticationError = false;
$scope.account = account;
$scope.isAuthenticated = Principal.isAuthenticated;
$scope.isInRole = Principal.isInRole;
if ($scope.isInRole('ROLE_STUDENT')) {
$scope.state.go('student_dashboard');
}
});
Otherwise the current identity is undefined.

Resources