I've created an app which works for Spotify Premium users only (PUT methods don't work for non-premium users according to Spotify's documentation). It's a ten-question interactive quiz where a playlist generates in your Spotify account, plays it and you have to guess the name of each song. It's generated with a NodeJS Backend and displayed via ReactJS. The game can be demoed here: https://am-spotify-quiz.herokuapp.com/
Code can be reviewed below:
server.js
const express = require('express');
const request = require('request');
const cors = require('cors');
const querystring = require('querystring');
const cookieParser = require('cookie-parser');
const client_id = ''; // Hiding for now
const client_secret = ''; // Hiding
const redirect_uri = 'https://am-spotify-quiz-api.herokuapp.com/callback/';
const appUrl = 'https://am-spotify-quiz.herokuapp.com/#';
/**
* Generates a random string containing numbers and letters
* #param {number} length The length of the string
* #return {string} The generated string
*/
var generateRandomString = function(length) {
var text = '';
var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (var i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
var stateKey = 'spotify_auth_state';
var app = express();
app.use(express.static(__dirname + '/public'))
.use(cors())
.use(cookieParser());
app.get('/login', function(req, res) {
var state = generateRandomString(16);
res.cookie(stateKey, state);
// scopes needed to make required functions work
var scope = 'user-read-private ' +
'user-read-email ' +
'user-read-playback-state ' +
'user-top-read ' +
'playlist-modify-public ' +
'playlist-modify-private ' +
'user-modify-playback-state ' +
'user-read-playback-state';
res.redirect('https://accounts.spotify.com/authorize?' +
querystring.stringify({
response_type: 'code',
client_id: client_id,
scope: scope,
redirect_uri: redirect_uri,
state: state
}));
});
app.get('/callback/', function(req, res) {
// your application requests refresh and access tokens
// after checking the state parameter
var code = req.query.code || null;
var state = req.query.state || null;
var storedState = req.cookies ? req.cookies[stateKey] : null;
if (state === null || state !== storedState) {
res.redirect(appUrl +
querystring.stringify({
access_token: access_token,
refresh_token: refresh_token
}));
} else {
res.clearCookie(stateKey);
var authOptions = {
url: 'https://accounts.spotify.com/api/token',
form: {
code: code,
redirect_uri: redirect_uri,
grant_type: 'authorization_code'
},
headers: {
'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64')),
},
json: true
};
request.post(authOptions, function(error, response, body) {
if (!error && response.statusCode === 200) {
var access_token = body.access_token,
refresh_token = body.refresh_token;
var options = {
url: 'https://api.spotify.com/v1/me',
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type': 'application/json' // May not need
},
body: { // Likely don't need this anymore!
'name': 'Test Playlist',
'public': false
},
json: true
};
// use the access token to access the Spotify Web API
request.get(options, function(error, response, body) {
console.log(body);
});
// we can also pass the token to the browser to make requests from there
res.redirect(appUrl +
querystring.stringify({
access_token: access_token,
refresh_token: refresh_token
}));
} else {
res.redirect(appUrl +
querystring.stringify({
error: 'invalid_token'
}));
}
});
}
});
// AM - May not even need this anymore!
app.get('/refresh_token', function(req, res) {
// requesting access token from refresh token
var refresh_token = req.query.refresh_token;
var authOptions = {
url: 'https://accounts.spotify.com/api/token',
headers: { 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64')) },
form: {
grant_type: 'refresh_token',
refresh_token: refresh_token
},
json: true
};
request.post(authOptions, function(error, response, body) {
if (!error && response.statusCode === 200) {
var access_token = body.access_token;
res.send({
'access_token': access_token
});
}
});
});
console.log('Listening on 8888');
app.listen(process.env.PORT || 8888);
I have a react component which displays as soon as the user is logged in, called premium.js. If you need all the code, you can see it here. Below are the two PUT methods that I need for my game; one to turn off the shuffle feature and the other one used to play the playlist:
removeShuffle() {
axios({
url: 'https://api.spotify.com/v1/me/player/shuffle?state=false',
method: "PUT",
headers: {
'Authorization': 'Bearer ' + this.state.accesstoken
}
})
.then((response) => {
console.log(response)
})
.catch((error) => {
console.log(error)
})
}
// Then... play the playlist to get started
playPlaylist(contextUri) {
axios({
url: 'https://api.spotify.com/v1/me/player/play',
method: "PUT",
data: {
context_uri: contextUri
},
headers: {
'Authorization': 'Bearer ' + this.state.accesstoken
}
})
.then((response) => {
console.log(response)
})
.catch((error) => {
console.log(error)
})
}
These work perfectly fine when I, the creator of the game, try it; however, I had another premium user try it and found this error:
This doesn't seem to make much sense as I've discovered this error happens with another user, regardless of whether they are using Windows or Mac. Does anyone know what this means, and how can I solve? Thanks in advance!
I've also been using Spotify's API and I eventually got the same error when trying to PUT https://api.spotify.com/v1/me/player/play after an inactivity period, where no device was marked as active (I don't know exactly how long, but no more than a couple of hours).
Apparently one device must be set as active so that you can invoke the play endpoint successfully.
If you want to change the status of a device as active, according to their documentation, you can first try to GET https://api.spotify.com/v1/me/player/devices in order to obtain the list of available devices:
// Example response
{
"devices" : [ {
"id" : "5fbb3ba6aa454b5534c4ba43a8c7e8e45a63ad0e",
"is_active" : false,
"is_private_session": true,
"is_restricted" : false,
"name" : "My fridge",
"type" : "Computer",
"volume_percent" : 100
} ]
}
and then select one of the available devices by invoking the player endpoint PUT https://api.spotify.com/v1/me/player, including:
device_ids Required. A JSON array containing the ID of the device on which playback should be started/transferred.
For example: {device_ids:["74ASZWbe4lXaubB36ztrGX"]}
Note: Although an array is accepted, only a single device_id is currently supported. Supplying more than one will return 400 Bad Request
play with value true if you want to start playing right away.
Most likely you didn't get that error yourself because one of your devices was already active when you tested it. If you have no activity during a couple of hours on your own account and then try to invoke the v1/me/player/play endpoint, I'd expect you to get the same error.
An easy workaround to make sure that this was in fact your problem would be to ask your test user to please start playing a song on the Spotify app (no matter which), then pause it, and then trigger the function on your app that invokes the v1/me/player/play endpoint. That shouldn't return the No active device found error anymore.
The way I understand it is you are trying to play a playlist that does not belong to the current user (/me) Which could be the cause of the error.
Related
I am currently writing to an API to try and get a token. I'm nearly there but fallen at the last hurdle..
const fs = require('fs');
const https = require('https');
const ConfigParams = JSON.parse(fs.readFileSync('Config.json', 'utf8'));
const jwt = require('jsonwebtoken');
const apikey = ConfigParams.client_id;
var privateKey = fs.readFileSync(**MY KEY**);
var tkn;
const jwtOptions = {
algorithm: 'RS512',
header: { kid: 'test-1' }
}
const jwtPayload = {
iss: apikey,
sub: apikey,
aud: **API TOKEN ENDPOINT**,
jti: '1',
exp: 300
}
jwt.sign(jwtPayload,
privateKey,
jwtOptions,
(err, token) => {
console.log(err);
//console.log(token);
tkn = token;
let = tokenPayload = {
grant_type: 'client_credentials',
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer/',
client_assertion: tkn
}
tokenAuthOptions = {
payload: tokenPayload,
host: **HOST**,
path: **PATH**,
method: 'POST',
}
https.request(
tokenAuthOptions,
resp => {
var body = '';
resp.on('data', function (chunk) {
body += chunk;
});
resp.on('end', function () {
console.log(body);
console.log(resp.statusCode);
});
}
).end();
}
)
the encoded token comes back fine for the first part, the https request though returns a problem.
the response I get back is grant_type is missing, so I know I have a formatting problem due to this x-www-form-urlencoded, but I can't figure out how to fix it.
here is what the website said:
You need to include the following data in the request body in
x-www-form-urlencoded format:
grant_type = client_credentials client_assertion_type =
urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion = <your signed JWT from step 4> Here's a complete
example, as a CURL command:
curl -X POST -H "content-type:application/x-www-form-urlencoded"
--data \ "grant_type=client_credentials\ &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion="
END POINT
Ideally I want a solution using the https request, but if that's not possible I'm open to other solutions.
Any help is greatly appreciated.
Thanks,
Craig
Edit - I updated my code based on a suggestion to:
const params = new url.URLSearchParams({
grant_type: 'client_credentials',
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer/',
client_assertion: tkn
});
axios.post("URL", params.toString()).then(resp => {
console.log("response was : " + resp.status);
}).catch(err => {
console.log("there was an error: " + err);
})
But I'm still getting an error code 400, but now with less detail as to why. (error code 400 has multiple message failures)
Postman is the best.
Thank for #Anatoly for your support which helped to point me in the right direction. I had no luck so used postman for the first time, and found it had a code snippet section, with four different ways of achieving this using node.js.
The solution with Axion was:
const axios = require('axios').default;
const qs = require('qs');
var data = qs.stringify({
'grant_type': 'client_credentials',
'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion': tkn
});
var config = {
method: 'post',
url: '',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: data
};
axios(config)
.then(function (response) {
console.log(JSON.stringify(response.status));
})
.catch(function (error) {
console.log(error);
});
I believe the issue was that I was not passing the information into 'data:' in combination with the querystring problem. Using qs.stringify to format the object, then passing this into the data: key solved the problem.
I have an issue with binance API signature for their REST API.
When trying to hit the route 'http://binance.com/api/v3/account', I get the following error:
{"code":-1022,"msg":"Signature for this request is not valid."}
I use nodejs and express.
I've seen there is a few questions on this subject but none seems to solve my problem so:
I define keys and urls there
const binanceConfig = {
API_URL: 'http://binance.com',
API_ENDPOINT: '/api/v3/account',
API_KEY: 'API_KEY_EXAMPLE',
API_SECRET: 'API_SECRET_EXAMPLE'
}
I create the signature
function generateSignature() {
const dataQueryString = "recvWindow=60000×tamp=" + Date.now();
return crypto
.createHmac('sha256', binanceConfig['API_SECRET'])
.update(dataQueryString)
.digest('hex');
}
I define query parameters here
const queryParameters = {
timestamp: Date.now(),
signature: generateSignature(),
recvWindow: '60000',
}
Set the header
var header = {
'Accept': 'Application/json',
'X-MBX-APIKEY': binanceConfig['API_KEY']
};
Create the route to call the API
router.get('/userInfo', (req, res) => {
var stringTest = `timestamp=${queryParameters['timestamp']}`
requestUrl = binanceConfig['API_URL'] + binanceConfig['API_ENDPOINT'] + "?" + stringTest + "&" + "signature=" + queryParameters['signature'] + "&recvWindow=" + queryParameters['recvWindow'];
const options = {
url: requestUrl,
headers: header,
method: 'GET'
}
request(options, (error, response) => {
if (error) { console.log('ERROR'); }
console.log(`Response: ${response.statusCode}`);
console.log(response.body);
});
});
If anyone has any idea why I get this error I'd be gratefull ! Thanks !
You need to have the timestamp and signature (respectively) as the last parameters.
Source
I was just experimenting with the DBS API Sandbox here (https://www.dbs.com/developers/). We have to use OAuth2 to get the access token before passing it in the header to authenticate API calls. When I tested everything out on their test client UI I got the results back from the API call.
However, when I coded my simple node app, I could successfully perform OAuth2 and get an access token, but when passed in together with the clientId, got a server error 500 from the API.
For example, the GET call to get a bank account details:
const options = {
method: 'GET',
uri: dbsHost + rootPath + '/accounts/22909292550739201999095',
headers: {
'Content-Type': 'application/json',
'accessToken': accessToken,
'clientId': clientId
}
}
request(options, (error, response, body) => {
// Server error 500
console.log(response)
})
These were the exact instructions as those given on the test client UI. On closer inspection I realised the access token I always got back had 488 characters, but that of the test client UI only had 475. Any reason for this difference? How do I know OAuth2 was performed correctly and I'm getting the right access token?
Here's how I did OAuth2:
On the frontend:
let authorizationUri = dbsHost + rootPath + '/oauth/authorize'
authorizationUri += '?client_id=' + clientId
authorizationUri += '&redirect_uri=' + redirectUri
authorizationUri += '&scope=Read'
authorizationUri += '&response_type=code'
authorizationUri += '&state=0399'
windows.location.assign(authorizationUri)
And at the redirect_uri endpoint once I get back the authorization code:
app.get('/authCallback', (req, res) => {
let authCode = req.query.code
// Encode client_id:client_secret in base-64 to pass in as Auth Basic Header
let b64 = new Buffer(clientId + ':' + clientSecret).toString('base64')
const options = {
method: 'POST',
url: dbsHost + rootPath + '/oauth/tokens',
headers: {
'Authorization': 'Basic ' + b64
},
form: {
'code': authCode,
'redirect_uri': redirectUri,
'grant_type': 'token',
'state': '0399'
}
}
request(options, (error, response, body) => {
if (error) {
console.error('Access Token Error', error.message)
return res.status(500).json('Authentication failed');
}
let json = JSON.parse(body)
let accessToken = json['access_token']
// Make a call with this access token
})
})
After a time I finally figure out how to use oAuth2 and how to create a access token with the refresh token. But I can´t find node.js samples for upload files the only thing I found was this module https://www.npmjs.com/package/onedrive-api
But this didn´t work for me because I get this error { error: { code: 'unauthenticated', message: 'Authentication failed' } }
Also if I would enter accessToken: manually with 'xxxxxxx' the result would be the same.
But I created before the upload the access token so I don´t know if this really can be a invalid credits problem. But the funny thing is if I would take the access token from https://dev.onedrive.com/auth/msa_oauth.htm where you can generate a 1h access token the upload function works. I created my auth with the awnser from this question. OneDrive Code Flow Public clients can't send a client secret - Node.js
Also I only used the scope Files.readWrite.all do I maybe need to allow some other scopes ? My code is
const oneDriveAPI = require('onedrive-api');
const onedrive_json_configFile = fs.readFileSync('./config/onedrive.json', 'utf8');
const onedrive_json_config = JSON.parse(onedrive_json_configFile);
const onedrive_refresh_token = onedrive_json_config.refresh_token
const onedrive_client_secret = onedrive_json_config.client_secret
const onedrive_client_id = onedrive_json_config.client_id
// use the refresh token to create access token
request.post({
url:'https://login.microsoftonline.com/common/oauth2/v2.0/token',
form: {
redirect_uri: 'http://localhost/dashboard',
client_id: onedrive_client_id,
client_secret: onedrive_client_secret,
refresh_token: onedrive_refresh_token,
grant_type: 'refresh_token'
}
}, function(err,httpResponse,body){
if (err) {
console.log('err: ' + err)
}else{
console.log('body full: ' + body)
var temp = body.toString()
temp = temp.match(/["]access[_]token["][:]["](.*?)["]/gmi)
//console.log('temp1: ', temp)
temp = temp.join("")
temp = temp.replace('"access_token":"','')
temp = temp.replace('"','')
temp = temp.replace('\n','')
temp = temp.replace('\r','')
//console.log('temp4: ', temp)
oneDriveAPI.items.uploadSimple({
accessToken: temp,
filename: 'box.zip',
parentPath: 'test',
readableStream: fs.createReadStream('C:\\Exports\\box.zip')
})
.then((item,body) => {
console.log('item file upload OneDrive: ', item);
console.log('body file upload OneDrive: ', body);
// returns body of https://dev.onedrive.com/items/upload_put.htm#response
})
.catch((err) => {
console.log('Error while uploading File to OneDrive: ', err);
});
} // else from if (err) { from request.post
}); // request.post({ get access token with refresh token
Can you send me your sample codes please to upload a file to OneDrive API with node.js. Would be great. Thank you
Edit: I also tried to upload a file with this
var uri = 'https://api.onedrive.com/v1.0/drive/root:/' + 'C:/files/file.zip' + ':/content'
var options = {
method: 'PUT',
uri: uri,
headers: {
Authorization: "Bearer " + accesstoken
},
json: true
};
request(options, function (err, res, body){
if (err) {
console.log('#4224 err:', err)
}
console.log('#4224 body:', body)
});
Same code: 'unauthenticated' stuff :/
How about this sample script? The flow of this sample is as follows.
Retrieve access token using refresh token.
Upload a file using access token.
When you use this sample, please import filename, your client id, client secret and refresh token. The detail information is https://dev.onedrive.com/items/upload_put.htm.
Sample script :
var fs = require('fs');
var mime = require('mime');
var request = require('request');
var file = 'c:/Exports/box.zip'; // Filename you want to upload on your local PC
var onedrive_folder = 'samplefolder'; // Folder name on OneDrive
var onedrive_filename = 'box.zip'; // Filename on OneDrive
request.post({
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
form: {
redirect_uri: 'http://localhost/dashboard',
client_id: onedrive_client_id,
client_secret: onedrive_client_secret,
refresh_token: onedrive_refresh_token,
grant_type: 'refresh_token'
},
}, function(error, response, body) {
fs.readFile(file, function read(e, f) {
request.put({
url: 'https://graph.microsoft.com/v1.0/drive/root:/' + onedrive_folder + '/' + onedrive_filename + ':/content',
headers: {
'Authorization': "Bearer " + JSON.parse(body).access_token,
'Content-Type': mime.lookup(file),
},
body: f,
}, function(er, re, bo) {
console.log(bo);
});
});
});
If I misunderstand your question, I'm sorry.
I'm trying to implement https://developers.podio.com/doc/items/add-new-item-22362 Podio API addItem call in a nodejs module. Here is the code:
var _makeRequest = function(type, url, params, cb) {
var headers = {};
if(_isAuthenticated) {
headers.Authorization = 'OAuth2 ' + _access_token ;
}
console.log(url,params);
_request({method: type, url: url, json: true, form: params, headers: headers},function (error, response, body) {
if(!error && response.statusCode == 200) {
cb.call(this,body);
} else {
console.log('Error occured while launching a request to Podio: ' + error + '; body: ' + JSON.stringify (body));
}
});
}
exports.addItem = function(app_id, field_values, cb) {
_makeRequest('POST', _baseUrl + "/item/app/" + app_id + '/',{fields: {'title': 'fgdsfgdsf'}},function(response) {
cb.call(this,response);
});
It returns the following error:
{"error_propagate":false,"error_parameters":{},"error_detail":null,"error_description":"No matching operation could be found. No body was given.","error":"not_found"}
Only "title" attribute is required in the app - I checked that in Podio GUI. I also tried to remove trailing slash from the url where I post to, then a similar error occurs, but with the URL not found message in the error description.
I'm going to setup a proxy to catch a raw request, but maybe someone just sees the error in the code?
Any help is appreciated.
Nevermind on this, I found a solution. The thing is that addItem call was my first "real"-API method implementation with JSON parameters in the body. The former calls were authentication and getApp which is GET and doesn't have any parameters.
The problem is that Podio supports POST key-value pairs for authentication, but doesn't support this for all the calls, and I was trying to utilize single _makeRequest() method for all the calls, both auth and real-API ones.
Looks like I need to implement one for auth and one for all API calls.
Anyway, if someone needs a working proof of concept for addItem call on node, here it is (assuming you've got an auth token beforehand)
_request({method: 'POST', url: "https://api.podio.com/item/app/" + app_id + '/', headers: headers, body: JSON.stringify({fields: {'title': 'gdfgdsfgds'}})},function(error, response, body) {
console.log(body);
});
You should set content-type to application/json
send the body as stringfied json.
const getHeaders = async () => {
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json; charset=utf-8',
};
const token = "YOUR APP TOKEN HERE";
headers.Authorization = `Bearer ${token}`;
return headers;
}
const createItem = async (data) => {
const uri = `https://api.podio.com/item/app/${APP_ID}/`;
const payload = {
fields: {
[data.FIELD_ID]: [data.FIELD_VALUE],
},
};
const response = await fetch(uri, {
method: 'POST',
headers: await getHeaders(),
body: JSON.stringify(payload),
});
const newItem = await response.json();
return newItem;
}