Authenticated requests after sign in with React Query and NextAuth - javascript

I'm having troubled sending an authenticated request to my API immediately after signing in to my Nextjs app using NextAuth. The request that is sent after signing in returns data for and unauthenticated user.
I believe the issue is that React Query is using a previous version of the query function with an undefined jwt (which means its unauthenticated). It makes sense because the query key is not changing so React Query does not think it's a new query, but, I was under the impression that signing in would cause loading to be set to true temporarily then back to false, which would cause React Query to send a fresh request.
I've tried invalidating all the queries in the app using queryClient, but that did not work. I've also used React Query Devtools to invalidate this specific query after signing in but it still returns the unauthenticated request. Only after refreshing the page does it actually send the authenticated request.
// useGetHome.js
const useGetHome = () => {
const [session, loading] = useSession();
console.log(`session?.jwt: ${session?.jwt}`);
return useQuery(
'home',
() => fetcher(`/home`, session?.jwt),
{
enabled: !loading,
},
);
}
// fetcher
const fetcher = (url, token) => {
console.log(`token: ${token}`);
let opts = {};
if (token) {
opts = {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}${url}`, opts);
if (!res.ok) {
const error = await res.json();
throw new Error(error.message);
}
return res.json();
}
// Home.js
const Home = () => {
const { data: home_data, isLoading, error } = useGetHome();
...
return(
...
)
}
Attached is the console immediately after signing in. You can see the the session object contains the jwt after signing in, but in the fetcher function it is undefined.
console after signing in
Any help here is appreciated. Is there a better way to handle authenticated requests using React Query and NextAuth? Thank you!

I have tried a similar situation here and struggled the same thing but the enabled property worked fine for me and it is good to go right now.
https://github.com/maxtsh/music
Just check my repo to see how it works, that might help.

Related

Axios Interceptor doesn't set Token to the Authorization Header

In my React Native app, I use Axios Interceptors to set an Auth header Token on every request. Everything works correctly but when I log out and clear the stored token (By PURGING the Redux store) and log in again, the new token doesn't get set on the Axios Authorization Header (Gets empty).
This is how I'm setting the Auth header on Axios with Interceptors:
const App = () => {
const token = useSelector(selectToken); // Token state
const isLoggedOut = useSelector(selectIsLoggedOutState); // User logout state
// Show logout message and purge the store
useEffect(() => {
if (isLoggedOut) {
Toast.show({
type: 'success',
text1: logoutMessage,
});
persistor.purge();
}
}, [isLoggedOut]);
// Intercept on request
api.interceptors.request.use(
config => {
// Set authorization header
if (token) {
config.headers.common['Authorization'] = 'Bearer ' + token;
}
return config;
},
err => {
return Promise.reject(err);
},
);
// REST OF THE CODE
}
The most weird thing is that the header Token gets changed even if token is false in the if block, as if it doesn't even get checked.
I'd really appreciate any answer that might help me fix this weird problem.

Different headers used in Axios patch

I spent an hour looking in the Chrome console and I cannot see where this bug comes from.
I am finishing an update of OAuth implementation in my Vue app.
The story begins when socialLink.js finds out that a new user must be created. Vue component Vue-authentication depends on the presence of access_token in a response so I return some dummy text:
return api.sendResponse(res, { email, name, socialId, access_token: 'abcd' });
The library stores this value in localStorage:
After a redirect, the SignUp.vue is rendered and I complete the form. The first communication with the server is a Vuex call to create a new user:
response = await this.$store.dispatch('CREATE_USER_PROFILE', payload);
Which returns a real short lived JWT token:
const token = auth.createToken(userId, nickname, new Date(), null, false, '1m');
return api.sendCreated(res, api.createResponse(token));
Which I store in the Vue page afterwards:
const { data } = response;
const token = data.data;
if (token === undefined) {
this.error = this.$t('sign-up.something-went-wrong');
return false;
}
I checked that the token contains what the server returned:
Request URL: https://beta.mezinamiridici.cz/api/v1/users
Request Method: POST
Status Code: 201 Created
{"success":true,"data":"eyJhbGciOiJIUzI1NiIs...Tl8JFw2HZ3VMXJk"}
Then I call another Vuex method and pass the current JWT token:
await this.$store.dispatch('UPDATE_USER_PROFILE', {
I checked in the Vuex devtools that there really is the correct JWT token. I then pass it further to api.js.
Here I create an Axios configuration holding an Authorization header:
function getAuthHeader(context, jwt = undefined, upload) {
const config = { headers: { } };
if (jwt || (context && context.rootState.users.userToken)) {
config.headers.Authorization = `bearer ${jwt || context.rootState.users.userToken}`;
}
Again, I checked that the correct JWT token is used there.
Finally, I pass all data to Axios:
function patch(endpoint, url, body, context, jwt) {
const headers = getAuthHeader(context, jwt);
console.log(headers);
if (endpoint === 'BFF') {
return axios.patch(`${VUE_APP_BFF_ENDPOINT}${url}`, body, headers);
} else {
return axios.patch(`${VUE_APP_API_ENDPOINT}${url}`, body, headers);
}
}
Which I log and can confirm the correct JWT is still there:
bearer eyJhbGciOiJIUzI1N....8JFw2HZ3VMXJk
There is nothing that could change the header now to abcd, but, the 'Network' tab shows it:
And the server fails with a parse error.
Has anybody got an idea why Axios uses the Authorization header with a different value than I pass it?
Ok, mystery solved. vue-authenticate is the reason, because, it creates Axios interceptors and handles the Authorization header itself.
vue-authenticate.common.js:
var defaultOptions = {
bindRequestInterceptor: function ($auth) {
var tokenHeader = $auth.options.tokenHeader;
$auth.$http.interceptors.request.use(function (config) {
if ($auth.isAuthenticated()) {
config.headers[tokenHeader] = [
$auth.options.tokenType, $auth.getToken()
].join(' ');
} else {
delete config.headers[tokenHeader];
}
return config
});
},
My code is more complex and it supports internal accounts with email/password so this code is breaking mine. The interceptor must be present and be a function, so the solution was:
Vue.use(VueAuthenticate, {
tokenName: 'jwt',
baseUrl: process.env.VUE_APP_API_ENDPOINT,
storageType: 'localStorage',
bindRequestInterceptor() {},
bindResponseInterceptor() {},
providers: {
facebook: {
clientId: process.env.VUE_APP_FACEBOOK_CLIENT_ID,
redirectUri: process.env.VUE_APP_FACEBOOK_REDIRECT_URI,
},

Firebase functions setting custom claims

I have a problem with the function(I suspect) in which I add custom claims. Here is the code:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.addAdminRole = functions.https.onCall((data)=> {
//get user and add custom claim (admin)
return admin.auth().getUserByEmail(data.email).then(user => {
return admin.auth().setCustomUserClaims(user.uid, {
admin: true
});
}).then( () => {
return {
message: `Success! ${data.email} has been made an admin`,
}
})
})
I call the function using the following code(I use Redux and React):
let addAdminRole = window.firebaseFunctions.httpsCallable('addAdminRole');
addAdminRole({email:employee.email}).then(res => {
console.log(res)
})
I get the expected message({message: Success! ${data.email} has been made an admin}), but the claim isn't added.
I make a separate Firebase Auth REST API via axios for authentication, but the claim 'admin' isn't there.
I have a Spark(free) billing plan and when checking the logs from firebase functions I see 'Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions' when addAdminRole is executed.
From what I read this is a message that you always get on the free plan and there shouldn't be a problem when accessing internal(google) info.
Here is the code for the axios Request:
axios({
method:'post',
url:urlAuth,
data:{
email:employee.email,
password:employee.password,
returnSecureToken: true
}
}).then(res => {
delete employee.password;
console.log(res)
const expirationDate = new Date().getTime() + res.data.expiresIn * 1000;
localStorage.setItem('token',res.data.idToken);
localStorage.setItem('expirationDate',expirationDate);
localStorage.setItem('userId', res.data.localId);
localStorage.setItem('admin', res.data.admin)
dispatch(checkAuthTimeout(res.data.expiresIn));
if(logIn){
dispatch(endAuth(res.data.idToken,res.data.localId,res.data.admin));
}else{
employee.userId = res.data.localId;
dispatch(addEmplRegister(employee,res.data.idToken,admin));
}
}).catch(err => {
dispatch(errorAuth(err.message))
})
FOUND OUT THE ISSUE, the information about claims isn't transmitted when using REST API authentication
Setting a custom claim doesn't take effect immediately for users with existing JWT tokens. Those users will have to either:
Sign out and back in again,
Force refresh their token, or
Wait up to one hour for that token to automatically refresh by the Fireabse Auth SDK.
On then will their new token show the custom claim.

Asynchronously authenticate before request

So I have an API and I am trying to authenticate by hitting an endpoint with credentials (this part I've gotten working) and then save the received token and use it in all subsequent requests.
My problem is that the authenticate() method is asynchronous, but all other request methods like get() need the token from the authenticate() method. So I can't just export my get() method because the export is synchronous (as I've read) and it will be exported before authentication happens. I could authenticate for every request but that seems wasteful and inefficient.
I am not sure what to do here, I'm using axios, what's the proper way of doing this?
Edit
I'll be a bit more specific here. I have created an axios instance:
var instance = axios.create({
baseURL: `http://${config.server}:${config.port}`,
timeout: 1000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
I want to get the authentication token, and include it in the instance header:
async function authenticate(instance) {
const result = await instance.post(
'/session',
{
'username': config.username,
'password': config.password
}
)
instance['X-Token'] = result.data.token
}
Now I want to export that instance to be used in other files
You can use async/await. This is semi-pseudocode:
async function doStuff() {
const result = await axios.authenticate();
const token = // extract token from whatever format of result is
const data = await axios.get(/* supply token to get */);
}
Alternatively, you can just use then:
function doStuff(token) {
const token = // extract token from whatever format of result is
const data = await axios.get(/* supply token to get */);
}
axios.authenticate().then(result => {
const token = // extract token from whatever format of result is
doStuff(token);
}
With Axios you have the ability to set default values for all requests.
So for just a single axios instance you can do...
async function authenticate(instance) {
const result = await instance.post(
'/session',
{
'username': config.username,
'password': config.password
}
)
instance.defaults.headers.common['X-Token'] = result.data.token;
}
Alternatively, (which it sounds like you want to do) you can add it for the default Axios export. Then all requests will automatically have the header.
async function authenticate(endpoint, username, password) {
const res = await axios.post(`${endpoint}/session`, { username, password });
axios.defaults.headers.common['X-Token'] = result.data.token;
}
Then you don't have to worry about passing around an instance between all parts of your app and can just use import * as axios from 'axios' and have the header set.
Axios also provides and extremely helpful function called interceptors which you can use to inspect a request prior to making it. You can use to check to make sure that the request has the auth header and if it doesn't you can perform that logic. I came up with this and it seems to work well!
axios.interceptors.request.use(async (config) => {
// request intercepted, check (1) the header is missing and (2) that the intercepted request isn't authorizing
if (!config.headers.common['X-Token'] && config.authorizing !== true) {
const { endpoint, username, password } = appConfig;
// make a request to get your token AND pass our custom config
const result = await axios.post(`${endpoint}/session`, { username, password }, { authorizing: true });
// update axios to include the header for future requests
axios.defaults.headers.common['X-Token'] = result.data.token;
}
return config;
});
Two things that you'll want to note -- not only do I check for the existence of your X-token header I also check for a new authorization value in the config. You want to check for that config value, because we are going to use it as a flag to let the interceptor know if it should skip a request. If you don't do this, the authorization request will trigger another authorization request and infinite loop.

Authenticating NodeJS with Axios

I have a closed CouchDB application (so it requires authentication).
I'm connecting to it from a NodeJS backend which has to get admin access and do its thing. So I want to intercept requests that are not authenticated or expired.
To do that, I've axios.create()d a client. That client has an interceptor which uses the original axios object to authenticate, then sets the cookie from the response to the client's default headers. The code looks a bit like this:
import axios from 'axios';
const baseURL = 'http://127.0.0.1:5984/';
const name = 'admin';
const password = 'mypass';
const cookieTimeout = (10 * 60 * 1000);
let cookieCreated;
const client = axios.create({
baseURL,
headers: {
Accept: 'application/json',
post: { 'Content-Type': 'application/x-www-form-urlencoded' },
},
});
const cookieInterceptor = (config) => {
const now = Date.now();
if (!cookieCreated || cookieCreated + cookieTimeout <= now) {
// Using the default 'axios' object because if we use 'client',
// it will loop into intercepting its own request again.
return axios.post(`${baseURL}/_session`, { name, password })
.then((response) => {
// Cache a new cookie and return it
cookieCreated = now;
client.defaults.headers.common.Cookie = response.headers['set-cookie'].join(';');
return config;
})
.catch(error => console.log(error));
}
return Promise.resolve(config);
};
client.interceptors.request.use(cookieInterceptor);
export default client;
This is probably a sub-optimal way of doing it, but the actual issue is that calling client.get() doesn't end up being an authenticated request when I'm calling it for the first time after launching the app. It's good afterwards though so it's probably that the authentication process goes somewhere in the background.
Question is, how do I make my interceptor authenticate my client before making the actual request? Is an interceptor the right way of doing it? Also, if there's a less horrible way to go about this, I'm fully open to suggestions.

Categories

Resources