How can I maintain accessToken from Google OAuth? - reactjs

I am trying to implement Google OAuth with MERN Stack.
There was a nice blog about this like as below.
https://medium.com/#alexanderleon/implement-social-authentication-with-react-restful-api-9b44f4714fa
I used react-google-login for the front-end and passport, passport-google-token, and jsonwebtoken for the back-end.
It was all good until express js got the accessToken and created a User document, but I actually don't know what is the next step to keep login and authenticate it. This question is mentioned in the above article, but he hasn't implemented it.
Is there any information to solve this?
"Step 7: Return a JWT token to the frontend. What you do with that token is out of scope for this tutorial, but it should probably be used to authenticate each of the logged-in user’s actions."
My react side code is as below
import React from 'react';
import GoogleLogin from 'react-google-login';
import api from '../config/api';
import { useGlobalState } from '../config/globalState';
import { AUTH_SIGN_IN, SET_USER } from '../config/types';
const Navbar = () => {
const { state, dispatch } = useGlobalState();
const { isLoggedIn } = state;
const responseGoogle = async (data) => {
try {
const { accessToken } = data;
const res = await api.post('/api/auth/signin', {
access_token: accessToken
});
console.log('res.data:', res.data);
dispatch({ type: AUTH_SIGN_IN });
} catch (err) {
console.log('err: ', err.message);
}
};
return (
<div>
This is the title
{
!isLoggedIn &&
<GoogleLogin
clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}
buttonText="Login with Google"
onSuccess={responseGoogle}
onFailure={responseGoogle}
/>
}
</div>
)
}
export default Navbar
This is passport.js
const passport = require('passport');
const GooglePlusTokenStrategy = require('passport-google-token').Strategy;
const User = require('../models/user');
module.exports = passport => {
passport.use('googleToken', new GooglePlusTokenStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}, async (accessToken, refreshToken, profile, done) => {
console.log('accessToken:', accessToken);
console.log('refreshToken:', refreshToken);
console.log('profile:', profile);
try {
const foundUser = await User.findOne({ googleId: profile.id})
if (foundUser) {
console.log('User already exists');
return done(null, foundUser);
} else {
const newUser = await User.create({
googleId: profile.id,
displayName: profile.displayName,
email: profile.emails[0].value,
photo: profile.photos[0].value
});
await newUser.save();
return done(null, newUser);
}
} catch(error) {
done(error, false, error.message);
}
}));
}
This is the /api/auth/signin route handling part.
const JWT = require('jsonwebtoken');
const User = require('../models/user');
const signToken = user => {
return JWT.sign({
iss: 'AnySecret',
sub: user.id,
iat: new Date().getTime(),
exp: new Date().setDate(new Date().getDate() + 1)
}, process.env.JWT_SECRET);
}
module.exports = {
signIn: async (req, res) => {
console.log('signIn called');
const token = signToken(req.user);
res.cookie('access_token', token, {
httpOnly: true
});
res.status(200).send({
user: req.user,
token
});
}
}

Related

Is there a way to use ctx.session.$create in api using Blitz.js

