a nextjs server with passport-local authentication responds with error - next.js

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

Related

Running an async function with React Native and Firestore returns goofy results

I'm trying to learn react native along with firebase firestore and I'm racking my brain trying to figure this situation out. Below is my code. I'm trying to pull user information stored in firebase. The console.log in the function returns the array with all of the values such as Document data: {"admin": true, "email": "bob#bob.com", "first": "Bob", "last": "BobLastName", "phone": "555-555-5555"}, however if I try to console.log the return outside of the function or try to assign it to a variable I'm getting. LOG {"_A": null, "_x": 0, "_y": 0, "_z": null}. Any help would be much appreciated before I chuck my laptop out of the window.
import { Pressable, Text } from 'react-native';
import { getAuth} from 'firebase/auth';
import { doc, getDoc, setDoc, getDocs, collection, query, where, Firestore } from 'firebase/firestore';
import { db } from '../components/FirebaseConfig';
import { useEffect, useId, useState } from 'react';
import { async } from '#firebase/util';
import { FlatList } from 'react-native-gesture-handler';
function HomeScreen() {
//Create an async function that will pull user details from firebase and return them as an object
async function getUserDetails() {
const user = getAuth().currentUser;
const userRef = doc(db, "users", user.uid);
const docSnap = await getDoc(userRef);
if (docSnap.exists()) {
console.log("Document data:", docSnap.data());
return docSnap.data();
} else {
console.log("No such document!");
}
}
//Call the getUserDetails function and store the returned object in a variable
const userDetails = getUserDetails();
//log userdetails to the console
console.log(userDetails);
//getUserDetails()
return <Text>Welcome HomeScreen</Text>;
}
export default HomeScreen
I've tried to use the above code to get the data to work. It seems like I am not extracting the data properly.
You are getting this kind of behavior because you are calling getUserDetails() functions directly but it should be called with await or used with .then() because it is returning a Promise<DocumentData> refer this
Basically it will return a promise that will eventually resolve with the data.
To Solve this you can either use
const userDetails = await getUserDetails();
console.log(userDetails);
OR
getUserDetails().then((data) => {
console.log(data);
});
Although using useEffect here is also not a bad option it will look something like this:
function HomeScreen() {
const [userDetails, setUserDetails] = useState(null);
useEffect(() => {
async function getUserDetails() {
const user = getAuth().currentUser;
const userRef = doc(db, "users", user.uid);
const docSnap = await getDoc(userRef);
if (docSnap.exists()) {
const data = docSnap.data();
console.log("Document data:", data);
setUserDetails(data);
} else {
console.log("No such document!");
}
}
getUserDetails();
}, []);
if (!userDetails) {
return <Text>Loading...</Text>;
}
return <Text>Welcome {userDetails.first}!</Text>;
}
Refer getDoc

How to send httponly cookies client side when using next-auth credentials provider?

