I'm working on updating a chrome extension to MV3, and therefore I can't use the firebase UI to login any more. What I trying to do is use chrome.identity.launchWebAuthFlow, get the token, create the credential, and sign in with firebase.
Here's what I have:
function launchGoogleAuthFlow(interactive) {
return new Promise((resolve, reject) => {
console.log('launching webauthflow')
const manifest = chrome.runtime.getManifest();
const clientId = encodeURIComponent(manifest.oauth2.client_id);
const scopes = encodeURIComponent(manifest.oauth2.scopes.join(' '));
let redirectUri = chrome.identity.getRedirectURL();
let nonce = Math.random().toString(36).substring(2, 15)
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', clientId);
authUrl.searchParams.set('response_type', 'id_token');
authUrl.searchParams.set('redirect_uri', redirectUri);
// Add the OpenID scope. Scopes allow you to access the user’s information.
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('nonce', nonce);
// Show the consent screen after login.
authUrl.searchParams.set('prompt', 'consent');
chrome.identity.launchWebAuthFlow(
{
'url': authUrl.href,
'interactive': interactive
},
(redirectedTo) => {
if (chrome.runtime.lastError) {
console.log(chrome.runtime.lastError.message);
resolve(null)
}
else {
let idToken = redirectedTo.substring(redirectedTo.indexOf('id_token=') + 9)
idToken = idToken.substring(0, idToken.indexOf('&'))
resolve(idToken)
}
}
)
})
}
launchGoogleAuthFlow(true).then((token)=>{
if (token) {
console.log('token:' + token);
const credential = GoogleAuthProvider.credential(null, token);
console.log(credential);
signInWithCredential(auth, credential).then((result) => {
showMain();
document.getElementById('loggedInAs').textContent = result.email;
console.log("Success!!!")
console.log(result)
}).catch((error) => {
// You can handle errors here
console.log(error)
});
} else {
console.error('The OAuth token was null');
}
});
console.log('finished authflow');
}
I'm getting prompted to sign in with my google credentials, then in the console I get Failed to load resource: the server responded with a status of 400 ()
and then the a log from SignInWithCredentials
FirebaseError: Firebase: Unsuccessful check authorization response from Google: {
"error_description": "Invalid Value"
}
(auth/invalid-credential).
at _errorWithCustomMessage (index-6bd8d405.js:453:1)
at _performFetchWithErrorHandling (index-6bd8d405.js:973:1)
at async _performSignInRequest (index-6bd8d405.js:988:1)
at async _signInWithCredential (index-6bd8d405.js:4721:1)
In the response_type you are requesting an id_token:
authUrl.searchParams.set('response_type', 'id_token');
So your launchGoogleAuthFlow returns an id_token and that's it what you must give to GoogleAuthProvider.credential. This method expects an a id_token as the first parameter and an a access_token as the second parameter.
So all you have to do is change from:
const credential = GoogleAuthProvider.credential(null, token);
to:
const credential = GoogleAuthProvider.credential(token);
Everything should works fine.
If you may want the access_token you must request response_type=token and remove nonce. Finally you'll need to extract the returned access_token from response URL (your redirectedTo variable) as you did with id_token.
PS: In your code I also noticed that you got scopes from manifest but did not use them while requesting the token.
Related
I have a Firebase application that authenticates a user and returns an access token that I can then use to access the Google Calendar and Sheets API. I also save the refreshToken. Sample code for authenticated token:
firebase
.signInWithGoogle()
.then(async (socialAuthUser) => {
let accessToken = socialAuthUser.credential.accessToken // token to access Google Sheets API
let refreshToken = socialAuthUser.user.refreshToken
this.setState({accessToken, refreshToken})
})
After 1 hour, the accessToken expires. Firebase auth provides a refresh token on the user object after sign-in
I use that refresh token to re-authenticate and get a new access_token by posting to:
https://securetoken.googleapis.com/v1/token?key=firebaseAppAPIKey
That new access token does not work for Google APIs anymore, and it doesn't have the authorized scopes anymore. I also try sending it to
https://www.googleapis.com/oauth2/v1/tokeninfo?access_token="refreshToken"
It gives me the error "Invalid token". When I use the original token from firebase, it works just fine.
Anyone else encountering a similar issue? I haven't figured out a way to refresh the original access token with the correct access scopes without making the user sign-out and sign-in again.
Thanks!
I was finally able to solve it after many attempts.
Posted detailed solution on Medium: https://inaguirre.medium.com/reusing-access-tokens-in-firebase-with-react-and-node-3fde1d48cbd3
On the client, I used React with the Firebase library, and on the server I used Node.js with the packages google-apis and the firebase-admin skd package linked to the same Firebase project.
Steps:
(CLIENT) Send a request to the server to generate an authentication link
(SERVER) Generate Auth Link and send it back to the client using the getAuthLink() from googleapis. Sign in with Google and handle the redirect.
(SERVER) On the redirect route, use the code from Google on the query string to authenticate the user and get his user credentials. Use these credentials to check if the user is registered on Firebase.
(SERVER) If the user is registered, get the access and refresh tokens using the oauth2.getTokens(code), update refresh token on the user profile in the database. If the user is not registered, create a new user with firebase.createUser(), also create the user profile on the database with the refresh token.
(SERVER) Use firebase.createCustomToken(userId) to send an id_token back to client and authenticate.
(SERVER) Use a res.redirect({access_token, referesh_token, id_token}) to send credentials back to client.
(CLIENT) On the client, use the signInWithCustomToken(id_token) to authenticate, also restructure the query to obtain access_token and refresh_token to send API calls.
(CLIENT) Set an expiration date for the access token. On each request, check if the current date is higher than the expiration date. If it is, request a new token to https://www.googleapis.com/oauth2/v4/token with the refresh token. Otherwise use the access_token stored.
Most stuff happens when handling the Google Redirect after authentication. Here's an example of handling auth and tokens on the backend:
const router = require("express").Router();
const { google } = require("googleapis");
const { initializeApp, cert } = require("firebase-admin/app");
const { getAuth } = require("firebase-admin/auth");
const { getDatabase } = require("firebase-admin/database");
const serviceAccount = require("../google-credentials.json");
const fetch = require("node-fetch");
initializeApp({
credential: cert(serviceAccount),
databaseURL: "YOUR_DB_URL",
});
const db = getDatabase();
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
"http://localhost:8080/handleGoogleRedirect"
);
//post to google auth api to generate auth link
router.post("/authLink", (req, res) => {
try {
// generate a url that asks permissions for Blogger and Google Calendar scopes
const scopes = [
"profile",
"email",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/calendar",
];
const url = oauth2Client.generateAuthUrl({
access_type: "offline",
scope: scopes,
// force access
prompt: "consent",
});
res.json({ authLink: url });
} catch (error) {
res.json({ error: error.message });
}
});
router.get("/handleGoogleRedirect", async (req, res) => {
console.log("google.js 39 | handling redirect", req.query.code);
// handle user login
try {
const { tokens } = await oauth2Client.getToken(req.query.code);
oauth2Client.setCredentials(tokens);
// get google user profile info
const oauth2 = google.oauth2({
version: "v2",
auth: oauth2Client,
});
const googleUserInfo = await oauth2.userinfo.get();
console.log("google.js 72 | credentials", tokens);
const userRecord = await checkForUserRecord(googleUserInfo.data.email);
if (userRecord === "auth/user-not-found") {
const userRecord = await createNewUser(
googleUserInfo.data,
tokens.refresh_token
);
const customToken = await getAuth().createCustomToken(userRecord.uid);
res.redirect(
`http://localhost:3000/home?id_token=${customToken}&accessToken=${tokens.access_token}&userId=${userRecord.uid}`
);
} else {
const customToken = await getAuth().createCustomToken(userRecord.uid);
await addRefreshTokenToUserInDatabase(userRecord, tokens);
res.redirect(
`http://localhost:3000/home?id_token=${customToken}&accessToken=${tokens.access_token}&userId=${userRecord.uid}`
);
}
} catch (error) {
res.json({ error: error.message });
}
});
const checkForUserRecord = async (email) => {
try {
const userRecord = await getAuth().getUserByEmail(email);
console.log("google.js 35 | userRecord", userRecord.displayName);
return userRecord;
} catch (error) {
return error.code;
}
};
const createNewUser = async (googleUserInfo, refreshToken) => {
console.log(
"google.js 65 | creating new user",
googleUserInfo.email,
refreshToken
);
try {
const userRecord = await getAuth().createUser({
email: googleUserInfo.email,
displayName: googleUserInfo.name,
providerToLink: "google.com",
});
console.log("google.js 72 | user record created", userRecord.uid);
await db.ref(`users/${userRecord.uid}`).set({
email: googleUserInfo.email,
displayName: googleUserInfo.name,
provider: "google",
refresh_token: refreshToken,
});
return userRecord;
} catch (error) {
return error.code;
}
};
const addRefreshTokenToUserInDatabase = async (userRecord, tokens) => {
console.log(
"google.js 144 | adding refresh token to user in database",
userRecord.uid,
tokens
);
try {
const addRefreshTokenToUser = await db
.ref(`users/${userRecord.uid}`)
.update({
refresh_token: tokens.refresh_token,
});
console.log("google.js 55 | addRefreshTokenToUser", tokens);
return addRefreshTokenToUser;
} catch (error) {
console.log("google.js 158 | error", error);
return error.code;
}
};
router.post("/getNewAccessToken", async (req, res) => {
console.log("google.js 153 | refreshtoken", req.body.refresh_token);
// get new access token
try {
const request = await fetch("https://www.googleapis.com/oauth2/v4/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
refresh_token: req.body.refresh_token,
grant_type: "refresh_token",
}),
});
const data = await request.json();
console.log("google.js 160 | data", data);
res.json({
token: data.access_token,
});
} catch (error) {
console.log("google.js 155 | error", error);
res.json({ error: error.message });
}
});
module.exports = router;
For anyone who comes across this now, there is a much easier way at this point.
I was able to solve this by implementing a blocking function that simply saved the refreshToken and exiry date to firestore. You can then query this from your frontend to get the tokens there as well.
Be sure to enable the refreshToken in the firebase settings, otherwise the blocking function won't have access to it.
https://firebase.google.com/docs/auth/extend-with-blocking-functions
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import {
AuthEventContext,
AuthUserRecord,
} from "firebase-functions/lib/common/providers/identity";
admin.initializeApp();
exports.beforeSignIn = functions.auth
.user()
.beforeSignIn((user: AuthUserRecord, context: AuthEventContext) => {
// If the user is created by Yahoo, save the access token and refresh token
if (context.credential?.providerId === "yahoo.com") {
const db = admin.firestore();
const uid = user.uid;
const data = {
accessToken: context.credential.accessToken,
refreshToken: context.credential.refreshToken,
tokenExpirationTime: context.credential.expirationTime,
};
// set will add or overwrite the data
db.collection("users").doc(uid).set(data);
}
});
I setup a Twitch OAuth integration using the Instagram example, now I can login into my app by opening the popup.html page that the example gave me.
Here's my adapted code:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const { AuthorizationCode } = require('simple-oauth2');
const fetch = require('node-fetch');
// Firebase Setup
const admin = require('firebase-admin');
// #ts-ignore
const serviceAccount = require('./service-account.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`,
});
const OAUTH_REDIRECT_URI = `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com/popup.html`;;
const OAUTH_SCOPES = 'user:read:email';
/**
* Creates a configured simple-oauth2 client for Twitch.
*/
function twitchOAuth2Client() {
// Twitch OAuth 2 setup
// TODO: Configure the `twitch.client_id` and `twitch.client_secret` Google Cloud environment variables.
const credentials = {
client: {
id: functions.config().twitch.client_id,
secret: functions.config().twitch.client_secret,
},
auth: {
tokenHost: 'https://id.twitch.tv',
tokenPath: '/oauth2/token',
authorizePath: '/oauth2/authorize',
},
options: {
bodyFormat: 'json',
authorizationMethod: 'body',
},
};
return new AuthorizationCode(credentials);
}
/**
* Redirects the User to the Twitch authentication consent screen. Also the 'state' cookie is set for later state
* verification.
*/
exports.redirect = functions.https.onRequest((req, res) => {
const authorizationCode = twitchOAuth2Client();
cookieParser()(req, res, () => {
const state = req.cookies.__session || crypto.randomBytes(20).toString('hex');
console.log('Setting verification state:', state);
res.cookie('__session', state.toString(), { maxAge: 3600000, httpOnly: true });
const redirectUri = authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI,
scope: OAUTH_SCOPES,
state: state,
});
console.log('Redirecting to:', redirectUri);
res.redirect(redirectUri);
});
});
/**
* Exchanges a given Twitch 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, display name, photo URL and Twitch acces token are sent back in a JSONP callback
* function with function name defined by the 'callback' query parameter.
*/
exports.token = functions.https.onRequest((req, res) => {
const authorizationCode = twitchOAuth2Client();
try {
cookieParser()(req, res, async () => {
try {
console.log('Received verification state:', req.cookies.__session);
console.log('Received state:', req.query.state);
if (!req.cookies.__session) {
throw new Error(
'State cookie not set or expired. Maybe you took too long to authorize. Please try again.'
);
} else if (req.cookies.__session !== req.query.state) {
throw new Error('State validation failed');
}
} catch (error) {
return res.jsonp({ error: error.toString() });
}
let accessToken;
try {
console.log('Received auth code:', req.query.code);
const options = {
client_id: functions.config().twitch.client_id,
client_secret: functions.config().twitch.client_secret,
code: req.query.code,
grant_type: 'authorization_code',
redirect_uri: OAUTH_REDIRECT_URI,
};
console.log('Asking token with options', JSON.stringify(options));
accessToken = await authorizationCode.getToken(options);
console.log('Auth code exchange result received');
const twitchUser = await getTwitchUser(accessToken.toJSON().access_token);
// Create a Firebase account and get the Custom Auth Token.
const firebaseToken = await createFirebaseAccount(twitchUser);
// Serve an HTML page that signs the user in and updates the user profile.
return res.jsonp({ token: firebaseToken });
} catch (error) {
return res.jsonp({ error: error.toString() });
}
});
} catch (error) {
return res.jsonp({ error: error.toString() });
}
});
/**
* Creates a Firebase account with the given user profile and returns a custom auth token allowing
* signing-in this account.
*
* #returns {Promise<string>} The Firebase custom auth token in a promise.
*/
async function createFirebaseAccount(twitchUser) {
// The UID we'll assign to the user.
const uid = `twitch:${twitchUser.id}`;
// Save the access token to the Firebase Database.
const db = admin.firestore();
const databaseTask = db.collection('users').doc(uid).set(twitchUser);
// Create or update the user account.
const userCreationTask = admin
.auth()
.updateUser(uid, {
displayName: twitchUser['display_name'],
photoURL: twitchUser['profile_image_url'],
email: twitchUser['email'],
})
.catch((error) => {
// If user does not exists we create it.
if (error.code === 'auth/user-not-found') {
return admin.auth().createUser({
uid: uid,
displayName: twitchUser['display_name'],
photoURL: twitchUser['profile_image_url'],
email: twitchUser['email'],
});
}
throw error;
});
// Wait for all async task 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;
}
async function getTwitchUser(accessToken) {
console.log('Fetching Twitch user with access_token', accessToken);
try {
const response = await fetch('https://api.twitch.tv/helix/users', {
method: 'GET',
headers: {
'Client-Id': functions.config().twitch.client_id,
Authorization: 'Bearer ' + accessToken,
},
});
const data = await response.json();
return { ...data.data[0], access_token: accessToken };
} catch (error) {
console.error(error);
}
}
I'd like, though, to login into Twitch using the firebase.auth().signInWithRedirect() method that I already use for Facebook and Google, unfortunately I can't find any documentation about this, and the Facebook provider source code refers to some externs.* resources so I'm not sure how to adapt it for my own needs.
Right now I have two endpoints/cloud functions: _twitchRedirect and _twitchToken, what should I do to integrate them with signInWithRedirect?
I was similarly curious, so spent a little time playing around with things today.
In short, when using Firebase Auth, I believe the providerId will need to be one of the existing supported providers.
If you upgrade to using the Google Cloud Identity Platform though, I believe you will be able to configure custom providers, and then use this function to authenticate:
https://cloud.google.com/identity-platform
We can see that firebase.auth.OAuthProvider and firebase.auth().signInWithPopup (or firebase.auth().signInWithRedirect) are used with a number of the providers here, eg.
https://cloud.google.com/identity-platform/docs/web/apple
https://cloud.google.com/identity-platform/docs/web/microsoft
In addition to these provider choices that we get with the standard Firebase Auth, Google Cloud Identity Platform allows us to also add SAML and OpenID Connect (OIDC) integrations:
https://cloud.google.com/identity-platform/docs/web/saml
https://cloud.google.com/identity-platform/docs/web/oidc
When adding a new identity provider using either of these, we are able to specify the 'Provider ID' to use (prefixed with either saml. or oidc.). This custom provider ID is then used with firebase.auth.OAuthProvider and firebase.auth().signInWithPopup (or firebase.auth().signInWithRedirect) as described above.
For example, if I created a new identity provider with an ID of oidc.foo, my integration code would end up looking like:
const provider = new firebase.auth.OAuthProvider('oidc.foo');
firebase.auth().signInWithPopup(provider)
.then((result) => {
// result.credential is a firebase.auth.OAuthCredential object.
// result.credential.providerId is equal to 'oidc.foo'.
// result.credential.idToken is the OIDC provider's ID token.
})
.catch((error) => {
// Handle error.
});
Based on my understanding of this, I believe we will only currently be able to add custom providers this way if they conform to the OpenID Connect (OIDC) standard (including the OIDC Discovery part, which uses a /.well-known/openid-configuration URL):
Note: If your OIDC provider doesn't comply with the OIDC specification for discovery, it won't work with Identity Platform.
So to my knowledge, the best way to implement 'normal' OAuth2 providers currently is the custom backend function flow you used above (based on the Firebase Auth examples).
As part of figuring this out, I decided to see what would happen if I used a provider ID that didn't match anything configured in my account (this is a fairly verbose step by step, and the main answer is already included above, but this may help provide some more context/help someone out, so including it here)
var provider = new firebase.auth.OAuthProvider("foo.example.com");
firebase
.auth()
.signInWithRedirect(provider)
.then((result) => console.log("OAuthProvider:", result))
.catch((error) => console.log("OAuthProvider::error:", error));
firebase
.auth()
.getRedirectResult()
.then((result) => console.log("RedirectResult:", result))
.catch((error) => console.log("RedirectResult::error:", error));
At first I go this auth/auth-domain-config-required error:
OAuthProvider::error: {
"code": "auth/auth-domain-config-required",
"message": "Be sure to include authDomain when calling firebase.initializeApp(), by following the instructions in the Firebase console."
}
I figured maybe this should be set to the OAuth provider I was wanting to login to, so I set authDomain in my firebase config to foo.myauthprovider.com, but when I called signInWithRedirect, it tried to load the following URL (where the apiKey is the API key of my firebase project), which failed to load:
https://foo.myauthprovider.com/__/auth/handler?apiKey=REDACTED&appName=%5BDEFAULT%5D&authType=signInViaRedirect&providerId=foo.example.com&redirectUrl=http%3A%2F%2Flocalhost%3A3000%2F&v=7.14.5
This /__/auth/handler URL is part of Firebase Auth's reserved URLs, which you can read more about at:
https://firebase.google.com/docs/hosting/reserved-urls#auth_helpers
And is explained a little better in this StackOverflow answer, but is basically what Firebase Auth uses to handle OAuth callbacks to avoid needing to expose sensitive credentials on the frontend, and so users don't need to implement their own handlers all the time):
Why does Firebase auth uses a "middleware" redirect before returning to my app?
Changing authDomain to the actual custom domain of my firebase project fixed that issue, and then resulted in the following auth/operation-not-allowed error when I tried to redirect:
RedirectResult::error: u {code: "auth/operation-not-allowed", message: "The identity provider configuration is not found.", a: null}
I'm trying to fetch google api data within my firebase app.
After login using firebase.auth().signInWithPopup, i get accessToken and idToken.
What I'm trying to do is after sign in, redirect to another page and fetch album data from google photos api with accessToken.
Here is my code for sign-in
export const signInWithGoogle = async () => {
try {
const response: any = await auth.signInWithPopup(provider);
const { accessToken, idToken } = response.credential;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('idToken', idToken);
} catch(e) {
console.log(e)
}
}
and this is what I'm gonna do in redirected page
const onClick = async (e) => {
const accessToken = localStorage.getItem('accessToken');
const response = await fetch('https://photoslibrary.googleapis.com/v1/albums', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
console.log(await response.json());
}
I actually get right data with above code but is it a good idea to store access token to local storage?
If it's not safe, how do I get access token in redirected page? or what is suggested/best practice for requesting google api in client side?
Here the code how i am generating the accessToken for Calling API.
import firebaseConfig from './../firebaseConfig';
import firebase from "firebase/app";
import "firebase/auth";
firebase.initializeApp(firebaseConfig);
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then(function(result) {
var token = result.credential.accessToken;
sessionStorage.setItem("__token", token); // Token saved
}.bind(this)).catch(function(error) {
});
With that code i am getting the token in sessionStorage.
Here's the snippet how i am using firebase Rest API.
var URL = https://firestore.googleapis.com/v1/projects/qtbt-a8bf8/databases/(default)/documents/users/[USER_ID]?key=[YOUR_API_KEY]
var token = sessionStorage.getItem("__token");
const config = {
headers: { Authorization: `Bearer ${token}` , Accept: 'application/json'}
};
axios.get(URL, config)
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
.finally(function () {
// always executed
});
In the axios call, i am getting Error 403.
Response:
code: 403
message: "Request had insufficient authentication scopes."
status: "PERMISSION_DENIED"
your missing the api-key in the call. apikey and token should both be in the headers.
var token = sessionStorage.getItem("__token");// verify your token is correct.
var headers = {'x-api-key' : 'key valu goes here', 'authorization': ${token} }
The security rule is slightly different when using an IdToken rather than an OAuth token.
Instead of using:
if request.auth.uid == resource.id;
I use:
if request.auth.token.email == resource.id;
I'm matching the token's owner email to the document id, which also happens to be the owner's email.
You can also access the user's child documents using something like:
request.auth.token.email == resource.data.ownerEa;
In that case, the document(s) being matched must have an attribute ownerEa that has value == user's email.
Very annoying documentation isn't it.
Hope that helps...
I have been working on a oauth2 flow for spotify by following this similar tutorial by the Firebase team for Instagram HERE
I am able to submit my credentials and return the user code and state in the url, but when I run the method to submit the code to return an auth token, the auth token that I print to console in the Firebase functions returns: Auth Token Error Not Found. Here's my workflow:
Here's the Spotify docs
FIRST, I have a function to configure my spotifyOAuth:
function spotifyOAuth2Client() {
// Spotify OAuth 2 setup
const credentials = {
client: {
id: functions.config().spotify.clientid,
secret: functions.config().spotify.clientsecret,
},
auth: {
tokenHost: 'https://accounts.spotify.com',
authorizePath: '/authorize'
},
};
return require('simple-oauth2').create(credentials);
}
I use that function in this Firebase function that is called using https://us-central1-<my project string>.cloudfunctions.net/redirect:
exports.redirect = functions.https.onRequest((req, res) => {
const oauth2 = spotifyOAuth2Client();
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,
});
const redirectUri = oauth2.authorizationCode.authorizeURL({
redirect_uri: OAUTH_REDIRECT_URI,
//scope: OAUTH_SCOPES,
state: state,
});
console.log('Redirecting to:', redirectUri);
res.redirect(redirectUri);
});
});
The code above returns a url string with the proper parameters, the following code block is where my code breaks, I have another cloud function that runs after being redirected from the res.redirect(redirectUri) above. And when I try to run the getToken() method, it appears to not return anything because I hit the catch block instead? This is where I observe the Auth Token Error Not Found.
const oauth2 = spotifyOAuth2Client();
try {
return cookieParser()(req, res, async () => {
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);
console.log(OAUTH_REDIRECT_URI);
// Get the access token object (the authorization code is given from the previous step).
const tokenConfig = {
code: req.query.code,
redirect_uri: 'http://localhost:8100/popup'
};
// Save the access token
try {
const result = await oauth2.authorizationCode.getToken(tokenConfig)
const accessToken = oauth2.accessToken.create(result);
console.log('inside try');
console.log(result);
console.log(accessToken);
} catch (error) {
console.log('Access Token Error', error.message);
}
I've double checked my spotify client/secret credentials in the config, what is going wrong with this OAuth2 flow?
Resolved my issue, I was not using the correct endpoints:
const credentials = {
client: {
id: functions.config().spotify.clientid,
secret: functions.config().spotify.clientsecret,
},
auth: {
tokenHost: 'https://accounts.spotify.com',
authorizePath: '/authorize',
tokenPath: '/api/token'
},
};