I'm using the following interceptors in a Vuejs v2 website to push a firebase token to my node backend. There in the backend, I detect/verify the token, pull some data using the uid from a database and then process any api calls.
Even though I am using the firebase onIdTokenChanged to automatically retrieve new ID tokens, sometimes, if the user is logged in, yet inactive for an hour, the token expires without refreshing. Now, this isn't a huge deal - I could check in the axios response interceptor and push them to a login page, but that seems annoying, if I can detect a 401 token expired, resend the axios call and have a refreshed token, the user won't even know it happened if they happen to interact with a component that requires data from an API call. So here is what I have:
main.js
Vue.prototype.$axios.interceptors.request.use(function (config) {
const token = store.getters.getSessionToken;
config.headers.Authorization = `Bearer ${token}`;
return config;
});
Vue.prototype.$axios.interceptors.response.use((response) => {
return response }, async function (error) {
let originalRequest = error.config
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
let user = auth.currentUser;
await store.dispatch("setUser", {user: user, refresh: true}).then(() => {
const token = store.getters.getSessionToken;
Vue.prototype.$axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
return Vue.prototype.$axios.request(originalRequest);
});
}
return Promise.reject(error); });
let app;
auth.onAuthStateChanged(async user => {
await store.dispatch("setUser", {user: user, refresh: false}).then(() => {
if (!app) {
app = new Vue({
router,
store,
vuetify,
render: h => h(App)
}).$mount('#app')
}
})
.catch(error => {
console.log(error);
});
});
vuex
setUser({dispatch, commit}, {user, refresh}) {
return new Promise((resolve) => {
if(user)
{
user.getIdToken(refresh).then(token => {
commit('SET_SESSION_TOKEN', token);
this._vm.$axios.get('/api/user/session').then((response) => {
if(response.status === 200) {
commit('SET_SESSION_USER', response.data);
resolve(response);
}
})
.catch(error => {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: 'Server unavailable: '+error
});
resolve();
});
})
.catch(error => {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: 'Unable to verify auth token.'+error
});
resolve();
});
}
else
{
console.log('running logout');
commit('SET_SESSION_USER', null);
commit('SET_SESSION_TOKEN', null);
resolve();
}
})
},
I am setting the token in vuex and then using it in the interceptors for all API calls. So the issue I am seeing with this code is, I'm making an API call with an expired token to the backend. This returns a 401 and the axios response interceptor picks it up and goes through the process of refreshing the firebase token. This then makes a new API call with the same config as the original to the backend with the updated token and returns it to the original API call (below).
This all seems to work, and I can see in dev tools/network, the response from the API call is sending back the correct data. However, it seems to be falling into the catch of the following api call/code. I get an "undefined" when trying to load the form field with response.data.server, for example. This page loads everything normally if I refresh the page (again, as it should with the normal token/loading process), so I know there aren't loading issues.
vue component (loads smtp settings into the page)
getSMTPSettings: async function() {
await this.$axios.get('/api/smtp')
.then((response) => {
this.form.server = response.data.server;
this.form.port = response.data.port;
this.form.authemail = response.data.authemail;
this.form.authpassword = response.data.authpassword;
this.form.sendemail = response.data.sendemail;
this.form.testemail = response.data.testemail;
this.form.protocol = response.data.protocol;
})
.catch(error => {
console.log(error);
});
},
I have been looking at this for a few days and I can't figure out why it won't load it. The data seems to be there. Is the timing of what I'm doing causing me issues? It doesn't appear to be a CORS problem, I am not getting any errors there.
Your main issue is mixing async / await with .then(). Your response interceptor isn't returning the next response because you've wrapped that part in then() without returning the outer promise.
Keep things simple with async / await everywhere.
Also, setting common headers defeats the point in using interceptors. You've already got a request interceptor, let it do its job
// wait for this to complete
await store.dispatch("setUser", { user, refresh: true })
// your token is now in the store and can be used by the request interceptor
// re-run the original request
return Vue.prototype.$axios.request(originalRequest)
Your store action also falls into the explicit promise construction antipattern and can be simplified
async setUser({ dispatch, commit }, { user, refresh }) {
if(user) {
try {
const token = await user.getIdToken(refresh);
commit('SET_SESSION_TOKEN', token);
try {
const { data } = await this._vm.$axios.get('/api/user/session');
commit('SET_SESSION_USER', data);
} catch (err) {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: `Server unavailable: ${err.response?.data ?? err.message}`
})
}
} catch (err) {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: `Unable to verify auth token. ${error}`
})
}
} else {
console.log('running logout');
commit('SET_SESSION_USER', null);
commit('SET_SESSION_TOKEN', null);
}
}
Related
I have a next.js app that has several API routes that I am hoping to protect from users who are not logged in. Using next-auth, I understand that I can add the following code to each API route to achieve this.
import { getSession } from 'next-auth/client'
export default async (req, res) => {
const session = await getSession({ req })
if (session) {
res.send({ content: 'This is protected content. You can access this content because you are signed in.' })
} else {
res.send({ error: 'You must be sign in to view the protected content on this page.' })
}
}
However, I was wondering if it is possible to use API middlewares, so I am not repeating the same code over and over again? I read through the Next.js API middlewares documentation (https://nextjs.org/docs/api-routes/api-middlewares) and did the following:
import Cors from 'cors';
import { getSession } from 'next-auth/react';
function initMiddleware(middleware) {
return (req, res) =>
new Promise((resolve, reject) => {
middleware(req, res, async (result) => {
const session = await getSession({ req });
if (!session) {
return reject(result);
}
return resolve(result);
});
});
}
const cors = initMiddleware(
Cors({
methods: ['GET', 'POST', 'OPTIONS'],
})
);
export default async function handler(req, res) {
await cors(req, res);
\* fetching from database *\
Although it works, the following error is returned when I tried to access the API route when unauthenticated, and it feels like I'm not doing it properly.
error - null
wait - compiling /_error (client and server)...
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:561:11)
at DevServer.renderError (/Users/alextung/Desktop/Projects/askit/node_modules/next/dist/server/next-server.js:1677:17)
at DevServer.run (/Users/alextung/Desktop/Projects/askit/node_modules/next/dist/server/dev/next-dev-server.js:452:35)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at async DevServer.handleRequest (/Users/alextung/Desktop/Projects/askit/node_modules/next/dist/server/next-server.js:325:20) {
code: 'ERR_HTTP_HEADERS_SENT'
}
error - Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
Would really appreciate some help on this given that this is my first time working with middlewares. Thank you!
I am using Firebase authentication for my application and using it to authenicate users to a back-end API using JWT tokens. On the API back-end I've configured the JWT-secret, which is the asymmetric keys pulled from this url:
https://www.googleapis.com/service_accounts/v1/jwk/securetoken#system.gserviceaccount.com
This is all working fine. I recently needed to create a cloud function, which needs to call the API back-end as well. To do this, I'm using the functionality to create a Custom Token found here:
https://firebase.google.com/docs/auth/admin/create-custom-tokens
This creates my token with correct custom claims
let additionalClaims = {
'x-hasura-default-role': 'admin',
'x-hasura-allowed-roles': ['user', 'admin']
}
admin.auth().createCustomToken(userId,additionalClaims).then(function (customToken) {
console.log(customToken);
response.end(JSON.stringify({
token: customToken
}))
})
.catch(function (error) {
console.log('Error creating custom token:', error);
});
however, when I try to use it against the back-end API, I get the "JWTInvalidSignature" error. In my cloud function, I specify the service account that is in my firebase project, but it doesn't seem to help. When I view the two tokens decoded, they definitely appear coming from different services.
CustomToken
{
"aud":
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
"iat": 1573164629,
"exp": 1573168229,
"iss": "firebase-adminsdk-r2942#postgrest-b4c8c.iam.gserviceaccount.com",
"sub": "firebase-adminsdk-r2942#postgrest-b4c8c.iam.gserviceaccount.com",
"uid": "mikeuserid",
"claims": {
"x-hasura-default-role": "admin",
"x-hasura-allowed-roles": [
"user",
"admin"
]
}
}
TOKEN from FireBase Auth
{
"role": "webuser",
"schema": "customer1",
"userid": "15",
"claims": {
"x-hasura-default-role": "user",
"x-hasura-allowed-roles": [
"user",
"admin"
],
"x-hasura-user-id": "OS2T2rdkM5UlhfWLHEjNExZ71lq1",
"x-hasura-dbuserid": "15"
},
"iss": "https://securetoken.google.com/postgrest-b4c8c",
"aud": "postgrest-b4c8c",
"auth_time": 1573155319,
"user_id": "OS2T2rdkM5UlhfWLHEjNExZ71lq1",
"sub": "OS2T2rdkM5UlhfWLHEjNExZ71lq1",
"iat": 1573164629,
"exp": 1573168229,
"email": "johnny1#gmail.com",
"email_verified": false,
"firebase": {
"identities": {
"email": [
"johnny1#gmail.com"
]
},
"sign_in_provider": "password"
}
}
How can I get this customToken to work with the existing JWT secret keys I have configured??
As documented in in Firebase Authentication: Users in Firebase Projects: Auth tokens, the tokens from Firebase Auth and the Admin SDK Custom Tokens are not the same, incompatible with each other and are verified differently.
Edited response after clarification:
As you are trying to identify the cloud functions instance as an authorative caller of your third-party API, you may use two approaches.
In both of the below methods, you would call your API using postToApi('/saveUserData', { ... }); in each example. You could probably also combine/support both server-side approaches.
Method 1: Use a public-private key pair
For this version, we use a JSON Web Token to certify that the call is coming from a Cloud Functions instance. In this form of the code, the 'private.key' file is deployed along with your function and it's public key kept on your third-party server. If you are calling your API very frequently, consider caching the 'private.key' file in memory rather than reading it each time.
If you ever wish to invalidate this key, you will have to redeploy all your functions that make use of it. Alternatively, you may modify the fileRead() call and store it in Firebase Storage (secure it - readable by none, writable by backend-admin). Which will allow you to refresh the private key periodically by simply replacing the file.
Pros: Only one remote request
Cons: Updating keys could be tricky
const jwt = require('jsonwebtoken');
const rp = require('request-promise-native');
const functionsAdminId = 'cloud-functions-admin';
function getFunctionsAuthToken(jwtOptions) {
jwtOptions = jwtOptions || {};
return new Promise((resolve, reject) => {
// 'private.key' is deployed with function
fs.readFile('private.key', 'utf8', (err, keyData) => {
if (err) { return reject({src: 'fs', err: err}); }
jwt.sign('cloud-functions-admin', keyData, jwtOptions, (err, token) => {
if (err) { return reject({src: 'jwt', err: err}); }
resolve(token);
});
});
});
}
Example Usage:
function postToApi(endpoint, body) {
return getFunctionsAuthToken()
.then((token) => {
return rp({
uri: `https://your-domain.here${endpoint}`,
method: 'POST',
headers: {
Authorization: 'Bearer ' + token
},
body: body,
json: true
});
});
}
If you are using express on your server, you can make use of express-jwt to deserialize the token. If configured correctly, req.user will be 'cloud-functions-admin' for requests from your Cloud Functions.
const jwt = require('express-jwt');
app.use(jwt({secret: publicKey});
Method 2: Add a cloud-functions-only user
An alternative is to avoid the public-private key by using Firebase Auth. This will have the tradeoff of potentally being slower.
Pros: No key management needed, easy to verify user on server
Cons: Slowed down by Firebase Auth calls (1-2)
const admin = require('firebase-admin');
const rp = require('request-promise-native');
const firebase = require('firebase');
const functionsAdminId = 'cloud-functions-admin';
function getFunctionsAuthToken() {
const fbAuth = firebase.auth();
if (fbAuth.currentUser && fbAuth.currentUser.uid == uid) {
// shortcut
return fbAuth.currentUser.getIdToken(true)
.catch((err) => {src: 'fb-token', err: err});
}
return admin.auth().createCustomToken(functionsAdminId)
.then(function(customToken) {
return fbAuth.signInWithCustomToken(token)
.then(() => {
return fbAuth.currentUser.getIdToken(false)
.catch((err) => {src: 'fb-token', err: err});
})
.catch((err) => {src: 'fb-login', err: err});
})
.catch((err) => {src: 'admin-newtoken', err: err});
}
Example Usage:
function postToApi(endpoint, body) {
return getFunctionsAuthToken()
.then((token) => {
return rp({
uri: `https://your-domain.here${endpoint}`,
method: 'POST',
headers: {
Authorization: 'Bearer ' + token
},
body: body,
json: true
});
});
}
On your server, you would use the following check:
// idToken comes from the received message
admin.auth().verifyIdToken(idToken)
.then(function(decodedToken) {
if (decodedToken.uid != 'cloud-functions-admin') {
throw 'not authorized';
}
}).catch(function(error) {
// Handle error
});
Or if using express, you could attach it to a middleware.
app.use(function handleFirebaseTokens(req, res, next) {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
var token = req.headers.authorization.split(' ')[1];
admin.auth().verifyIdToken(idToken)
.then((decodedToken) => {
req.user = decodedToken;
next();
}, (err) => {
//ignore bad tokens?
next();
});
} else {
next();
}
});
// later on: req.user.uid === 'cloud-functions-admin'
Original response:
If your client uses Firebase Authentication from an SDK to log in and your server uses the Admin SDK, you can use the client's ID token on the cloud function to speak to your server to verify a user by essentially "passing the parcel".
Client side
firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function(idToken) {
// Send token to your cloud function
// ...
}).catch(function(error) {
// Handle error
});
Cloud Function
// idToken comes from the client app
admin.auth().verifyIdToken(idToken) // optional (best-practice to 'fail-fast')
.then(function(decodedToken) {
// do something before talking to your third-party API
// e.g. get data from database/secret keys/etc.
// Send original idToken to your third-party API with new request data
}).catch(function(error) {
// Handle error
});
Third-party API
// idToken comes from the client app
admin.auth().verifyIdToken(idToken)
.then(function(decodedToken) {
// do something with verified user
}).catch(function(error) {
// Handle error
});
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'
},
};
I'm currently using redux / redux-thunk to fetch a user using api-sauce like so
let authToken = await AsyncStorage.getItem('#TSQ:auth_token')
if (authToken) {
store.dispatch(fetchUser(authToken))
console.log('show login screen')
// dont worry, if the token is invalid, just send us to onboarding (api determines this)
loggedInView()
} else {
Onboarding ()
}
....
export const fetchUser = authToken => async dispatch => {
console.log('dispatching auth token')
console.log('here goes request')
let res = await api.get(`/auth/${authToken}`);
if (res.ok) {
console.log('have the user')
dispatch(
setUser(res.data)
)
} else {
dispatch({
type: 'SET_USER_DEFAULT'
})
}
}
When this code is ran, the user is still loading and the console.logs are not in order
`dispatching auth token`
`here goes request`
`show login screen`
Why is this happening?
This is because the actual call to store.dispatch(fetchUser(authToken)) is synchronous - the dispatch() method is not asynchronous, so the logging "show login screen" will occur immediately after execution of the fetchUser() method.
If you want loggedInView() to be executed after a response is returned from your network request (ie the call to the async method api.get()), then you could consider refactoring your code in the following way:
if (authToken) {
store.dispatch(fetchUser(authToken))
// Remove navigation from here
} else {
Onboarding ()
}
And then:
export const fetchUser = authToken => async dispatch => {
console.log('dispatching auth token')
console.log('here goes request')
let res = await api.get(`/auth/${authToken}`);
if (res.ok) {
console.log('have the user')
// Occurs after network request is complete
console.log('show login screen')
// Add navigation here to go to logged in view now that request is complete
loggedInView()
dispatch(
setUser(res.data)
)
} else {
dispatch({
type: 'SET_USER_DEFAULT'
})
}
Hope this helps!
I'm authenticating users iva JWT Auth on WP Rest API from a React Native app, so, something like that:
export function userAuth(username, password) {
return dispatch => {
dispatch(requestLogin());
const appAuth = new FormData();
appAuth.append('username', username);
appAuth.append('password', password);
return fetch(wp.jwtEndpoint, {
method: 'POST',
body: appAuth
})
.then(function(res) {
return res.json();
})
.then(function(body) {
if(body.token){
getUserDataFromUsername(body.user_nicename, body.token, dispatch);
return dispatch(userSuccessLogin(body));
}
else {
return dispatch(userFailLogin(body));
}
});
}
}
The response for this request is:
{ token: 'eyJ0eXAiOiJKXXXQ',
user_email: 'supertest#gmail.com',
user_nicename: 'super-test-avatar',
user_display_name: 'TEST TEST' }
My issue is: as I can't get user ID from there, how can I retrieve user data form a request like https://www.wp.com/wp-json/wp/v2/users/${userId}?
I tried using https://github.com/dest81/wp-api-get-user-by-username, which would allow me to do that based on username, but its endpoints goes to 404, so I think it's outdated.