How should I handle JWT token refresh in my Redux app? - javascript

Our Redux application use JWT tokens for authentication. The access_token expires every 15 minutes and the refresh_token expires every 30 days. Both of them are provided by our API every time you log in and stored in the browser's local storage. If a secure endpoint receives a request with an expired token, it returns a 401 HTTP error.
Unfortunately, I don't know how to proceed to handle the refresh process without having a negative impact on the user. From a technical point of view, here is what I would like to achieve:
Action creator calls the API with an expired token
Client receives a 401 HTTP error
Client triggers a function that calls the API to obtain a new token (by providing the refresh token).
If the call fails (refresh_token is expired), prompt the user the re-enter its credentials to re-obtain both tokens then re-attempt the original request.
If the call succeeds, re-attempt the original request.
I would like to have a function that would handle the refreshing process and that would be called in the error handling portion of the action creator.
Here is what I have tried so far:
export function handleError(dispatch, current_func, error, handling) {
if(error.response) {
if(error.response.status === 401 && readToken("token") !== null) {
return attemptTokenRefresh(dispatch, current_func)
}
if(error.response.status === 422 && readToken("token") === null) {
return attemptTokenRefresh(dispatch, current_func)
}
}
return(handling())
}
export function attemptTokenRefresh(dispatch, on_success) {
let token = readToken("refresh_token");
let instance = axios.create({
headers: {"Authorization": token}
});
instance.post("api/refresh").then(response => {
if (response.data["token"]) {
storeToken("token", response.data["token"]);
on_success();
}
}).catch(error => {
//TODO: Allow user to sign back (prevent wiping the state)
});
}
dispatch refers to the dispatch function provided by Redux
current_func refers to the action creator
error refers to the error returned by the API
handling refers to the error handling function for other types of errors
Any help would be greatly appreciated :)

Related

How to get ID token refreshed when using Firebase Auth javascript SDK

I use a backend app server to communicate with firestore, so the ReactJS client sends ID token to server which in turn communicates with firebase using that token. Client does not make any firebase calls directly other than auth calls. Problem I am facing is, the ID token never gets refreshed and after an hour, backend server fails to recognize token and ends up sending 401 to client. My understanding is that the client should always refresh the token and always send a valid token to backend app server.
firebase.auth().onAuthStateChanged(auth, (user) => {
if (user) {
localStorage.setItem("auth.uid", user.uid)
localStorage.setItem("auth.email", user.email)
user.getIdToken(true).then(function(idToken) {
localStorage.setItem("auth.token", idToken)
})
} else {
// User is signed out
localStorage.removeItem("auth.email")
localStorage.removeItem("auth.uid")
localStorage.removeItem("auth.token")
}
})
firebase.auth().onIdTokenChanged((user) => {
// ** NOT GETTING THIS CALLBACK AFTER 1 HR TO REFRESH **
if (user) {
// User is signed in or token was refreshed
user.getIdToken(true).then(function(idToken) {
localStorage.setItem("auth.token", idToken)
})
}
})
Other option, may be, is to use the REST API to manually fetch a new ID token and use that but having passed forceRefresh value true I would expect this is handled automatically.
What am I missing?

Handling Firebase auth authentication

I need some help understanding if I am handling the authentication/authorization correctly with Firebase Auth, JS and Python.
Once the user has signed in, I capture the idToken and create a cookie in the browser:
firebase.auth().signInWithEmailAndPassword(email, pass)
.then(({user}) => {
return user.getIdToken().then((idToken) => {
if (idToken) {
document.cookie = "token=" + idToken;
window.location.assign('/profile');
} else {
document.cookie = "token="
}
})
})
.catch((error) => {
//handle error here
});
The route /profile should be protected so I created a decorator and I retrieve the cookie and verify it:
id_token = request.cookies.get("token")
if not id_token:
return redirect(url_for('login'))
try:
decoded_token = auth.verify_id_token(id_token)
uid = decoded_token['uid']
except Exception as e:
print("Exception: {}".format(e))
return redirect(url_for('login'))
This is working so far but I want to see if this is the ideal situation from a security perspective. Also, what about the onAuthStateChanged? How should I handle it in the case above?
Firebase SDKs send the ID token with each request in the Authorization header, so sending it in a cookie is not going to more or less dangerous than that.
Instead of determining the token in signInWithEmailAndPassword though, I'd instead monitor ID token generation by listening to onIdTokenChanged events and using that moment to update your cookie.

