-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Description
How to reproduce these conditions
Carry out instructions as per the github post. Then use credentials from Firebase by saving the spotify credentials in another file and using a '.then' promise every time spotify is called so that it retrieves the credentials in time. Then see that redirect works, but token does not.
Failing Function code used (including require/import commands at the top)
`'use strict';
const functions = require('firebase-functions');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
// Firebase Setup
const admin = require('firebase-admin');
const serviceAccount = require('./service-account.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: https://${process.env.GCLOUD_PROJECT}.firebaseio.com,
});
// Spotify OAuth 2 setup
const spotify = require('./spotify');
// TODO: Configure the spotify.client_id and spotify.client_secret Google Cloud environment variables.
const SpotifyWebApi = require('spotify-web-api-node');
// Scopes to request.
const OAUTH_SCOPES = ['user-read-email'];
/**
- Redirects the User to the Spotify authentication consent screen. Also the 'state' cookie is set for later state
- verification.
*/
exports.redirect = functions.https.onRequest((req, res) => {
cookieParser()(req, res, () => {
const state = req.cookies.state || crypto.randomBytes(20).toString('hex');
console.log('Setting verification state:', state);
res.cookie('state', state.toString(), {maxAge: 3600000, secure: true, httpOnly: true});
spotify.then(Spotify =>{
console.log('This is the client id used in redirect ' + Spotify.getClientId());
console.log('This is the client secret used in redirect ' + Spotify.getClientSecret());
const authorizeURL = Spotify.createAuthorizeURL(OAUTH_SCOPES, state.toString());
res.redirect(authorizeURL);
})
});
});
/**
-
Exchanges a given Spotify auth code passed in the 'code' URL query parameter for a Firebase auth token.
-
The request also needs to specify a 'state' query parameter which will be checked against the 'state' cookie.
-
The Firebase custom auth token is sent back in a JSONP callback function with function name defined by the
-
'callback' query parameter.
*/
exports.token = functions.https.onRequest((req, res) => {
try {cookieParser()(req, res, () => {
console.log('Received verification state:', req.cookies.state);
console.log('Received state:', req.query.state);
if (!req.cookies.state) {
throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.');
} else if (req.cookies.state !== req.query.state) {
throw new Error('State validation failed');
}
console.log('Received auth code:', req.query.code);
spotify.then(Spotify =>{
Spotify.authorizationCodeGrant(req.query.code, (error, data) => {
if (error) {
throw error;
}
console.log('Received Access Token:', data.body['access_token']);
Spotify.setAccessToken(data.body['access_token']);Spotify.getMe(async (error, userResults) => { if (error) { throw error; } console.log('Auth code exchange result received:', userResults); // We have a Spotify access token and the user identity now. const accessToken = data.body['access_token']; const spotifyUserID = userResults.body['id']; const profilePic = userResults.body['images'][0]['url']; const userName = userResults.body['display_name']; const email = userResults.body['email']; // Create a Firebase account and get the Custom Auth Token. const firebaseToken = await createFirebaseAccount(spotifyUserID, userName, profilePic, email, accessToken); // Serve an HTML page that signs the user in and updates the user profile. res.jsonp({token: firebaseToken}); }); });})
});
} catch (error) {
return res.jsonp({error: error.toString});
}
return null;
});
/**
- Creates a Firebase account with the given user profile and returns a custom auth token allowing
- signing-in this account.
- Also saves the accessToken to the datastore at /spotifyAccessToken/$uid
- @returns {Promise} The Firebase custom auth token in a promise.
*/
async function createFirebaseAccount(spotifyID, displayName, photoURL, email, accessToken) {
// The UID we'll assign to the user.
const uid =spotify:${spotifyID};
// Save the access token to the Firebase Realtime Database.
const databaseTask = admin.database().ref(/spotifyAccessToken/${uid}).set(accessToken);
// Create or update the user account.
const userCreationTask = admin.auth().updateUser(uid, {
displayName: displayName,
photoURL: photoURL,
email: email,
emailVerified: true,
}).catch((error) => {
// If user does not exists we create it.
if (error.code === 'auth/user-not-found') {
return admin.auth().createUser({
uid: uid,
displayName: displayName,
photoURL: photoURL,
email: email,
emailVerified: true,
});
}
throw error;
});
// Wait for all async tasks to complete, then generate and return a custom auth token.
await Promise.all([userCreationTask, databaseTask]);
// Create a Firebase custom auth token.
const token = await admin.auth().createCustomToken(uid);
console.log('Created Custom token for UID "', uid, '" Token:', token);
return token;
}`
^index.js
`const SpotifyWebApi = require('spotify-web-api-node');
// Firebase Setup
const admin = require('firebase-admin');
const serviceAccount = require('./service-account.json');
function regexIdAndSecret(clientIdOrSecret) {
const regex = /[(\w)]+/g;
let n;
let match;
while ((n = regex.exec(clientIdOrSecret)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (n.index === regex.lastIndex) {
regex.lastIndex++;
}
// The result can be accessed through the `n`-variable.
n.forEach((match, groupIndex) => {
console.log(`Found match, group ${groupIndex}: ${match}`);
});
console.log(`Found n, ${n}`);
return n;
}
}
class Credentials {
constructor(client_id, client_secret) {
this.client_id = client_id;
console.log('Id in class ' + this.client_id);
this.client_secret = client_secret;
console.log('Secret in class ' + this.client_secret);
}
}
module.exports = admin.firestore().collection('credentials').doc('marc.jwatts@gmail.com').get().then((snapshot) => {
let client_id = JSON.stringify(snapshot.data().client_id);
let client_secret = JSON.stringify(snapshot.data().client_secret);
// console.log(JSON.stringify(doc.data().client_id));
// Credentials.client_id = JSON.stringify(doc.data().client_id);
// console.log(JSON.stringify(doc.data().client_secret));
// Credentials.client_secret = JSON.stringify(doc.data().client_secret);
const credentials = new Credentials(regexIdAndSecret(client_id), regexIdAndSecret(client_secret));
const Spotify = new SpotifyWebApi({
client_id: credentials.client_id,
client_secret: credentials.client_secret,
redirectUri: `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com/popup.html`
});
Spotify.setClientId(credentials.client_id);
Spotify.setClientSecret(credentials.client_secret);
console.log('This is the client id after it has been set ' + credentials.client_id);
console.log('This is the client secret after it has been set ' + credentials.client_secret);
return Spotify;
});
`
^spotify.js
Steps to set up and reproduce
see above^^^
Debug output
Errors in the console logs
| { [WebapiError: Bad Request] name: 'WebapiError', message: 'Bad Request', statusCode: 400 } | more_vert |
|---|
Expected behavior
The auth code should be valid, and in fact it is as I have tried it on postman
Actual behavior
Error appears saying the auth code is not valid - no access code nor refresh token are therefore received.
Extra information
I have an app which requires credentials from Spotify. I am using a template (https://github.com/firebase/functions-samples/tree/master/spotify-auth) to sign in via Spotify but I am changing it so that the spotify client id and client secret credentials are retrieved from the Firestore database.
In order to save the credentials for use throughout the main index.js file, I am exporting the data via a spotify.js file and storing that within a const in the index.js file.
Everything works here apart from the exports.token function: this is the function that stores the auth code (from the address bar) and then uses the auth code in the authorisationCodeGrant. This eventually gets an access token and a refresh token. (see popup.html below for how this looks in the code. None of this was changed but it could be the reason for the problem)
For some reason, the auth code which is used comes back as 'invalid' on the authorisationCodeGrant and this means I don't get the access nor refresh tokens back.
As I mention, this problem does not occur when the details are in the same file. Does anyone know why the token part could be breaking due to data being from another file? What's even more strange is that the authorisation codes do indeed work perfectly well - I have tested these on Postman. But they come back as invalid in the code - no idea why! I thought it might be whitespace issue but this isn't it either, any ideas anyone? Got this project in tomorrow night so getting pretty frantic at this point...