Building a login form using Reactjs, redux, axios and redux-thunk. I have two tokens - one named access token and refresh token.
When the user is authenticated, store the access token which should last for 12 hours. The refresh token is also provided and will last 30 days.
Once the access token has expired need to check the timestamp (date) if access token is expired.
How can I update the access token once expired? Token data looks like this so I have a timestamp to check against:
{
"access_token": "toolongtoinclude",
"token_type": "bearer",
"refresh_token": "toolongtoinclude",
"expires_in": 43199,
"scope": "read write",
"roles": [
"USER"
],
"profile_id": "b4d1e37d-7d05-4eb3-98de-0580d3074a0d",
"jti": "e975db65-e3b7-4034-a6e4-9a3023c3d175"
}
Here are my actions to save, get and update tokens from storage. I'm just unsure on how to refresh the token.
export function submitLoginUser(values, dispatch) {
dispatch({type: constants.LOADING_PAGE, payload: { common: true }})
return axios.post(Config.API_URL + '/oauth/token', {
username: values.email,
password: values.password,
scope: Config.WEBSERVICES_SCOPE,
grant_type: Config.WEBSERVICES_GRANT_TYPE_PASSWORD
},
{
transformRequest: function (data) {
var str = [];
for (var p in data) {
str.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
}
return str.join('&');
},
headers: {
'Authorization': 'Basic ' + window.btoa(Config.WEBSERVICES_AUTH),
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then(response => {
const {access_token, refresh_token} = response.data;
dispatch({type: constants.LOADING_PAGE, payload: { common: false }})
dispatch({
type: constants.LOGIN_SUCCESS,
payload: {
access_token: access_token,
refresh_token: refresh_token
}
});
saveTokens(response)
browserHistory.push('/questions');
refreshToken(response);
})
.catch(error => {
dispatch({type: constants.LOADING_PAGE, payload: { common: false }})
//401 Error catch
if(error.response.status === 401) {
throw new SubmissionError({username: 'User is not authenticated', _error: message.LOGIN.loginUnAuth})
}
//Submission Error
throw new SubmissionError({username: 'User does not exist', _error: message.LOGIN.loginFailed})
})
}
/**
* Save tokens in local storage and automatically add token within request
* #param params
*/
export function saveTokens(params) {
const {access_token, refresh_token} = params.data;
localStorage.setItem('access_token', access_token);
if (refresh_token) {
localStorage.setItem('refresh_token', refresh_token);
}
//todo fix this later
getinstanceAxios().defaults.headers.common['Authorization'] = `bearer ${access_token}`
}
/**
*
*/
export function getTokens() {
let accessToken = localStorage.getItem('access_token');
return accessToken
}
/**
* update the get requests
*/
export function updateTokenFromStorage() {
const tokenLocalStorage = getTokens();
getinstanceAxios().defaults.headers.common['Authorization'] = `bearer ${tokenLocalStorage}`;
}
/**
* Refresh user access token
*/
export function refreshToken(dispatch) {
//check timestamp
//check access expired - 401
//request new token, pass refresh token
//store both new access and refresh tokens
}
check this out:
https://github.com/mzabriskie/axios#interceptors
I think this can help you. You intercept your request and make your validations.
EDIT
Here is the code I've tried to use in my store to test, not getting any log back
import { createStore, applyMiddleware, compose } from 'redux'
import { devTools, persistState } from 'redux-devtools'
import axios from 'axios'
import Middleware from '../middleware'
import Reducer from '../reducers/reducer'
import DevTools from '../containers/DevTools'
let finalCreateStore
if (__DEVELOPMENT__ && __DEVTOOLS__) {
finalCreateStore = compose(
applyMiddleware.apply(this, Middleware),
// Provides support for DevTools:
DevTools.instrument(),
// Optional. Lets you write ?debug_session=<key> in address bar to persist debug sessions
persistState(getDebugSessionKey())
)(createStore)
} else {
finalCreateStore = compose(
applyMiddleware.apply(this, Middleware)
)(createStore)
}
function getDebugSessionKey() {
// You can write custom logic here!
// By default we try to read the key from ?debug_session=<key> in the address bar
const matches = window.location.href.match(/[?&]debug_session=([^&]+)\b/)
return (matches && matches.length > 0)? matches[1] : null
}
axios.interceptors.response.use((err) => {
if (err.status === 401) {
console.log('ACCESS TOKEN EXPIRED!');
}
});
export const store = finalCreateStore(Reducer)
Related
I'm trying to implement Server Side Authentication with Supabase and Sveltekit. I followed the Quickstart Guide and was able to do authentication client-side. (preventDefault on the submit event and do client-side POST request).
But when trying to do the same thing Server-Side , the auth cookie with the token is not created. Here's the logic:
// src/routes/login/+page.server.ts
import type { PostgrestResponse } from '#supabase/supabase-js';
import { supabaseClient } from '$lib/supabaseClient';
import type { Database } from '$lib/types/database.types';
import type { PageLoad } from './$types';
import type { PageServerLoad, Actions } from './$types';
import { redirect } from '#sveltejs/kit';
export const actions: Actions = {
'login-with-password': async ({ request }) => {
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
console.log(email, password);
const { data, error } = await supabaseClient.auth.signInWithPassword({ email, password });
console.log(data);
if (error) {
return {
status: 500,
body: {
error: error.message
}
};
}
throw redirect(302, '/');
return { success: true };
}
};
data seems to hold the correct response, with token and everything, but that's not persisted as a cookie.
https://stackblitz.com/~/github.com/gkatsanos/client
enter image description here
Here i have screen shot of my local storage. how can i fetch access token from there pass as headers in below action page. please provide any solution for this. how we can fetch token from local storage using react redux and display in action page.
import axios from 'axios';
export const upiAction = {
upi,
};
function upi(user) {
return (dispatch) => {
var data = {
upiId: user.upiId,
accountNumber: user.accountNumber,
};
axios
.post('http://localhost:9091/upiidcreation', data,
)
.then((res) => {
console.log("res", (res));
const { data } = res;
alert(JSON.stringify(data.responseDesc));
// window.location.pathname = "./homes";
if (data.responseCode === "00") {
window.location.pathname = "./home"
}
})
.catch(err => {
dispatch(setUserUpiError(err, true));
alert("Please Check With details");
});
};
}
export function setUserUpi(showError) {
return {
type: 'SET_UPI_SUCCESS',
showError: showError,
};
}
export function setUserUpiError(error, showError) {
return {
type: 'SET_UPI_ERROR',
error: error,
showError: showError,
};
}
if you just need to fetch the token and send it as header in the api request you can do this
let storageValue =JSON.parse(localStorage.getItem('currentUser')
storageValue object will have the whole thing that you've stored in localStorage .
axios.post('http://localhost:9091/upiidcreation', data, {
headers: {
token : storageValue?.data?.accessToken
}
})
You can get localStorage Object like this
let localStorageObject = JSON.parse(localStorage.getItem('currentUser'));
Then You can use it that object to get access token like this:
localStorageObject?.data?.accessToken
This is code in api/auth/[...nextAuth].js
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import User from "#/models/user";
import connectToDb from "#/config/dbConnection";
export default NextAuth({
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
async authorize(credentials) {
connectToDb();
const { email, password } = credentials;
// check if email and passwird entered
if (!email || !password) {
throw new Error("please enter email or password");
}
const user = await User.findOne({
email,
// becasue in pre.save I excluded returning password in api response
}).select("+password");
if (!user) {
throw new Error("Invalid email or password");
}
// check if password is correct
// I added comparePassword to userSchema methods in mongoose
const isPasswordMatched = await user.comparePassword(password);
if (!isPasswordMatched) {
throw new Error("please enter email or password");
}
// creating a promise
return Promise.resolve(user);
},
}),
],
callbacks: {
// jwt callback is only called when token is created
jwt: async ({ token, user }) => {
// user is obj that we have received from authorize Promise.resolve(user)
user && (token.user = user);
// not this token has user property
return Promise.resolve(token);
},
// user arg here is actully token that returned from jwt.
session: async ({ session, token }) => {
// session callback is called whenever a session for that particular user is checked
console.log("user in ...next auth api", token);
session.user = token.user;
// since I get error, I return Promise.resolve(session)
return Promise.resolve(session);
},
},
});
then I write a middleware to get the session:
const isAuthenticatedUser = asyncErrors(async (req, res, next) => {
// when user logged in we create the session of user
// getSession gets auth-token from req.headers
const session = await getSession({ req });
// THIS RETURNS NULL
console.log("session", session);
if (!session) {
return next(new ErrorHandler("Login first to access this resource", 401));
}
req.user = session.user;
next();
});
export { isAuthenticatedUser };
I write api/me to check the user
import nc from "next-connect";
import connectToDb from "#/config/dbConnection";
import { currentUserProfile } from "#/controllers/authController";
import { isAuthenticatedUser } from "#/middlewares/auth";
import onError from "#/middlewares/errors";
const handler = nc({ onError });
connectToDb();
handler.use(isAuthenticatedUser).get(currentUserProfile);
export default handler;
When I visit http://localhost:3000/api/me, since session is logged as null, I get not authneticated, even though, in chrome debugging tools' Aplication tab next-auth credentials are present
Since https://next-auth.js.org/getting-started/upgrade-v4
Version 4 makes using the SessionProvider mandatory.
this is _app.js component
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
// `session` comes from `getServerSideProps` or `getInitialProps`.
// Avoids flickering/session loading on first load.
<SessionProvider session={session} refetchInterval={5 * 60}>
<Component {...pageProps} />
</SessionProvider>
)
}
This problem is happening due to not specifying e.preventDefault() on login button.
The working code should look like this :-
async function login(e) {
e.preventDefault(); //Add this to your code.
const getLoginStatus = await signIn("credentials", {
redirect: false,
username,
password,
})};
The question sounds vague so allow me to explain. I am wondering, what is the correct/best way to pass get a token from local storage and pass it into my axios request.
This is what I am doing now, and I am sure this is not correct so I want to fix it but am unsure how.
I have a component called TicketsComponent that requires authorization. Therefore, in componentDidMount(), I validate the token, and if its invalid then send the user back to login, otherwise, load the tickets. It looks like this:
componentDidMount() {
this._isMounted = true;
// validate token
const token = localStorage.getItem("token");
AuthService.validateToken()
.then((res) => {
if (res == undefined || res == null || !token) {
this.props.history.push("/login");
}
})
.then(() => {
TicketService.getTickets().then((res) => {
if (this._isMounted) {
this.setState({ tickets: res.data });
}
});
});
}
Both AuthService.validateToken() and TicketService.getTickets() require the JWT in the header. Here are those two functions:
validateToken() {
return axios
.get(API_BASE_URL + "authenticate", {
headers: {
token: this.getTokenFromLocalStorage(),
},
})
.then("did it")
.catch((error) => {
console.log("failed to validate token" + error);
return null;
});
}
getTickets() {
console.log("getting tickets!");
console.log("Environment variable: " + process.env.NODE_ENV);
return axios
.get(API_BASE_URL + "tickets", {
headers: { Authorization: `Bearer ${this.getTokenFromLocalStorage()}` },
})
.then("yessssss")
.catch((error) => {
console.log("failed to get tickets" + error);
});
}
The problem is that both AuthService and TicketService share the same function called getTokenFromLocalStorage(). That looks like this:
getTokenFromLocalStorage() {
const token = localStorage.getItem("token");
console.log("the token is -> " + token);
if (token === null) {
return undefined;
}
return token;
}
catch(err) {
return undefined;
}
So obviously this is not ideal. I have the same function in two services just to get the token from the header. What is the recommended way of doing this?
EDIT: I hope this kind of question is allowed. Even though the code is not actually broken per se, I still think this is useful to beginners like me to implement best practice.
You can create a shared axios instance like so:
const API_BASE_URL = 'https://example.com/api/'
const instance = axios.create({
baseURL: API_BASE_URL,
headers: { Authorization: `Bearer ${this.getTokenFromLocalStorage()}` },
});
Then you'd just import "instance" into the components and call:
import {instance} from '../wherever' // decide if you want to import default or not
// make sure to either include or exclude the / in the first parameter passed into the request method (e.g. '/authenticate' or 'authenticate') below based on whether you provided a / in the API_BASE_URL
instance.post('authenticate', {
// any additional config relevant to the request, e.g:
data: {
username: 'my user!',
password: 'super_secret_password'
}
})
I am trying to build a react app using relay following instructions from the react-relay step by step guide. In the guide the auth token is stored in the env file, and I am trying to retrieve my token from memory which is created when the user logs in and is passed down to all components using the context API. I am not storing it in local storage and have a refresh token to automatically refresh the JWT.
From the tutorial, the relay environment class is not a React component because of which I cannot access the context object.
Is there a way to pass the token from my context to the relay environment class or any middleware implementation to accomplish this.
Any help is greatly appreciated.
import { useContext } from 'react';
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
import axios from "axios";
import { AppConstants } from './app-constants';
import { AuthContext, AuthSteps } from "./context/auth-context";
import { useCookie } from './hooks/useCookie';
interface IGraphQLResponse {
data: any;
errors: any;
}
async function fetchRelay(params: { text: any; name: any; }, variables: any, _cacheConfig: any) {
const authContext = useContext(AuthContext); //Error - cannot access context
const { getCookie } = useCookie(); //Error - cannot access context
axios.interceptors.request.use(
(config) => {
const accessToken = authContext && authContext.state && authContext.state.token;
if(accessToken) config.headers.Authorization = `Bearer ${accessToken}`;
return config;
},
(error) => {
Promise.reject(error);
}
);
axios.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
const refreshToken = getCookie(AppConstants.AUTH_COOKIE_NAME);
if(refreshToken && error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const response = await axios
.post(process.env.REACT_APP_REFRESH_TOKEN_API_URL!, { refreshToken: refreshToken });
if (response.status === 200 && response.data && response.data.accessToken) {
authContext && authContext.dispatch && authContext.dispatch({
payload: {
token: response.data.accessToken
},
type: AuthSteps.SIGN_IN
});
accessToken = response.data.accessToken;
return axios(originalRequest);
}
}
return Promise.reject(error);
}
);
const data: IGraphQLResponse = await axios.post(process.env.REACT_APP_GRAPHQL_URL!, {
query: params.text,
variables
});
if(Array.isArray(data.errors)) {
throw new Error(
`Error fetching GraphQL query '${
params.name
}' with variables '${JSON.stringify(variables)}': ${JSON.stringify(
data.errors,
)}`,
);
}
return data;
}
export default new Environment({
network: Network.create(fetchRelay),
store: new Store(new RecordSource(), {
gcReleaseBufferSize: 10,
}),
});