Firebase ID Token expiration in an hour

so I am using redux-saga in my react-native app and tried to use refresh token but didn't work, so my approach was the following in app.js in order to get the token specifically for each request and force refresh it:
handleResponse = async () => {
const {dispatch} = this.store;
await axios.interceptors.request.use(config => {
// Important: request interceptors **must** return the request.
console.log("refreshToken")
let user = firebase.auth().currentUser;
firebase.auth().onAuthStateChanged(function(user) { if (user) {
console.log("auth changed: ",user)
user.getIdToken(true).then((token) => {
setAccessToken(token);
config.headers.authorization = token;
}
);
} else { console.log("didn't auth change") } });
console.log("req in handle response: ",JSON.stringify(config));
return config;
});
axios.interceptors.response.use(config => config, (err) => {
if (err.response) {
const response = err.response;
const state = this.store.getState();
if (
response.status === 401
&& state.auth.isAuthenticated
) {
dispatch(logout());
}
}
return Promise.reject(err);
});
};
But it always ends up after an hour throwing me the following error::
Firebase ID token has expired. Get a fresh token from your client app and try again (auth/id-token-expired). See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
so I was wondering if there's another approach I can try to solve the issue from my side?
Thanks in advance.
Firebase auth tokens automatically refresh every hour. That's a function of the SDK, and you don't have to do anything to enable it.
You definitely do not want to call firebase.auth().onAuthStateChanged for each request. That starts a persistent listener, and adds a new callback every time it's used. Typically you add just one listener globally for your app, and use its updates as they happen.
If you need to know when the SDK refreshes the token in order to get a new one immediately, you should instead use onIdTokenChanged to set up a callback that will be invoked every time the user's token changes. Again, you should only set up one of these globally for your app, and use its current value at the time of the request.

How to get JWT cookies in our react application, how to check the user is login or not I am unable to find how to handle my react application session

How to get JWT cookies in our react application, how to check the user is login or not I am unable to find how to handle my react application session.
I really appreciate who helps me out with this problem.
Thanks in advance
The server side API is setting the HTTPOnly cookie which you wont be able to read in JS.
What you need to do it in your react App handle a 401 status error and based on that set a flag isAuthenticated or something as false. Otherwise keep it to be true. With each request to the server HTTPOnly cookie would be sent automatically so you don't need to handle the token inside a cookie.
The backend code needs to send a 401 once the cookie is expired, or the logout is requested or the JWT inside a cookie expires.
Before I say anything, you have included app.use(cookieParser()) in index.js right? Because if not, you're gonna need that once you've installed it with npm i cookie-parser
But anyway, a few things:
You can create a PrivateRoute in React, as far as I'm aware this tends to work well to protect routes from unauthorized users.
You can simply store an isAuthenticated in either the state or localStorage: however this will require that you make absolutely sure that a user shouldn't be able to just change the value in the state or add isAuthenticated in localStorage and just spoof authenticity (this is the part that took me the longest to get right).
Anyway, the way I've handled this is that when a user logs in an access token (and a refresh token if it doesn't already exists) are generated from the server and sent to the client in an httpOnly cookie, while this makes it so that you can't do anything with it on the client side with JavaScript as Pavan pointed out in his answer (which is generally a good thing), you should use res.status for validation when you make the fetch request. Here's what my fetch request kind of looks like:
const login = async (user) => {
const body = JSON.stringify(user);
return fetch(loginURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'include', //this is important
body: body
}).then(function(res) {
if (res.status === 200) {
const id = Date.now();
localStorage.sid = id; //this is temporary
return res.json()
} else {
return res.json()
}
})
// you can ignore this part
.then(async resp => {
return resp
})
}
Side-note: Your browser automatically handles the httpOnly cookies you send from your server however the credentials: 'include' needs to be included in your subsequent fetch requests.
In one of my main parent components, the login function is called:
login = async (user) => {
this.setState({ error: null });
await adapter.login(user).then(data => {
if (!data.error) {
this.setState({session: "active"})
} else if (data.error && data.error.status === 401) {
// this is so I can handle displaying invalid credentials errors and stuff
this.setState({ error: data.error, loading: false });
}
});
}
I also have a middleware on the server-side that is run before any of the code in the routes to verify that the user making the request is actually authorized. This is also what handles access token expiration for me; if the access token has expired, I use the refresh token to generate a new access token and send it back in the httpOnly cookie, again with a status of 200 so the user experience isn't jarring. This does of course mean that your refresh token would have to live longer than your access token (I haven't decided how long in my case yet, but I'm thinking either 7 or 14 days) though as far as I'm aware that is okay to do.
One last thing, the parent component I referred to earlier calls a validate function which is a fetch request to the server within its componentDidMount so that the user is verified each time the component mounts, and then I've included some conditional rendering:
render() {
return (
<div className="container">
{
!localStorage.sid && <LoginForms {...yourPropsHere}/>
}
{
this.state.loading && <Loading />
}
{
localStorage.sid && this.state.session === "active" && <Route path="/" render={(props) => <Root error={this.state.error} {...props}/>}/>
}
</div>
);
}
I've gone the conditional rendering route as I couldn't get PrivateRoute to work properly in the time that I had, but either should be fine.
Hopefully this helps.
Your login API should return JWT token and how long it should be live.
Your login API response would be like :
{
jwt: your jwt token,
duration: in seconds
}
Use universal-cookies NPM to store this result into cookies.
For more details how to manipulate with cookies, visit
https://www.npmjs.com/package/universal-cookie
For setting cookies your code like:
const cookies = new Cookies();
cookies.set(name of cookies, jwt value from API call, {
maxAge: duration,
});
Above code store the jwt cookies in browser and after maxAge automatically remove it from browser.
So for identifying session is present or not, you should check after specific interval cookies has present in browser or not. If cookies has present in browser then session is on, otherwise session has expired.

