Authenticating NodeJS with Axios - javascript

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.

Related

How do I add a JS state variable of 1 React component for use in other components?

I have a Home.js component that signs the user up to the API and logs in and then gets the token received from the response authorization header and saves it in the state 'token' variable.
This token will be used in all other components to access the API when requests are made, so what is the best way of using this value for all other components?
Home.js:
const SIGNUP_URL = 'http://localhost:8080/users/signup';
const LOGIN_URL = 'http://localhost:8080/login';
class Home extends Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated:false,
token: ''
};
}
componentDidMount() {
const payload = {
"username": "hikaru",
"password": "JohnSmith72-"
};
fetch(SIGNUP_URL, {
method: 'POST',
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
})
.then(response => response.json())
.then((data) => {
console.log(data);
});
fetch(LOGIN_URL, {
method: 'POST',
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
})
.then(response =>
this.setState({token: response.headers.get("Authorization"), isAuthenticated:true})
)
}
For example the userList component which will fetch the user data from the API, but requires the API token stored in the Home component's token state variable to send the request successfully via the authorization header.
Thanks for any help
You can create a custom function called authenticated_request for example. This function could fetch your token from the CookieStorage in case of web or async storage in case of react-native or even if you have it in some state management library. Doesn't matter. Use this function instead of the fetch function and call fetch inside it. Think of it as a higher order function for your network requests.
const authenticated_request(url, config) {
fetch(url, {
...config,
headers: {
...config.headers,
Authorization: getToken()
}
});
}
You can also leverage the usage of something like axios and use request interceptors to intercept requests and responses. Injecting your token as needed.
You should be using AuthContext and localStorage to do this, save the token in the state or localStorage and make a config file which uses the same token when calling an api i have done it in axios. Axios has a concept of interceptors which allows us to attach token to our api calls, Im saving the token in the localStorage after a successfull login and then using the same token from localStorage to add to every call which needs a token, if the api doesnt need a token (some apis can be public) i can use axios directly, check out the below code:
import axios from 'axios';
let apiUrl = '';
let imageUrl = '';
if(process.env.NODE_ENV === 'production'){
apiUrl = `${process.env.REACT_APP_LIVE_URL_basePath}/web/v1/`;
}else{
apiUrl = `http://127.0.0.1:8000/web/v1/`;
}
const config = {
baseURL: apiUrl,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
"Access-Control-Allow-Origin": "https://www.*******.com",
'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE',
},
};
const authAxios = axios.create(config);
authAxios.interceptors.request.use(async function(config) {
config.headers.Authorization = localStorage.getItem('access_token') ?
`Bearer ${localStorage.getItem('access_token')}` :
``;
return config;
});
export { apiUrl, axios, authAxios };
now on making api call u can do something like below:
import { apiUrl, authAxios } from '../config/config'
export async function saveAssignment(data) {
try {
const res = await authAxios.post(apiUrl + 'assignment/save-assignment', data)
return res.data;
}
catch(e){
}
}
here pay attention im not using axios to make api call but using authAxios to make calls(which is exported from the config file) which will have token in the header.
(You can also use a third party library like Redux but the concept remains the same)
You need a centralized state that's what State Management libraries are for. You can use third-party libraries such as Redux, or simply use React's own context. You can search on google for state management in React and you'll find a lot of helpful recourses
You can place the token into a cookie if your app is SSR. To do that, you have to create the following functions:
export const eraseCookie = (name) => {
document.cookie = `${name}=; Max-Age=-99999999;`;
};
export const getCookie = (name) => {
const pairs = document.cookie.split(';');
const pair = pairs.find((cookie) => cookie.split('=')[0].trim() === name);
if (!pair) return '';
return pair.split('=')[1];
};
export const setCookie = (name, value, domain) => {
if (domain) {
document.cookie = `${name}=${value};path=/`;
} else {
document.cookie = `${name}=${value}`;
}
};
You can also place your token into the local storage:
Set into local storage via built-in function:
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c');
Get the token via built-in function:
const token = localStorage.getItem('token');

Authenticated requests after sign in with React Query and NextAuth

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.

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.

Categories

Resources