redux refresh token middleware - redux

I have a middleware that can go to the refresh token before the next action runs and then run the other action when the access token expires.
But if I make more than one request at a time and the access token is over, I am trying to get as much refresh token as I am requesting. I am checking the isLoading property in state to prevent this. But after the request, isLoading value is true in the reducer, it seems to be false in the middleware, so it requests again and again.
I am sending refreshTokenPromise in fetching_refresh_token action, but I never get state.refreshTokenPromise, it is always undefined.
I definitely have a problem with the state.
So here is my question, how can I access the changing state value in middleware?
Refresh token middleware: (this version hits the endpoint multiple times)
import { AsyncStorage } from 'react-native';
import { MIN_TOKEN_LIFESPAN } from 'react-native-dotenv';
import moment from 'moment';
import Api from '../lib/api';
import {
FETCHING_REFRESH_TOKEN,
FETCHING_REFRESH_TOKEN_SUCCESS,
FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';
export default function tokenMiddleware({ dispatch, getState }) {
return next => async (action) => {
if (typeof action === 'function') {
const state = getState();
if (state) {
const expiresIn = await AsyncStorage.getItem('EXPIRES_IN');
if (expiresIn && isExpired(JSON.parse(expiresIn))) {
if (!state.refreshToken.isLoading) {
return refreshToken(dispatch).then(() => next(action));
}
return state.refreshTokenPromise.then(() => next(action));
}
}
}
return next(action);
};
}
async function refreshToken(dispatch) {
const clientId = await AsyncStorage.getItem('CLIENT_ID');
const clientSecret = await AsyncStorage.getItem('CLIENT_SECRET');
const refreshToken1 = await AsyncStorage.getItem('REFRESH_TOKEN');
const userObject = {
grant_type: 'refresh_token',
client_id: JSON.parse(clientId),
client_secret: JSON.parse(clientSecret),
refresh_token: refreshToken1,
};
const userParams = Object.keys(userObject).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(userObject[key])).join('&');
const refreshTokenPromise = Api.post('/token', userParams).then(async (res) => {
await AsyncStorage.setItem('ACCESS_TOKEN', res.access_token);
await AsyncStorage.setItem('REFRESH_TOKEN', res.refresh_token);
await AsyncStorage.setItem('EXPIRES_IN', JSON.stringify(res['.expires']));
dispatch({
type: FETCHING_REFRESH_TOKEN_SUCCESS,
data: res,
});
return res ? Promise.resolve(res) : Promise.reject({
message: 'could not refresh token',
});
}).catch((err) => {
dispatch({
type: FETCHING_REFRESH_TOKEN_FAILURE,
});
throw err;
});
dispatch({
type: FETCHING_REFRESH_TOKEN,
refreshTokenPromise,
});
return refreshTokenPromise;
}
function isExpired(expiresIn) {
return moment(expiresIn).diff(moment(), 'seconds') < MIN_TOKEN_LIFESPAN;
}
Refresh token reducer:
import {
FETCHING_REFRESH_TOKEN,
FETCHING_REFRESH_TOKEN_SUCCESS,
FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';
const initialState = {
token: [],
isLoading: false,
error: false,
};
export default function refreshTokenReducer(state = initialState, action) {
switch (action.type) {
case FETCHING_REFRESH_TOKEN:
return {
...state,
token: [],
isLoading: true,
};
case FETCHING_REFRESH_TOKEN_SUCCESS:
return {
...state,
isLoading: false,
token: action.data,
};
case FETCHING_REFRESH_TOKEN_FAILURE:
return {
...state,
isLoading: false,
error: true,
};
default:
return state;
}
}
In the meantime, when I send it to the getState to refreshToken function, I get to the changing state value in the refreshToken. But in this version, the refresh token goes to other actions without being refreshed.
Monkey Patched version: (this version only makes 1 request)
import { AsyncStorage } from 'react-native';
import { MIN_TOKEN_LIFESPAN } from 'react-native-dotenv';
import moment from 'moment';
import Api from '../lib/api';
import {
FETCHING_REFRESH_TOKEN,
FETCHING_REFRESH_TOKEN_SUCCESS,
FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';
export default function tokenMiddleware({ dispatch, getState }) {
return next => async (action) => {
if (typeof action === 'function') {
const state = getState();
if (state) {
const expiresIn = await AsyncStorage.getItem('EXPIRES_IN');
if (expiresIn && isExpired(JSON.parse(expiresIn))) {
if (!state.refreshTokenPromise) {
return refreshToken(dispatch, getState).then(() => next(action));
}
return state.refreshTokenPromise.then(() => next(action));
}
}
}
return next(action);
};
}
async function refreshToken(dispatch, getState) {
const clientId = await AsyncStorage.getItem('CLIENT_ID');
const clientSecret = await AsyncStorage.getItem('CLIENT_SECRET');
const refreshToken1 = await AsyncStorage.getItem('REFRESH_TOKEN');
const userObject = {
grant_type: 'refresh_token',
client_id: JSON.parse(clientId),
client_secret: JSON.parse(clientSecret),
refresh_token: refreshToken1,
};
if (!getState().refreshToken.isLoading) {
const userParams = Object.keys(userObject).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(userObject[key])).join('&');
const refreshTokenPromise = Api.post('/token', userParams).then(async (res) => {
await AsyncStorage.setItem('ACCESS_TOKEN', res.access_token);
await AsyncStorage.setItem('REFRESH_TOKEN', res.refresh_token);
await AsyncStorage.setItem('EXPIRES_IN', JSON.stringify(res['.expires']));
dispatch({
type: FETCHING_REFRESH_TOKEN_SUCCESS,
data: res,
});
return res ? Promise.resolve(res) : Promise.reject({
message: 'could not refresh token',
});
}).catch((err) => {
dispatch({
type: FETCHING_REFRESH_TOKEN_FAILURE,
});
throw err;
});
dispatch({
type: FETCHING_REFRESH_TOKEN,
refreshTokenPromise,
});
return refreshTokenPromise;
}
}
function isExpired(expiresIn) {
return moment(expiresIn).diff(moment(), 'seconds') < MIN_TOKEN_LIFESPAN;
}
Thank you.

I solved this problem using axios middlewares. I think is pretty nice.
import { AsyncStorage } from 'react-native';
import Config from 'react-native-config';
import axios from 'axios';
import { store } from '../store';
import { refreshToken } from '../actions/refreshToken'; // eslint-disable-line
const instance = axios.create({
baseURL: Config.API_URL,
});
let authTokenRequest;
function resetAuthTokenRequest() {
authTokenRequest = null;
}
async function getAuthToken() {
const clientRefreshToken = await AsyncStorage.getItem('clientRefreshToken');
if (!authTokenRequest) {
authTokenRequest = store.dispatch(refreshToken(clientRefreshToken));
authTokenRequest.then(
() => {
const {
token: { payload },
} = store.getState();
// save payload to async storage
},
() => {
resetAuthTokenRequest();
},
);
}
return authTokenRequest;
}
instance.interceptors.response.use(
response => response,
async (error) => {
const originalRequest = error.config;
if (
error.response.status === 401
&& !originalRequest._retry // eslint-disable-line no-underscore-dangle
) {
return getAuthToken()
.then(() => {
const {
token: {
payload: { 'access-token': accessToken, client, uid },
},
} = store.getState();
originalRequest.headers['access-token'] = accessToken;
originalRequest.headers.client = client;
originalRequest.headers.uid = uid;
originalRequest._retry = true; // eslint-disable-line no-underscore-dangle
return axios(originalRequest);
})
.catch(err => Promise.reject(err));
}
return Promise.reject(error);
},
);
export default instance;
If you have a problem, do not hesitate to ask.

you could benefit from redux-sagas
https://github.com/redux-saga/redux-saga
redux-sagas is just background runner which monitors your actions and can react when some specific action is met. You can listen for all actions and react to all or you can react to only latest as mentioned in comments
https://redux-saga.js.org/docs/api/#takelatestpattern-saga-args
while redux-thunk is just another way to create actions on the go and wait for some I/O to happen and then create some more actions when I/O is done. It's more like synced code pattern and redux-sagas is more like multi-threaded. On main thread you have your app running and on background thread you have sagas monitors and reactions

Related

Redux Saga not waiting for Firebase service to return

SignUpSaga is broken. It doesn't wait for "yield put(updateUserAction(user)); " to return a value, it just returns undefined breaking the app then, sign up service returns after.
How do I make it wait? I thought this is what yielding does.
Parts of redux package:
export const updateUserAction = (user: User | null): UpdateUserActionType => ({
type: UPDATE_USER,
user,
});
...
export const userReducer = (
state: StateSlice = initialState.user,
action: UpdateUserActionType
): StateSlice => {
switch (action.type) {
case UPDATE_USER:
return updateHandler(state, action);
case SIGN_OUT:
return signOutHandler();
default:
return state;
}
};
Interactor
export class SignUpInteractor {
signUpService: SignUpService;
constructor(signUpService: SignUpService) {
this.signUpService = signUpService;
}
async signUp(
firstName: string,
lastName: string,
credential: Credential
): Promise<User> {
const user = new User(firstName, lastName, credential.email);
return this.signUpService.signUpUser(user, credential);
}
}
saga
function* signUpSaga(action: SignUpActionType) {
const { firstName, lastName, credential } = action;
const service = new FirebaseLogin();
const interactor = new SignUpInteractor(service);
const user = yield call(() => {
interactor.signUp(firstName, lastName, credential);
});
yield put(updateUserAction(user));
}
Finally, sign up service:
export class FirebaseLogin implements SignUpService {
async signUpUser(user: User, credential: Credential): Promise<User> {
var user: User;
try {
db.app
.auth()
.createUserWithEmailAndPassword(credential.email, credential.password)
.then((authData) => {
db.app
.database()
.ref("users/" + authData.user.uid)
.set({
uid: authData.user.uid,
email: credential.email,
firstName: user.firstName,
lastName: user.lastName,
})
.then(() => {
db.app
.database()
.ref("users/" + authData.user.uid)
.once("value")
//do this to make sure data was returned as values, not resolved later by firebase.
.then((snapchat) => {})
.then(() => {
console.log("returning");
return user;
});
})
.catch((error) => {
return new User("error", "error", "err#err.com");
});
})
.catch((error) => {
console.log("error here");
return new User("error", "error", "err#err.com");
});
} catch (error) {
console.log("error here");
return new User("error", "error", "err#err.com");
}
}
I think the primary problem is how you're using call:
const user = yield call(() => {
interactor.signUp(firstName, lastName, credential);
});
You're passing a synchronous function that will start an async call and return immediately. In addition, that function returns nothing, hence the undefined.
What you really want is to tell the middleware to make the async call, and thus wait for the promise to be resolved:
const user = yield call(interactor.signUp, firstName, lastName, credential);
Also, it looks like your code is wayyyy more complicated than it needs to be: writing Redux code by hand, extra functions in the reducers, defining separate TS types for action objects, and also this "interactor" and "service" bit. Even using sagas is overkill here for this example. You should be using our official Redux Toolkit package and following our recommendations for using Redux with TS. That will simplify your code dramatically.
If I was writing this myself, I'd have the signUp part be a standalone async function, and use RTK's createAsyncThunk:
async function signUpUser(user: User, credential: Credential): Promise<User> {
// omit Firebase code
return user;
}
interface SignUpArgs {
firstName: string;
lastName: string;
credential: Credential;
}
const signUp = createAsyncThunk(
'users/signUp',
async ({firstName, lastName, credential}: SignUpArgs) => {
const user = new User(firstName, lastName, credential.email);
return signUpUser(user, credential);
}
)
type UserState = User | null;
const initialState : UserState = null;
const userSlice = createSlice({
name: 'users',
initialState, // whatever your state actually is,
reducers: {
// assorted reducer logic here
signOut(state, action) {
return null;
}
},
extraReducers: builder => {
builder.addCase(signUp.fulfilled, (state, action) => {
return action.payload
});
}
})
Much less code in the end, especially if you're doing any actual meaningful state updates in the reducers.

a nextjs server with passport-local authentication responds with error

Pages/api/signup.js
Signups api
import dbConnect from '../../utils/dbConnect'
import Author from '../../models/Author'
import auth from "../../middleware/auth"
import isEmail from 'validator/lib/isEmail';
import normalizeEmail from 'validator/lib/normalizeEmail';
import bcrypt from 'bcryptjs';
import nextConnect from "next-connect"
const handler = nextConnect()
handler.use(auth)
handler.post(async (req, res) => {
await dbConnect()
try {
const {name, image, password} = req.body;
const email = normalizeEmail(req.body.email);
if (!isEmail(email)) { res.status(400).send('The email you entered is invalid.');
return;
}
if (!password || !name || !image) {
res.status(400).send('Missing field(s)');
return; }
// check if email existed
Author.exists({email: email}, async (error, result)=>{
if(result) {
res.status(403).send('The email has already been used.');
return;
}else {
const hashedPassword = await bcrypt.hash(password, 10);
const author = await Author.create({
email,
password: hashedPassword,
name, image
}) /* create a new model in the database */
console.log("new author registered successfully")
req.logIn(author, (err) => { if (err) throw err;
// when we finally log in, return the (filtered) user object
res.status(201).json({
author: {name, email, image}})
});
}
})
} catch (error) {
console.log(error)
}
})
export default handler;
Login Api
Pages/api/login.js
import nextConnect from 'next-connect'
import auth from '../../middleware/auth'
import passport from '../../lib/passport'
const handler = nextConnect()
handler.use(auth)
handler.post(passport.authenticate('local'), (req, res) => {
// return our user object
const { email, _id, name, image} = req.author
const author = { email, name, image } res.json( {author} ) });
export default handler
Middleware function
middleware/auth.js
import nextConnect from 'next-connect'
import passport from '../lib/passport'
import session from '../lib/session'
import dbConnect from "../utils/dbConnect"
const auth = nextConnect()
auth
.use(dbConnect)
.use(session)
.use(passport.initialize())
.use(passport.session())
export default auth
Code for passport-local strategy
lib/passport.js
import passport from 'passport'
import LocalStrategy from 'passport-local'
import Author from "../models/Author"
passport.serializeUser(function(author, done) {
done(null, author.email); });
passport.deserializeUser(function(email, done) {
User.findOne({email: email}, function(err, author) { done(err, author);
});
});
passport.use(new LocalStrategy(
function(email, password, done) {
Author.findOne({ email: email }, async function (err, author) { if (err) { return done(err); }
if (!author) { return done(null, false); }
if (!(await bcrypt.compare(password, author.password) )){
return done(null, false);
}
return done(null, author);
});
}
)
)
export default passport
Code to get section
lib/session.js
const session = require('express-session');
const MongoStore = require('connect-mongo').default;
export default function (req, res, next) {
const mongo = process.env.MONGODB_URL
return session({
secret: process.env.TOKEN_SECRET,
store: MongoStore.create({ mongoUrl: `${mong}`})
})(req, res, next)
}
My database connection
utils/dbConnect
import mongoose from 'mongoose';
const dbConnect = async (req, res, next) => {
try { if (!global.mongoose) { global.mongoose = await mongoose.connect(process.env.MONGODB_URL,{
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
});
}
} catch (err) {
console.error(err);
}
// You could extend the NextRequest interface
// with the mongoose instance as well if you wish. //req.mongoose = global.mongoose;
return next();
};
export default dbConnect
Those are the codes for the API, I'm not sure if I should add the code from the client side that's making the request?
I can't really figure out what's wrong with this code, I keep getting a 500 response error code whenever I make a post request.
Sorry for the long code tho

Redux saga is is going to catch() instead of putting the action

I have this action in userActions.js:
const receiveFilialData = (data) => {
return {
type: "RECEIVE_FILIAL_DATA",
payload: data
}
}
export default {
receiveFilialData
}
In my login component, I'm making a dispatch to get some data from a saga:
const Login = props => {
const dispatch = useDispatch()
const filiais = useSelector(state => state.filiais)
useEffect(() => {
dispatch({type: 'GET_FILIAIS_REQUEST'})
}, [dispatch, filiais])
}
In LoginService.js I'm making the HTTP request:
const fetchFiliais = async () => {
const response = await fetch(`${baseUrlApi.baseUrlApi}/filiais`);
const responseJson = await response.json()
return responseJson
}
export default {
fetchFiliais
}
And I create a saga to make this request when there's a GET_FILIAIS_REQUEST dispatch:
// worker Saga: will be fired on GET_FILIAIS_REQUEST actions
function* fetchFiliais(action) {
try {
const data = yield call(LoginService.fetchFiliais())
console.log(data)
yield put({type: "RECEIVE_FILIAL_DATA", data});
} catch (e) {
yield put({type: 'FETCH_FILIAIS_FAILED', message: e.message})
}
}
/*
Starts fetchFiliais on each dispatched `GET_FILIAIS_REQUEST` action.
*/
function* mySaga() {
yield takeEvery("GET_FILIAIS_REQUEST", fetchFiliais);
}
If I put a console.log(response.json) in the LoginService file, fetchFiliais is printing my response, but my console.log(data) after yield call() is not running; the control flow is going to catch(e) and throwing:
type: "FETCH_FILIAIS_FAILED" message: "call: argument of type
{context, fn} has undefined or null fn"
Why?
Fixed, putting LoginService.fetchFiliais instead LoginService.fetchFiliais())
This is not a function but a const

Why are my redux actions not firing correctly?

I am trying to implement a check for authentication and to login/logout users using redux and firebase. I have the following code:
Action Types:
export const LOGIN_REQ = 'AUTH_REQ';
export const LOGOUT_REQ = 'LOGOUT_REQ';
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
export const AUTH_FAILED = 'AUTH_FAILED';
export const GET_AUTH = 'GET_AUTH';
Reducers:
import * as ActionTypes from './ActionTypes';
export const auth = (state = {
isAuth: false,
user: null
}, action) => {
switch (action.type) {
case ActionTypes.LOGIN_REQ:
return { ...state, isAuth: false, user: null };
case ActionTypes.LOGOUT_REQ:
return { ...state, isAuth: false, user: null };
case ActionTypes.AUTH_FAILED:
return { ...state, isAuth: false, user: null };
case ActionTypes.AUTH_SUCCESS:
return { ...state, isAuth: true, user: action.payload };
case ActionTypes.GET_AUTH:
return state;
default:
return state;
}
}
Thunks:
export const getAuth = () => (dispatch) => {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
console.log('Get AUTH called');
dispatch(authSuccess());
}
else {
console.log('Get AUTH called');
dispatch(authFailed());
}
});
}
export const loginReq = (email, password, remember) => (dispatch) => {
firebase.auth().signInWithEmailAndPassword(email, password)
.then((cred) => {
if (remember === false) {
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
console.log('Logged In with Redux without persist');
}
else {
console.log('Logging in with Persist');
}
console.log('Dispatching Success !');
dispatch(authSuccess(cred.user.uid));
})
.catch((err) => {
console.log(err);
dispatch(authFailed(err));
});
}
export const logoutReq = () => (dispatch) => {
firebase.auth().signOut()
.then(() => dispatch(getAuth()))
.catch((err) => console.log(err));
}
export const authSuccess = (uid = null) => ({
type: ActionTypes.AUTH_SUCCESS,
payload: uid
});
export const authFailed = (resp) => ({
type: ActionTypes.AUTH_FAILED,
payload: resp
});
And I am calling it from a component as shown below:
const mapStateToProps = state => {
return {
isAuth: state.isAuth,
user: state.user
}
}
const mapDispatchToProps = dispatch => ({
getAuth: () => { dispatch(getAuth()) },
loginReq: (email, password, remember) => { dispatch(loginReq(email, password, remember)) },
logoutReq: () => { dispatch(logoutReq()) }
})
handleLogin() {
this.props.loginReq(this.state.email, this.state.password, this.state.remember);
}
handleLogOut() {
this.props.logoutReq();
}
<BUTTON onClick=()=>this.handleLogOut()/handleLogin()>
I am close to tears because I cannot figure out why my loginReq fires one or many gitAuth() methods even when i click on the button once. This happens only for the loginReq() action. I have not specified anywhere that loginReq() should fire it.
Also i have called the getAuth() method in the component did mount method of my main screen which checks authentication status once at the start of the app.
EDIT: I have console logged in the component did mount method in the main component so I know that this getAuth() call is not coming from there.
Imo the answer is badly done, try to reestructure it better, what you call "Thunks" are actually "Actions". But if I were to tell you something that could help is that maybe the problem lies in the thunk middleware config or with the way firebase is beign treated by the dispatcher, so I would say that you better try coding an apporach with the react-redux-firebase library (this one: http://react-redux-firebase.com/docs/getting_started ) it makes easier to connect redux with a firebase back end. Other great reference, the one that I learned with, is The Net Ninja's tutorial playlist about react, redux and firebase.
A friend of mine told me this has to do with something known as an 'Observer' which is in the onAuthStateChange() provided by firebase. Basically there is a conflict between me manually considering the user as authenticated and the observer doing so.

Object data null sms still sent twilio authy

Im trying to implement the authy-node phone verification with firebase functions and my app in react-native the message is sent to the correct mobile phone but for some reason the data I get back from the api is null any ideas out there
My Api firebase functions
import * as functions from 'firebase-functions';
const authy = require('authy')('mySecret');
export const getCode = functions.https.onCall((data, context) => {
const {
number, countryCode
} = data;
return authy.phones().verification_start(number, countryCode, { via:
'sms', locale: 'en', code_length: '4' }, (err: any, res: any) => {
if (err) {
throw new functions.https.HttpsError(err);
}
return res;
});
});
and this is my call from my app
export default class test extends Component {
constructor() {
super();
}
componentWillMount() {
const getCode = firebase.functions().httpsCallable('getCode');
getCode({number: 'theCorrectNumber', countryCode: '44'})
.then(function (result) {
const data = result;
console.log(data)
}).catch( function (error){
console.log(error)
})
}
render() {
return (
<View/>
);
}
}
Twilio developer evangelist here.
From what I can see in the Authy Node library that I'm assuming you're using, making a request to the API does not return a Promise. Instead it is built with request and responds to asynchronous requests using callbacks only. You do deal with the callback, but you are returning the result of calling the asynchronous function, which is null, rather than the result from the callback.
Perhaps including a callback as part of the function call would work better:
import * as functions from 'firebase-functions';
const authy = require('authy')('mySecret');
export const getCode = functions.https.onCall((data, callback) => {
const { number, countryCode } = data;
return authy
.phones()
.verification_start(
number,
countryCode,
{ via: 'sms', locale: 'en', code_length: '4' },
callback
);
});
You can then use it like this:
export default class test extends Component {
constructor() {
super();
}
componentWillMount() {
const getCode = firebase.functions().httpsCallable('getCode');
getCode({ number: 'theCorrectNumber', countryCode: '44' }, (err, res) => {
if (err) {
throw new functions.https.HttpsError(err);
}
const data = res;
console.log(data);
});
}
render() {
return <View />;
}
}
Let me know if that helps at all.

Resources