React Native + Redux basic authentication

I'm looking for a way to create a basic authentication for my react-native app.
I couldn't find any good example for react-native app.
To login, the app sends the email/password + clientSecret to my server
If OK, the server returns accessToken + refreshToken
The user is logged in, all other requests include the bearer with the accessToken.
If the accessToken expires, the app requests a new one with the refreshToken automatically.
The user stays logged in all the time, the states should be saved in the phone.
What would be the best approach for this?
Thanks.
When an app communicates with a HTTP API which enforces some form of authentication, the app typically follows these steps:
The app is not authenticated, so we prompt the user to log in.
The user enters their credentials (username and password), and taps submit.
We send these credentials to the API, and inspect the response:
On success (200 - OK): We cache the authentication token/ hash, because we're going to use this token/ hash in every subsequent request.
If the token/ hash does not work during any of the subsequent API requests (401 - Unauthorized), we'll need to invalidate the hash/ token and prompt the user to log in again.
Or, on failure (401 - Unauthorized): We display an error message to the user, prompting them re-enter their credentials.
Logging In
Based on the work flow defined above our app starts by displaying a login form, step 2 kicks in when the user taps the login button which dispatches the login action creator below:
/// actions/user.js
export function login(username, password) {
return (dispatch) => {
// We use this to update the store state of `isLoggingIn`
// which can be used to display an activity indicator on the login
// view.
dispatch(loginRequest())
// Note: This base64 encode method only works in NodeJS, so use an
// implementation that works for your platform:
// `base64-js` for React Native,
// `btoa()` for browsers, etc...
const hash = new Buffer(`${username}:${password}`).toString('base64')
return fetch('https://httpbin.org/basic-auth/admin/secret', {
headers: {
'Authorization': `Basic ${hash}`
}
})
.then(response => response.json().then(json => ({ json, response })))
.then(({json, response}) => {
if (response.ok === false) {
return Promise.reject(json)
}
return json
})
.then(
data => {
// data = { authenticated: true, user: 'admin' }
// We pass the `authentication hash` down to the reducer so that it
// can be used in subsequent API requests.
dispatch(loginSuccess(hash, data.user))
},
(data) => dispatch(loginFailure(data.error || 'Log in failed'))
)
}
}
There's a lot of code in the function above, but take comfort in the fact that
the majority of the code is sanitising the response and can be abstracted away.
The first thing we do is dispatch an action LOGIN_REQUEST which updates our store and lets us know that the user isLoggingIn.
dispatch(loginRequest())
We use this to display an activity indicator (spinning wheel, "Loading...", etc.), and to disable the log in button in our log in view.
Next we base64 encode the user's username and password for http basic auth, and pass it to the request's headers.
const hash = new Buffer(`${username}:${password}`).toString('base64')
return fetch('https://httpbin.org/basic-auth/admin/secret', {
headers: {
'Authorization': `Basic ${hash}`
}
/* ... */
If everything went well, we'll dispatch a LOGIN_SUCCESS action, which results in us having an authentication hash in our store, which we'll use in subsequent requests.
dispatch(loginSuccess(hash, data.user))
On the flip side, if something went wrong then we also want to let the user know:
dispatch(loginFailure(data.error || 'Log in failed')
The loginSuccess, loginFailure, and loginRequest action creators are fairly generic and don't really warrant code samples. See: https://github.com/peterp/redux-http-basic-auth-example/blob/master/actions/user.js)
Reducer
Our reducer is also typical:
/// reducers/user.js
function user(state = {
isLoggingIn: false,
isAuthenticated: false
}, action) {
switch(action.type) {
case LOGIN_REQUEST:
return {
isLoggingIn: true, // Show a loading indicator.
isAuthenticated: false
}
case LOGIN_FAILURE:
return {
isLoggingIn: false,
isAuthenticated: false,
error: action.error
}
case LOGIN_SUCCESS:
return {
isLoggingIn: false,
isAuthenticated: true, // Dismiss the login view.
hash: action.hash, // Used in subsequent API requests.
user: action.user
}
default:
return state
}
}
Subsequent API requests
Now that we have an authentication hash in our store we can pass it into subsequent request's headers.
In our example below we're fetching a list of friends for our authenticated user:
/// actions/friends.js
export function fetchFriends() {
return (dispatch, getState) => {
dispatch(friendsRequest())
// Notice how we grab the hash from the store:
const hash = getState().user.hash
return fetch(`https://httpbin.org/get/friends/`, {
headers: {
'Authorization': `Basic ${hash}`
}
})
.then(response => response.json().then(json => ({ json, response })))
.then(({json, response}) => {
if (response.ok === false) {
return Promise.reject({response, json})
}
return json
})
.then(
data => {
// data = { friends: [ {}, {}, ... ] }
dispatch(friendsSuccess(data.friends))
},
({response, data}) => {
dispatch(friendsFailure(data.error))
// did our request fail because our auth credentials aren't working?
if (response.status == 401) {
dispatch(loginFailure(data.error))
}
}
)
}
}
You may find that most API requests typically dispatch the same 3 actions as above: API_REQUEST, API_SUCCESS, and API_FAILURE, and as such the majority of the request/ response code can be pushed into Redux middleware.
We fetch the hash authentication token from the store and setup the request.
const hash = getState().user.hash
return fetch(`https://httpbin.org/get/friends/`, {
headers: {
'Authorization': `Basic ${hash}`
}
})
/* ... */
If the API response with a 401 status code then we've got to remove our hash from the store, and present the user with a log in view again.
if (response.status == 401) {
dispatch(loginFailure(data.error))
}
I've answered the question generically and only dealing with http-basic-auth.
I think that the concept may remain the same, you'll push the accessToken and refreshToken in the store, and extract it in subsequent requests.
If the request fails then you'll have to dispatch another action which updates the accessToken, and then recalls the original request.
I haven't seen too much by way of examples in this area, and think it's definitely something that needs more coverage. I've not yet implemented auth myself, else I'd point you to some code examples. But I can point you to a couple links I've collected that may help you in the right direction...
Oauth 2 with React Native
React Native: Auth0 Login + Firebase
Regardless how you perform your auth, you'll need to securely store your access, refresh, and secret tokens. On iOS I believe you'd do that using keychain and for Android it looks like KeyStore is the way. You may find oblador/react-native-keychain helpful, though it doesn't yet support android it looks like it may support android soon.
I'm actually working on a video tutorial series that answers at least some of the questions your asking. The video along with a transcript and sample code can be found here: http://codecookbook.co/post/how-to-build-a-react-native-login-form-with-redux-pt1/

Categories

Resources