I'm creating a next js application, using next-auth to handle authentication.
I have an external backend api, so I'm using Credentials Provider.
The problem is that the backend sends httponly cookies, but those are not being attached to the browser when i make a request client side.
In /pages/api/[...auth].js
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import clientAxios from '../../../config/configAxios'
export default NextAuth({
providers: [
Providers.Credentials({
async authorize(credentials) {
try {
const login = await clientAxios.post('/api/login', {
username: credentials.username,
password: credentials.password,
is_master: credentials.is_master
})
const info = login.data.data.user
const token = {
accessToken: login.data.data.access_token,
expiresIn: login.data.data.expires_in,
refreshToken: login.data.data.refresh_token
}
// I can see cookies here
const cookies = login.headers['set-cookie']
return { info, token, cookies }
} catch (error) {
console.log(error)
throw (Error(error.response.data.M))
}
}
})
],
callbacks: {
async jwt(token, user, account, profile, isNewUser) {
if (token) {
// Here cookies are set but only in server side
clientAxios.defaults.headers.common['Cookie'] = token.cookies
}
if (user) {
token = {
user: user.info,
...user.token,
}
}
return token
},
async session(session, token) {
// Add property to session, like an access_token from a provider.
session.user = token.user
session.accessToken = token.accessToken
session.refreshToken = token.refreshToken
return session
}
},
session: {
jwt: true
}
})
my axios config file
import axios from 'axios';
const clientAxios = axios.create({
baseURL: process.env.backendURL,
withCredentials: true,
headers:{
'Accept' : 'application/json',
'Content-Type' : 'application/json'
}
});
export default clientAxios;
a page component
import { getSession } from "next-auth/client";
import clientAxios from "../../../config/configAxios";
import { useEffect } from "react"
export default function PageOne (props) {
useEffect(async () => {
// This request fails, cookies are not sent
const response = await clientAxios.get('/api/info');
}, [])
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
export async function getServerSideProps (context) {
const session = await getSession(context)
if (!session) {
return {
redirect: {
destination: '/login',
permanent: false
}
}
}
// This request works
const response = await clientAxios.get('/api/info');
return {
props: {
session,
info: response.data
}
}
}
After time of researching I have figured it out.
I had to make a change in /pages/api/auth in the way I'm exporting NextAuth.
Instead of
export default NextAuth({
providers: [
...
]
})
Export it like this, so we can have access to request and response object
export default (req, res) => {
return NextAuth(req, res, options)
}
But to access them in the options object, we can make it a callback
const nextAuthOptions = (req, res) => {
return {
providers: [
...
]
}
}
export default (req, res) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
To send a cookie back to the frontend from the backed we must add a 'Set-Cookie' header in the respond
res.setHeader('Set-Cookie', ['cookie_name=cookie_value'])
The complete code would be
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
const nextAuthOptions = (req, res) => {
return {
providers: [
CredentialsProvider({
async authorize(credentials) {
try {
const response = await axios.post('/api/login', {
username: credentials.username,
password: credentials.password
})
const cookies = response.headers['set-cookie']
res.setHeader('Set-Cookie', cookies)
return response.data
} catch (error) {
console.log(error)
throw (Error(error.response))
}
}
})
]
}
}
export default (req, res) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
Update - Typescript example
Create a type for the callback nextAuthOptions
import { NextApiRequest, NextApiResponse } from 'next';
import { NextAuthOptions } from 'next-auth';
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
Combining everything
import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import axios from 'axios'
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
const nextAuthOptions: NextAuthOptionsCallback = (req, res) => {
return {
providers: [
CredentialsProvider({
credentials: {
},
async authorize(credentials) {
try {
const response = await axios.post('/api/login', {
username: credentials.username,
password: credentials.password
})
const cookies = response.headers['set-cookie']
res.setHeader('Set-Cookie', cookies)
return response.data
} catch (error) {
console.log(error)
throw (Error(error.response))
}
}
})
],
callbacks: {
...
},
session: {
...
}
}
}
export default (req: NextApiRequest, res: NextApiResponse) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
To remove cookie in nextAuth after signing out, I used the following block of code - set the cookie parameters to match what you have for the cookie to be expired - Use the SignOut event in [...nextauth].js file
export default async function auth(req, res) {
return await NextAuth(req, res, {
...
events: {
async signOut({ token }) {
res.setHeader("Set-Cookie", "cookieName=deleted;Max-Age=0;path=/;Domain=.techtenum.com;");
},
},
...
}
}
You need to configure clientAxios to include cookies that the server sends as part of its response in all requests back to the server. Setting api.defaults.withCredentials = true; should get you what you want. See the axios configuration for my vue application below:
import axios from "axios";
export default ({ Vue, store, router }) => {
const api = axios.create({
baseURL: process.env.VUE_APP_API_URL
});
api.defaults.withCredentials = true; ------> this line includes the cookies
Vue.prototype.$axios = api;
store.$axios = api;
};

Calling a redux action inside another action is not working

i have console log the error and the error is visible in console but the setAlert action is not dispatching , why is that ?
This is the Auth action code
import axios from "axios"
import {setAlert} from "../actions/alert";
import { REGISTER_FAIL } from "./conts"
export const register = ({ name, username, email, password }) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
}
const body = JSON.stringify({ name, username, email, password });
try {
const res = await axios.post("http://localhost:8080/signup",body,config);
setAlert("Email has been sent..! Please Confirm Your email","success",10000) //not working
}
catch (err) {
console.log(err.response.data.error);
const error = err.response.data.error;
setAlert(error,"error",5000); //not working
dispatch({
type:REGISTER_FAIL
});
}
}
this is Alert action
import {SET_ALERT,REMOVE_ALERT} from "./conts";
import {v4 as uuidv4} from "uuid";
export const setAlert =(msg,alertType,timeOut =5000)=> dispatch=>{
const id= uuidv4();
dispatch({
type: SET_ALERT,
payload:{msg,alertType,id}
});
setTimeout(()=> dispatch({type:REMOVE_ALERT,payload:id}),timeOut)
}
ok it seems , i just needed to dispatch the setAlerts

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.

redux refresh token middleware

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

Resources