I have implemented local as well as google login using passport.js in a mern web application. The local authentication is working fine with the frontend but I am getting errors when using the Google strategy.
Error:
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?.........' (redirected from 'http://localhost:5000/auth/google') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Error: Network Error
at createError (createError.js:16)
at XMLHttpRequest.handleError (xhr.js:84)
Issue shown by the console:
Indicate whether a cookie is intended to be set in a cross-site context by specifying its SameSite attribute.
Indicate whether to send a cookie in a cross-site request by specifying its SameSite attribute
I tested the google strategy using POSTMAN and it was working fine but when requesting from my frontend there seems to be some issue.
server.js
require("dotenv").config();
const express=require("express");
const cors=require("cors");
const mongoose=require("mongoose");
const session=require("express-session");
const passport=require("passport");
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const errorController=require("./controllers/errorController")
const app=express();
const port = process.env.PORT || 5000;
app.use(
cors({
origin: "http://localhost:3000", // <-- location of the react app were connecting to
credentials: true,
})
);
app.use(express.static("public"));
app.use(express.urlencoded({extended: true}));
app.use(express.json());
app.use(session({
secret:process.env.SECRET,
resave:false,
saveUninitialized:false,
}));
app.use(passport.initialize());
app.use(passport.session());
const User=require("./models/user.model");
passport.use(User.createStrategy());
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
passport.use(new GoogleStrategy({
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: "http://localhost:5000/auth/google/keeper"
},
function(accessToken, refreshToken, profile, cb) {
console.log(profile);
User.findOrCreate({ googleId: profile.id }, function (err, user) {
return cb(err, user);
});
}
));
mongoose.connect(process.env.MONGO_URI, {useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify:false, useCreateIndex:true});
const connection = mongoose.connection;
connection.once('open', () => {
console.log("MongoDB database connection established successfully");
});
const loginRouter=require("./routes/login");
const registerRouter=require("./routes/register");
const logoutRouter=require("./routes/logout");
const authRouter=require("./routes/auth");
app.use("/login", loginRouter);
app.use("/register", registerRouter);
app.use("/logout", logoutRouter);
app.use("/auth/google", authRouter);
app.use(errorController);
app.listen(port, function(){
console.log("server started on port 5000");
});
auth.js (handles the google login related routes)
const router=require("express").Router();
const passport=require("passport");
router.get("/", passport.authenticate("google", { scope: ["profile"] }));
router.get("/keeper",
passport.authenticate("google", { failureRedirect: "/login" }),
function(req, res) {
// Successful authentication, redirect home.
res.redirect("/");
});
module.exports=router;
Axios request from the frontend
function googleLogin(event){
Axios({
method: "GET",
withCredentials: true,
url: "http://localhost:5000/auth/google",
})
.then(function(res){
console.log(res);
})
.catch(function(err){
console.log(err);
})
event.preventDefault();
}
button that triggers the request
<button className="btn btn-danger" onClick={googleLogin}>Sign in with Google</button>
You can't make an axios call to the /auth/google URL!!
Here's my solution...
// step 1:
// onClick handler function of the button should use window.open instead
// of axios or fetch
const googleLogin = () => window.open("http://[server:port]/auth/google", "_self")
//step 2:
// on the server's redirect route add this successRedirect object with correct url.
// Remember! it's your clients root url!!!
router.get(
'/google/redirect',
passport.authenticate('google',{
successRedirect: "[your CLIENT root url/ example: http://localhost:3000]"
})
)
// step 3:
// create a new server route that will send back the user info when called after the authentication
// is completed. you can use a custom authenticate middleware to make sure that user has indeed
// been authenticated
router.get('/getUser',authenticated, (req, res)=> res.send(req.user))
// here is an example of a custom authenticate express middleware
const authenticated = (req,res,next)=>{
const customError = new Error('you are not logged in');
customError.statusCode = 401;
(!req.user) ? next(customError) : next()
}
// step 4:
// on your client's app.js component make the axios or fetch call to get the user from the
// route that you have just created. This bit could be done many different ways... your call.
const [user, setUser] = useState()
useEffect(() => {
axios.get('http://[server:port]/getUser',{withCredentials : true})
.then(response => response.data && setUser(response.data) )
},[])
Explanation....
step 1 will load your servers auth url on your browser and make the auth request.
step 2 then reload the client url on the browser when the authentication is
complete.
step 3 makes an api endpoint available to collect user info to update the react state
step 4 makes a call to the endpoint, fetches data and updates the users state.
Related
I'm currently running a webserver using the MERN stack, and I'm trying to get OAuth login working properly. However, when I click the "login with google" button, react loads the homepage (but the URL changes). Fetching the URL directly gets a 302 response from the server, but my front-end doesn't change.
Server.js
const express = require('express');
const path = require('path');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const logger = require('morgan');
const cors = require('cors');
const secure = require('express-force-https');
const passport = require('passport');
const cookieSession = require('cookie-session');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 5000;
const dbRoute = process.env.MONGODB_URI || 'NO DB ROUTE PROVIDED';
// db setup
mongoose.connect(
dbRoute,
{
useNewUrlParser: true,
useUnifiedTopology: true,
dbName: process.env.DATABASE_NAME,
}
);
let db = mongoose.connection;
db.once('open', () => console.log("Connected to the database"));
db.on('error', console.error.bind(console, "MongoDB connection error: "));
// middleware
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false })); // body parsing
app.use(bodyParser.json());
app.use(logger("dev"));
app.use(express.static(path.join(__dirname, "client", "build"))); // for serving up the clientside code
app.use(secure); // ensure that the connection is using https
app.use(cookieSession({ // cookies!
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
keys:['vcxzkjvasddkvaosd'] // yeah i'm sure that's secure enough
}));
// models
require('./models/rule');
require('./models/affix');
require('./models/user');
// passport security
require('./config/passport');
app.use(passport.initialize());
app.use(passport.session());
// routes
app.use(require('./routes'));
// The "catchall" handler: for any request that doesn't
// match one above, send back React's index.html file.
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname+'/client/build/index.html'));
});
app.listen(port);
console.log(`Server listening on ${port}`);
Route (There are a few index files in different folders, so the full path for this route it /api/user/google)
const mongoose = require('mongoose');
const passport = require('passport');
const router = require('express').Router();
const auth = require('../auth');
const User = mongoose.model('User');
router.get('/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
router.get('/google/callback',
passport.authenticate('google', { failureRedirect: '/affixes'}),
(req, res) => {
res.redirect('/?token=' + req.user.token);
}
);
Passport.js
const mongoose = require('mongoose');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
require('dotenv').config();
const User = mongoose.model('User');
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id).then((user) => {
done(null, user);
})
});
passport.use(new GoogleStrategy({
clientID: process.env.OAUTH_CLIENT_ID,
clientSecret: process.env.OAUTH_CLIENT_SECRET,
callbackURL: '/api/user/google/callback',
proxy: true
},
(accessToken, refreshToken, profile, done) => {
User.findOne({ googleId: profile.id })
.then((existingUser) => {
if (existingUser) {
done(null, existingUser);
} else {
new User({ googleId: profile.id }).save()
.then((user) => done(null, user));
}
});
}
));
Frontend login page (has a fetch button and a link button. As described above, different behavior)
import React from 'react';
import {
ComingSoon
} from '../Common';
import {
Button
} from '#material-ui/core';
const handleClick = () => {
fetch('/api/user/google')
}
export default function Login() {
return (
<>
<Button onClick={handleClick}>
Login with Google
</Button>
<button>Log in with Google</button>
</>
);
}
Update: Looks like some kind of CORS issue, although I still don't know how to fix it. Browser spits out
Access to fetch at '...' (redirected from 'http://localhost:3000/api/user/google') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Adding the requested header gives me
Access to fetch at '...' (redirected from 'http://localhost:3000/api/user/google') from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
It turns out I was quite wrong about the nature of this issue! The problem was that my fetch requests to my OAuth endpoint were calling my frontend, not my backend because the request included text/html in its Accept header. Using the react advanced proxy setup to route to the proper URI fixed the issue.
See: https://github.com/facebook/create-react-app/issues/5103 and https://github.com/facebook/create-react-app/issues/8550
I have a React app.
I also have an Express server, that using passport-saml I can authenticate against the company's PingID SSO IdP.
I would like to get the React app, to somehow call the Express app, to authenticate.
The Express Server and the React app are running on the same host.
Here's what I have:
// Express App - rendering code pulled out
const express = require('express');
var passport = require('passport');
var Strategy = require('passport-saml').Strategy;
var fs = require('fs')
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const expressSession = require('express-session');
const app = express();
const port = process.env.PORT || 4005;
passport.use('saml2', new Strategy({
path: 'http://MYSERVER:4005/assert',
entryPoint: 'https://sso.connect.pingidentity.com/sso/idp/SSO.saml2?XXXXXXXX',
issuer: 'MYAPP',
audience: 'MYAPP',
},
function(profile, cb) {
return cb(null, profile);
}));
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(obj, done) {
done(null, obj);
});
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(expressSession({
secret: '123xyz',
resave: true,
saveUninitialized: true
}));
// Initialize Passport and restore authentication state, if any, from the session.
app.use(passport.initialize());
app.use(passport.session());
app.get('/login/idp', () =>{
passport.authenticate('saml2')
console.log('Authentication called');
});
app.get('/login', () =>{
console.log('Authentication failed, try again');
});
app.post('/assert',
passport.authenticate('saml2', { failureRedirect: '/login' }),
function(req, res) {
console.log('Authentication succeeded');
console.log(req.user)
res.redirect('/');
});
app.listen(port, () => console.log(`Listening on port ${port}`));
In my React app's package.json I have:
{
...
"proxy": "http://localhost:4005/",
...
}
The outline of the toy Create React App is:
// Create React App
import React, { useState } from 'react';
import './App.css';
function App() {
const handleLogin = async e => {
e.preventDefault();
const response = await fetch('/login/idp', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
};
return (
<div className="App">
<form onSubmit={handleLogin}>
<button type="submit">Login</button>
</form>
</div>
);
}
export default App;
I can click happily on the button, and the console shows that the Express server's GET is triggered, but nothing comes back to the React client.
Is proxying the way to go? Do I have the right approach? If so, how do I get the result back from the Express app into the React app?
I have a solution, but it seems like an awful hack. However, it works, and I need to get this over the line. If anyone can suggest an improvement or alternative approach, I'd be grateful.
We start with a basic Express server (hosted at 4005), that can validate the user via Passport-SAML SSO:
const express = require('express');
const jwt = require('jsonwebtoken')
const passport = require('passport');
const Strategy = require('passport-saml').Strategy;
require('dotenv').config()
const signature = process.env.SIGNATURE
const expiresIn = process.env.EXPIRESIN
// Simplification: actually there's a db look-up here
// based on req.user in order to get just the id
// but you get the idea
const createToken = user =>
jwt.sign({ user.email }, signature, { expiresIn: expiresIn })
passport.use('saml2', new Strategy({
path: 'http://localhost:4005/assert',
entryPoint: 'https://sso.connect.pingidentity.com/sso/idp/SSO.saml2?idpid=XXXX_YOURVALUEHERE_XXXX',
issuer: 'XXXX_YOURIDHERE_XXXX',
audience: 'XXXX_YOURIDHERE_XXXX',
},
function(profile, cb) {
return cb(null, profile);
}));
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(obj, done) {
done(null, obj);
});
// Create a new Express application.
var app = express();
app.use(require('cookie-parser')());
app.use(require('body-parser').urlencoded({ extended: true }));
// Initialize Passport and restore authentication state, if any, from the
// session.
app.use(passport.initialize());
app.get('/login/idp', passport.authenticate('saml2'));
app.post('/assert',
passport.authenticate('saml2',
{ failureRedirect: `http://localhost:3000/?error=unauthenticated` } ),
function(req, res) {
const token = createToken(req.user)
res.redirect(`http://localhost:3000/signinOK?token=${token}`);
});
app.listen(4005);
Then in the React src folder, add the required setupProxy.js:
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/login',
createProxyMiddleware({
target: 'http://localhost:4005',
headers: {
"Connection": "keep-alive"
}
})
);
};
Then in the React app (hosted at port 3000) we create a simple button component for the front page:
import React from 'react'
import { Button } from '#material-ui/core'
function StartBtn() {
return (
<Button type="submit" variant="contained" color="primary" >
Login
</Button>
)
}
export default StartBtn
At this point, we stick the <StartBtn /> on the front page, and rig up a Route that responds to http://localhost:3000/signinOK?token=... by grabbing the token, using that as the value in any subsequent bearer: authentications, and redirecting to the main site.
The flow is as follows:
User loads front page, and clicks the <StartBtn/>;
Link is redirected thanks to setupProxy.js to the Express server;
Express server attempts the Passport-SAML authentication;
The result of the authentication process is a POST call from the IdP (PingID Authentication Server) to the Express server, on the /assert route.
The result either succeeds or fails, but in both cases re-directs to the React app.
In case of success, the user details are returned as JWT; or
In case of failure, an error is returned.
I'll come back to this answer, if I can find ways to improve it, or expand on the JWT stage.
I hope that someone either (a) finds this useful, or (b) invents a time-machine, goes back and posts this 3 weeks ago, so that I can save more of my remaining hair follicles. Or (c) tells me the way I should have done it.
I am using passport.js google strategy for user authentication.
I am using OAUTH2.
When I launch my server and hit API through browser, it launches google signin page.
But when I hit API from react front-end, it never launches the googles signin page.
Please find below server code,
const bodyParser = require('body-parser');
const express = require('express');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
const app = express();
const cors = require('cors');
app.use(passport.initialize());
app.use(passport.session());
app.use(cors());
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, OPTIONS, PUT, PATCH, DELETE"
);
res.setHeader(
"Access-Control-Allow-Headers",
"X-Requested-With,content-type"
);
res.setHeader("Access-Control-Allow-Credentials", true);
next();
});
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.json());
passport.use('google', new GoogleStrategy({
clientID: "clientID", -- my client id
clientSecret: "clientSecret", -- my clientsecret
callbackURL: "callbackURL" -- url
},
function (accessToken, refreshToken, profile, done) {
// User.findOrCreate({ googleId: profile.id }, function (err, user) {
// console.log(err,user);
// return done(err, user);
// });
console.log(profile);
return done(err, user);
}
));
app.get('/googleauth',
passport.authenticate('google', { scope: ['https://www.googleapis.com/auth/plus.login'] }));
passport.serializeUser(function (user, done) {
done(null, user);
})
passport.deserializeUser(function (user, done) {
done(null, user);
})
app.listen(4000, () => {
console.log("REST server started on port 4000");
});
Axios call from react code,
handleJGoogleLogin(e) {
e.preventDefault();
var current = this;
axios.get('http://localhost:4000/googleauth')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
I am stuck here, looking for some help.
Thanks in advance
The call from the react front-end is a bit different for the OAuth flow. Instead of the normal back-end call using fetch or axios
make the call like this:
window.open(`http://localhost:${backend-port}/auth/google`, "_self");
This command will make a get request to the back-end server AND open that google sign in window at the same time.
Took me a lot of time to figure this out but this is the way...
I think that Passport thing only works on the server. It is probably doing a redirect or something like that.
A few things under your passport.use add in another parameter
passport.use('google', new GoogleStrategy({
clientID: "clientID", -- my client id
clientSecret: "clientSecret", -- my clientsecret
callbackURL: "callbackURL" -- url
proxy: true
},
change:
const GoogleStrategy = require('passport-google-
auth').OAuth2Strategy;
to:
const GoogleStrategy = require('passport-google-oauth20').Strategy;
add in a second scope of profile
app.get('/googleauth',
passport.authenticate('google', { scope:
['https://www.googleapis.com/auth/plus.login','profile'] }));
Can you post your react code?
I've set up an angular/nodejs(express) application with google authentication but whenever I request to google oauth through angular app it throws a cors error but if I request directly from browser it works as it should.
Backend is running on port 3000 and angular is running on 4200.
I'm using cors package to allow all cors requests in server.js:
app.use(passport.initialize());
// Passport Config
require('./config/passport')(passport);
// Allowing all cors requests
app.use(cors());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/users', usersRouter);
app.use('/auth', authRouter);
Passport config:
passport.use(
new GoogleStrategy(
keys,
(token, refreshToken, profile, done) => {
process.nextTick(() => {
User.findOne({ email: email })
.then(user => {
if (user) {
return done(null, user);
}
})
.catch(err => console.log(err));
});
}
)
);
This is the google authentication route:
router.get('/google', passport.authenticate('google', {
scope: ['profile', 'email'],
session: false
}),
(req, res) => {
// Create JWT payload
const payload = {
id: req.user.id,
email: req.user.email
};
jwt.sign(payload, secret, expires, (err, token) => {
res.json(token);
});
}
);
And here is angular request:
googleAuth() {
return this.http.get<any>('http://localhost:3000/auth/google').pipe(
map(user => {
if (user) {
localStorage.setItem(
'currentUser',
JSON.stringify(user)
);
}
return user;
})
);
}
Error in chrome console:
Failed to load "https://accounts.google.com/o/oauth2/v2/auth?response_type=code...": No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access
I've also authorized JavaScript origins in google developers console:
origins
I'm pretty new to angular and nodejs and I have read all the similar questions but couldn't find a workaround to this problem.
Try out this method of directly calling the URL: ( Pardon if I missed something syntactically, but basically this is the idea )
googleAuth(){
window.open('/google',"mywindow","location=1,status=1,scrollbars=1, width=800,height=800");
let listener = window.addEventListener('message', (message) => {
//message will contain google user and details
});
}
and in server you have passport strategy set I assume, Now
router.get('/google', passport.authenticate('google', {
scope: ['profile', 'email'],
session: false } ))
router.get('/google/callback',
passport.authenticate('google', { failureRedirect: '/auth/fail' }),
function(req, res) {
var responseHTML = '<html><head><title>Main</title></head><body></body><script>res = %value%; window.opener.postMessage(res, "*");window.close();</script></html>'
responseHTML = responseHTML.replace('%value%', JSON.stringify({
user: req.user
}));
res.status(200).send(responseHTML);
});
I'm trying to add auth0 to my web app, I've followed their tutorials and other tutorials on the web, including creating account/client and everything else, but I keep getting the usual white page loading screen and after several minutes I receive this error:
ERR_EMPTY_RESPONSE
These are parts of my code:
app.js
...
var cookieParser = require('cookie-parser');
var session = require('express-session');
var passport = require('passport');
var Auth0Strategy = require('passport-auth0');
...
// Configure Passport to use Auth0
var strategy = new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL: process.env.AUTH0_CALLBACK_URL
},
(accessToken, refreshToken, extraParams, profile, done) => {
return done(null, profile);
}
);
passport.use(strategy);
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(user, done) {
done(null, user);
});
...
app.use(cookieParser());
app.use(
session(
{
secret: uuid(),
resave: false,
saveUninitialized: false
}
)
);
app.use(passport.initialize());
app.use(passport.session());
...
app.get('/', routes.index);
app.get('/home', routes.home);
...
http.createServer(app).listen(app.get('port'), function(){
console.log('Server listening on port: ' + app.get('port'));
});
module.exports = app;
index.js
exports.home = function(req, res){
res.render('home', { title: ' homepage ' });
};
exports.index = function(req, res){
var express = require('express');
var passport = require('passport');
var router = express.Router();
var env = {
AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
AUTH0_DOMAIN: process.env.AUTH0_DOMAIN,
AUTH0_CALLBACK_URL: process.env.AUTH0_CALLBACK_URL
};
// GET home page.
router.get('/', function(req, res, next) {
res.render('home', { title: ' homepage ' });
});
// Perform the login
router.get(
'/login',
passport.authenticate('auth0', {
clientID: env.AUTH0_CLIENT_ID,
domain: env.AUTH0_DOMAIN,
redirectUri: env.AUTH0_CALLBACK_URL,
audience: 'https://' + env.AUTH0_DOMAIN + '/userinfo',
responseType: 'code',
scope: 'openid'
}),
function(req, res) {
res.redirect('/');
}
);
// Perform session logout and redirect to homepage
router.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
});
// Perform the final stage of authentication and redirect to '/home'
router.get(
'/callback',
passport.authenticate('auth0', {
failureRedirect: '/'
}),
function(req, res) {
res.redirect(req.session.returnTo || '/home');
}
);
}
There are some parts that are not clear to me or on which I would like to have a confirmation:
1) the callback URL must be my homepage (180.180.180.180/home) or the real first page (180.180.180.180)? Which one should be included in the auth0 dashboard?
2) In the router, should I also specify the / login and / logout fields or should these be managed directly by the auth0 API?
Sorry for my ignorance but it's days I have this problem, I do not understand if it's an authorization error with the auth0 account or something else.
I have the credentials in a .env file, but they should not be the problem, as I can access other data in them to connect to my MySQL database.
As per the documentation of auth0 The callback URL is not necessarily the same URL to which you want users redirected after authentication.
redirect_uri field is used as a callback URL. Auth0 invokes callback URLs after the authentication process and are where your application gets routed.
You can redirect the user to a non callback URL after authenticating the user and storing the same url in web storage.
On app.get(/login)...>authenticated>>landing page>>stores the access tokens
So post authentication it should login to your landing page (home).
On app.get(/logout), you can clear the access tokens or make it available for desired time and let it get expired after certain time.