I am trying to use blitz.js login API in a Flutter project. So I have created a /api/auth/login.ts file with the following code
import { getAntiCSRFToken, getSession, SecurePassword } from "#blitzjs/auth"
import { authenticateUser } from "app/auth/mutations/login"
import { AuthenticationError } from "blitz"
import db from "db"
import { Role } from "types"
const handler = async (req, res) => {
const session = await getSession(req, res)
const { email, password } = req.body
if (req.method !== "POST" || !req.body.data || !session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` })
} else {
console.log("Create a new session for the user")
// Create a new session for the user
//login
const user = await authenticateUser(email, password)
const user = await db.user.findFirst({ where: { email } })
if (!user) return res.json({ data: "Hello", email, password })
const result = await SecurePassword.verify(user.hashedPassword, password)
const { hashedPassword, ...rest } = user
await req.session.$create({ userId: user.id, role: user.role as Role })
res.json({ rest })
}
export default handler
I also tried to use their docs but it was not clear enough and understandable
Can I use ctx.session.$create and insert it to db using blitz.js api
I have solved the problem using this code
import { Role } from "types"
import { authenticateUser } from "app/auth/mutations/login"
import { getSession } from "#blitzjs/auth"
export default async function customRoute(req, res) {
const session = await getSession(req, res)
const { email, password } = req.body
console.log(email, password)
console.log(session.$isAuthorized())
const user = await authenticateUser(email, password)
if (user.id === session.userId) {
return res.status(409).json({ error: `Already exist` })
}
await session.$create({ userId: user.id, role: user.role as Role })
// // res.setHeader("Content-Type", "application/json")
res.end(JSON.stringify({ userId: session.userId }))
}
At first, I was getting a CSRF mismatch error and then a localStorage is undefined and now somehow everything is working with this code.

Correct implementation of refreshtoken with ApolloClient

I am trying to refresh the authentication token when it is near the expiration time, however I end up with an endless loop. The expectation is that the code that checks the expiration time in App.js should work and reset the auth token and the refresh token, however is starts looping endlessly.
I have the following Auth helper functions:
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const { AuthenticationError } = require("apollo-server-express");
const config = require("config");
const jwtSecret = config.get("jwt_secret");
const jwtRefreshTokenSecret = config.get("jwt_refresh_token");
const authFunctions = {
checkSignedIn: async (req, requireAuth = true) => {
const header = req.headers.authorization;
if (header) {
const token = header.replace("Bearer ", "");
const decoded = jwt.verify(token, jwtSecret);
console.log(decoded);
let user = await User.findById(decoded.id);
if (!user) {
throw new AuthenticationError("Invalid user credentials.");
}
return user;
}
if (requireAuth) {
throw new AuthenticationError("You must be logged in.");
}
return null;
},
issueToken: async (user) => {
let token = "Bearer " + (await authFunctions.createToken(user));
let refreshToken = await authFunctions.createToken(user, 100);
return { token, refreshToken };
},
issueNewToken: async (req) => {
try {
const token = req.headers.refreshtoken;
if (token) {
const decoded = await jwt.verify(token, jwtRefreshTokenSecret);
let user = await User.findById(decoded.id);
console.log(user);
if (!user) {
throw new AuthenticationError("No user found.");
}
let tokens = await authFunctions.issueToken(user);
return { ...tokens, user };
}
} catch (err) {
throw new AuthenticationError("Invalid Refresh Token.");
}
},
createToken: async ({ id, address }, expiresIn = 60) => {
let secret = expiresIn === 60 ? jwtSecret : jwtRefreshTokenSecret;
return await jwt.sign({ id, address }, secret, { expiresIn });
},
};
module.exports = authFunctions;
The schema:
const { gql } = require("apollo-server");
const typeDefs = gql`
scalar Date
scalar MongoId
type User {
_id: MongoId!
address: String!
createdAt: Date
}
type Auth {
user: User!
token: String!
refreshToken: String!
}
input LoginInput {
address: String!
}
type Query {
refreshTokens: Auth!
}
type Mutation {
createOrGetUser(loginInput: LoginInput): Auth!
}
`;
module.exports = typeDefs;
The resolvers:
const User = require("../../models/User");
const {
issueToken,
issueNewToken,
checkSignedIn,
} = require("../../middleware/Auth");
const resolvers = {
Query: {
refreshTokens: async (root, args, { req }, info) =>
await issueNewToken(req),
},
Mutation: {
createOrGetUser: async (root, args, { req }, info) => {
try {
const existingUser = await User.findOne({
address: args.loginInput.address,
})
.populate({
path: "orders",
model: Order,
})
.exec();
if (existingUser) {
let tokens = await issueToken(existingUser);
return {
user: existingUser,
...tokens,
};
}
const user = await new User({
address: args.loginInput.address,
});
const result = await user.save();
let tokens = await issueToken(result);
return {
user,
...tokens,
};
} catch (err) {
throw err;
}
},
},
};
module.exports = resolvers;
And the App.js
import React, { Fragment } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import {
ApolloClient,
ApolloLink,
createHttpLink,
useReactiveVar,
concat,
} from "#apollo/client";
import { ApolloProvider } from "#apollo/client/react";
import { accessTokenVar, isLoggedInVar } from "./cache";
import cache from "./cache.js";
import jwtDecode from "jwt-decode";
// Styling
import "./styles/App.css";
// Components
import Routes from "./components/routing/Routes";
import Navbar from "./components/navbar/Navbar";
import { REFRESH_TOKENS } from "./queries/User";
const httpLink = createHttpLink({
uri: "/graphql",
});
const refreshTokens = () => {
return client.query({ query: REFRESH_TOKENS }).then((response) => {
console.log(response);
// Need to set the token to cache here, but the query doesn't get executed it seems
//accessTokenVar(response.data)
return;
});
};
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
let token = accessTokenVar();
if (token) {
token = token.replace("Bearer ", "");
const { exp } = jwtDecode(token);
console.log(exp);
// Refresh the token a minute early to avoid latency issues
const expirationTime = exp - 30;
if (Date.now() / 1000 >= expirationTime) {
refreshTokens(); // this keeps going on a loop
}
}
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: token ? token : "",
},
}));
return forward(operation);
});
const client = new ApolloClient({
cache,
link: concat(authMiddleware, httpLink),
connectToDevTools: true,
credentials: "include",
});
const App = () => {
const accessToken = useReactiveVar(accessTokenVar);
const isLoggedIn = useReactiveVar(isLoggedInVar);
//let isLoggedIn;
accessToken ? isLoggedInVar(true) : isLoggedInVar(false);
return (
<ApolloProvider client={client}>
<Router>
<Fragment>
<Navbar isLoggedIn={isLoggedIn} />
</Fragment>
</Router>
</ApolloProvider>
);
};
export default App;
Note: When createOrGetUser is executed the first time, the flow works. Then, I wait for 30 seconds and send a protected query, after which it gets in an endless loop.
Overall, I feel this flow is broken somehow, but I can't figure out what exactly. Would appreciate any help with this!

Next Auth: Redirect page before success login

I want make a custom login use next Auth.
What I want is, after check my email and password true, I want redirect to google auth page and after google auth true I want set the token to session and redirect to home. Is it possible to make it like that?
//This is the flow
Login -> google Auth -> Home
This is login page
//Front End
const handler = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
const res = await signIn("credentials", {
redirect: false,
data: emailRef.current?.value,
password: passwordRef.current?.value,
});
if (res?.error) {
toast.error(res?.error);
setLoading(false);
} else {
Router.replace("/");
Router.events.on("routeChangeComplete", success);
}
};
This is the API Page
//API
export default NextAuth({
session: {
jwt: true,
},
providers: [
Providers.Credentials({
async authorize(credentials: credentialsData, req: NextApiRequest) {
let client;
try {
client = await ConnectDatabase();
} catch (error) {
throw new Error("Failed connet to database.");
}
const checkEmail = await client
.db()
.collection("users")
.findOne({ email: credentials.data });
const checkPhone = await client
.db()
.collection("users")
.findOne({ phone: credentials.data });
let validData = {
password: "",
email: "",
};
if (!checkEmail && !checkPhone) {
client.close();
throw new Error("Email atau No HP tidak terdaftar.");
} else if (checkEmail) {
validData = checkEmail;
} else if (checkPhone) {
validData = checkPhone;
}
const checkPassword = await VertifyPassword(
credentials.password,
validData.password
);
if (!checkPassword) {
client.close();
throw new Error("Password Salah.");
}
client.close();
return validData;
},
}),
],
callbacks: {
async session(session) {
const data = await getSelectedUser(session.user!.email!);
session.user = data.userData;
return Promise.resolve(session);
},
},
});
In above Example I don't use google auth, so after login success it immediately redirects to home

Login to MS Graph with React and MSAL fails with no errors on console

I'm trying to use MSAL and React to login to MSGraph. I get the popup to authenticate when I call userAgentApplication.loginPopup({propt: "select_account", scopes: config.scopes})
After entering my login information, it appears that I authenticated but when I try to make a request the login popup continues to display as if I didn't authenticate already. I get no errors on the console.
I refresh the page and check localStorage and see msal.error = invalid_state_error
I'm using MSAL version v1.4.6
Here is my code
ContextualMenu.js
import { msgraph } from './actions/graphAction';
const graph = useSelector((state) => state.graph);
const userAgentApplication = new UserAgentApplication({
auth: {
clientId: config.appId,
redirectUri: config.redirectUri
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: true
}
});
const getUserProfile = async () => {
try {
let accessToken = await userAgentApplication.acquireTokenSilent({
scopes: config.scopes
});
if (accessToken) {
let user = await getUserDetails(accessToken);
let uu = {
displayName: user.displayName,
email: user.mail || user.userPrincipalName,
givenName: user.givenName,
surname: user.surname
}
dispatch(msgraph(true, uu, null));
}
} catch (err) {
console.log(err);
}
};
const msLogin = async () => {
try {
await userAgentApplication.loginPopup({
prompt: "select_account"
});
getUserProfile();
}
catch (err) {
console.log('failed', err);
}
};
const emailFiles = () => {
setRootClassName('close');
if (graph.isAuthenticated) {
checkSelectedFile();
return false;
}
msLogin();
}
After loginPopup is called it never gets to getUserProfile and doesn't error either.
Please any help is appreciated

Set interval gets clear on browser refresh in my react application

I am using the setInterval function to do an API call to get refresh token after some interval. Every time if I refresh the browser setInterval timer gets clear and starts counting from zero. My access token gets expired and the refresh token never getting a call and user logged out. Is there any way to deal with this?
useEffect(() => {
const interval = setInterval(() => {
setTokenByReSendingAuth(); //dispatch action
}, 300000);
return () => clearInterval(interval);
}, [setTokenByReSendingAuth]);
Using MERN:
Heres your dependencies:
jwt-decode: https://www.npmjs.com/package/jwt-decode
passport-jwt: http://www.passportjs.org/packages/passport-jwt/
passport: https://www.npmjs.com/package/passport
jsonwebtoken: https://www.npmjs.com/package/jsonwebtoken
bcryptjs: https://www.npmjs.com/package/bcryptjs
Create an express server.js like this:
const express = require("express");
const cors = require("cors");
const mongoose = require("mongoose");
const path = require("path");
const passport = require("passport");
const db = require("./config/.env").mongoURI; //uri in .env file
const app = express();
const port = process.env.PORT || 5000;
app.use(cors());
app.use(express.json());
mongoose.connect(db, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true,
});
const connection = mongoose.connection;
connection.once("open", () => {
console.log("MongoDB database connection established successfully");
});
const myRouter = require("./routes/example.js");
app.use(passport.initialize()); // used to attatch token to request headers
require("./config/passport")(passport);
app.use("/example", myRouter);
if (process.env.NODE_ENV === "production") {
app.use(express.static("client/build"));
app.get("*", (req, res) => {
res.sendFile(path.resolve(__dirname, "client", "build", "index.html"));
});
}
app.listen(port, () => {
console.log(`Server is running on port: ${port}`);
});
Install your dependencies:
"dependencies": {
"axios": "^0.21.1",
"bcryptjs": "^2.4.3",
"concurrently": "^5.3.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.11.8",
"passport": "^0.4.1",
"passport-jwt": "^4.0.0",
"validator": "^13.5.2",
"nodemon": "^2.0.7"
},
Add this to your package.json in whatever directory your server is in:
"devDependencies": {
"nodemon": "^2.0.7"
},
"scripts": {
"start": "node server.js",
"server": "nodemon server.js"
},
Now to the Auth part:
Make a config folder for your passport and URI:
In a .env file:
module.exports = {
mongoURI: "mongodb+srv://",
secretOrKey: "abunchofrandomcharacterscreatedwithbcrypt",
};
Make a passport.js file:
This adds the user's token to all request headers, it is automatically running since we used it in our server.js file.
const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
const mongoose = require("mongoose");
const User = mongoose.model("users");
const keys = require("./.env");
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = keys.secretOrKey;
module.exports = (passport) => {
passport.use(
new JwtStrategy(opts, (jwt_payload, done) => {
User.findById(jwt_payload.id)
.then((user) => {
if (user) {
return done(null, user);
}
return done(null, false);
})
.catch((err) => console.log(err));
})
);
};
Make a middleware folder for your backend:
Add an auth.js file:
const jwt = require("jsonwebtoken");
const config = require("../config/.env").secretOrKey;
function authUser(req, res, next) {
const authHeader = req.header("Authorization");
const token = authHeader && authHeader.split(" ")[1];
// Check for token
if (!token)
return res.status(401).json({ msg: "No token, authorization denied" });
try {
// Verify token
const decoded = jwt.verify(token, config);
// Add user from payload
req.user = decoded;
next();
} catch (e) {
res.status(400).json({ msg: "Token is not valid" });
}
}
module.exports = {
authUser,
};
This file is attached to your routes in the header, like this:
router.post("/example/get", authUser, (req, res) => {
const { reqData } = req.body; //dont ever put user ids in here
.catch((err) => {
res.status(400).json({ msg: err });
});
});
The route to login and register should look like this:
const router = require("express").Router();
var mongoose = require("mongoose");
var Schema = mongoose.Schema;
const { authUser } = require("../middleware/auth"); //used in the header of auth needed requests
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const keys = require("../config/.env");
const validateRegisterInput = require("../validation/register"); //checks and validates user register inputs
const validateLoginInput = require("../validation/login"); //checks and validates user register inputs
const User = require("../models/user");
const { Permissions } = require("../models/permissions");
//uses a middleware to validate register inputs, checks if user data exists in db, salts and hashes the password.
router.post("/register", (req, res) => {
const { errors, isValid } = validateRegisterInput(req.body);
if (!isValid) {
return res.status(400).json(errors);
}
User.findOne({ email: req.body.email }).then((user) => {
if (user) {
return res.status(400).json({ email: "Email already exists" });
} else {
const newUser = new User({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
password: req.body.password,
});
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (err, hash) => {
if (err) throw err;
newUser.password = hash;
newUser
.save()
.then((user) => res.json(user))
.catch((err) => console.log(err));
});
});
}
});
});
//login creds are req through this route, the details are compared to the db user collection, and the user data that matches the decoded password and username will be responed back through the token.
router.post("/login", (req, res) => {
const { errors, isValid } = validateLoginInput(req.body);
if (!isValid) {
return res.status(400).json(errors);
}
const email = req.body.email;
const password = req.body.password;
User.findOne({ email }).then((user) => {
if (!user) {
return res.status(404).json({ email: "Email not found" });
}
bcrypt.compare(password, user.password).then((isMatch) => {
if (isMatch) {
const payload = {
id: user.id,
firstName: user.firstName,
};
jwt.sign(
payload,
keys.secretOrKey,
{
expiresIn: 31556926, //expires in a year
},
(err, token) => {
res.json({
success: true,
token: "Bearer " + token,
});
}
);
} else {
return res
.status(400)
.json({ passwordincorrect: "Password incorrect" });
}
});
});
});
module.exports = router;
That's basically it for the backend auth routing side of things, but for the client to get there token in the browser you need to add this stuff to the client:
In your index.js add this outside a component to run on every render no matter what:
This checks to see if there's a jwttoken in the browser, it decodes it and sets the user data into the state to be used globally. It also redirects the user.
import setAuthToken from "./utils/setAuthToken";
import jwt_decode from "jwt-decode";
if (localStorage.jwtToken) {
// Set auth token header auth
const token = localStorage.jwtToken;
setAuthToken(token);
// Decode token and get user info and exp
const decoded = jwt_decode(token);
// Set user and isAuthenticated
store.dispatch(setCurrentUser(decoded)); // using redux, can easily also just use contextApi or something else
// Check for expired token
const currentTime = Date.now() / 1000; // to get in milliseconds
if (decoded.exp < currentTime) {
// Logout user
store.dispatch(logoutUser());
// Redirect to login
window.location.href = "./";
}
}
Create login, register and logout functions:
import axios from "axios";
import setAuthToken from "../../utils/setAuthToken";
import jwt_decode from "jwt-decode";
import { SET_CURRENT_USER } from "./authTypes"; //puts user data into state
import { showSnackbar } from "../inventory/inventoryActions";
export const registerUser = (userData) => (dispatch) => {
axios
.post("/users/register", userData)
.then(() => {
console.log("logged in")
})
.catch(() => {
console.log("something wrong")
});
};
export const loginUser = (userData) => (dispatch) => {
axios
.post("/users/login", userData)
.then((res) => {
const { token } = res.data;
localStorage.setItem("jwtToken", token);
setAuthToken(token);
const decoded = jwt_decode(token);
dispatch(setCurrentUser(decoded));
dispatch(showSnackbar(`Successfully signed in!`, "success", 3000));
})
.catch(() => {
console.log("somethings wrong")
});
};
export const setCurrentUser = (decoded) => { // used in loginUser
return {
type: SET_CURRENT_USER,
payload: decoded,
};
};
//removes token from localstorage
export const logoutUser = () => {
return (dispatch) => {
localStorage.removeItem("jwtToken");
setAuthToken(false);
dispatch(setCurrentUser({}));
};
};
If you have any private components you only want logged in users to access use this PrivateRoute Component wrapper:
This redirects any user not logged in to the home page
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import PropTypes from "prop-types";
const PrivateRoute = ({ component: Component, auth, ...rest }) => (
<Route
{...rest}
render={(props) =>
auth.isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect to="/" />
)
}
/>
);
PrivateRoute.propTypes = {
auth: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
auth: state.auth,
});
export default connect(mapStateToProps)(PrivateRoute);
Use it as a react-router-dom element:
<PrivateRoute exact path="/example" component={privateComponentExample} />
If you have any questions, let me know. :)

Resources