I'm starting to work on an express API using graphql with apollo-server-express and graphql-tools. My register user process steps are:
User submit user name, email and password.
Server send an email to user by Mailgun with unique link generated by uuid.
User follow the link to verify the registration.
But I'm in struggle at how to bind the mutation in the resolver. See snippets:
server.js
const buildOptions = async (req, res, done) => {
const user = await authenticate(req, mongo.Users)
return {
schema,
context: {
dataloaders: buildDataloaders(mongo),
mongo,
user
},
}
done()
}
// JWT setting
app.use('/graphAPI',
jwt({
secret: JWT_SECRET,
credentialsRequired: false,
}),
graphqlExpress(buildOptions),
res => data => res.send(JSON.stringify(data))
)
Mutation on resolver
signupUser: async (root, data, {mongo: { Users }}) => {
// Check existed accounts,
// if account is not exist, assign new account
const existed = await Users.findOne({email: data.email})
if (!existed) {
// create a token for sending email
const registrationToken = {
token: uuid.v4(),
created_at: new Date(),
expireAfterSeconds: 3600000 * 6 // half day
}
const newUser = {
name: data.name,
email: data.email,
password: await bcrypt.hash(data.password, 10),
created_at: new Date(),
verification_token: registrationToken,
is_verified: false,
}
const response = await Users.insert(newUser)
// send and email to user
await verifyEmail(newUser)
return Object.assign({id: response.insertedIds[0]}, newUser)
}
// Throw error when account existed
const error = new Error('Email existed')
error.status = 409
throw error
},
// VERIFY USER
// Set verify to true (after user click on the link)
// Add user to mailist
verifiedUser: async (root, data, {mongo: { Users }}) => {
await Users.updateOne(
{ email: data.email },
{
set: {is_verified: true},
unset: {verification_token: {token: ''}}
}
)
},
route config
routes.get('/verify?:token', (req, res, next) => {
res.render('verified', {title: 'Success'})
})
the route config is where I stuck, because the object is passed to all resolvers via the context inside graphqlExpress
Any one help me out or suggest for me any articles related. Thanks so much.
You will need 3 graphql endpoints and 1 apollo http endpoint for proper workflow.
Optionally you can combine 3 graphql endpoints in one, but then it will be a one big function with a lot of different responsibilities.
1# graphql endpoint: changepass-request
expects email param
check if user with such email found in db:
generate code
save it in the local account node
send code to the user email with http link to confirm code:
http://yoursite.com/auth/verify?code=1234
return redirect_uri: http://yoursite.com/auth/confirm-code
for UI page with prompt for confirmation code
2# graphql endpoint: changepass-confirm
expects code param:
if user with such code found in db, return redirect_uri to UI page with prompt for new pass with confirmation code in params: http://yoursite.com/auth/change-pass?code=1234
3# graphql endpoint: changepass-complete
expects code and new pass:
hash new password
search in db for local account with such code
3a. if not found:
return error with redirect_uri to login page:
http://yoursite.com/auth?success=false&message="Confirmation code is not correct, try again."
3b. if found:
change password for new, return success status with redirect_uri to login page:
http://yoursite.com/auth?success=true&message="ok"
4# apollo HTTP endpoint: http://yoursite.com/auth/verify?code=1234
if no code provided:
redirect to UI registration page with error message in params:
http://yoursite.com/auth?success=false&message="Confirmation code is not correct, try again."
if code provided: search in db for local account with such code
1a. if user not found:
redirect to reg ui with err mess in params:
http://yoursite.com/auth?success=false&message="Confirmation code is not correct, try again."
1.b if user found:
redirect to ui page with new password prompt and attach new code to params
I didn't put any code above, so you can use this workflow in other auth scenarios.
It seems like rather than utilizing the verifiedUser endpoint, it would be simpler to just keep that logic inside the controller for the /verify route. Something like:
routes.get('/verify?:token', (req, res) => {
Users.updateOne(
{ verification_token: { token } },
{
$set: {is_verified: true},
$unset: {verification_token: {token: ''}}
},
(err, data) => {
const status = err ? 'Failure' : 'Success'
res.render('verified', {title: status})
}
)
})
Related
I am struggling with getting Google OAuth to work with my Express/React application whilst using Passport.js. I am using JWTs, not sessions.
In my React webapp client, I have a "login with Google" button that calls my backend API /auth/google/ with the following route setup in Express:
router.get('auth/google', passport.authenticate('google', {session: false, scope: ['email','profile']}) );
My Passport.js google strategy is:
const googleStrategy = new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:3000/api/v1/auth/google/callback",
passReqToCallback : true
},
async (request, accessToken, refreshToken, profile, done) => {
try {
console.log('profile', profile);// ** CORRECT USER PRINTED **
let existingUser = await User.findOne({ 'google.id': profile.id });
// if user exists return the user
if (existingUser) {
console.log('Found existing user...');
return done(null, existingUser);
}
// if user does not exist create a new user
const newUser = new User({
method: 'google',
googleId: profile.id,
profileImage: profile.photos[0].value,
firstName: profile.name.givenName,
lastName: profile.name.familyName,
shortName: profile.displayName,
});
await newUser.save();
return done(null, newUser);
} catch (error) {
return done(error, false)
}
}
);
My Google developer dashboard is setup to call the following URL in my Express API backend upon successful authentication: /auth/google/callback
My Express route for this is defined as: router.get('auth/google/callback', passport.authenticate('google', {session: false}), authController.googleAuthCallback);
My Express googleAuthCallback function is defined as:
exports.googleAuthCallback = async (req, res) => {
console.log(req.user) // ** WRONG USER PRINTED HERE ** different from above user printed in google strategy
}
The strange this is when I console.log the profile variable in my googleStrategy, I get the right user profile information for the account from Google. This means the authentication vis a vis Google is fine. However, this same account is NOT being provided to my /auth/google/callback endpoint in the req.user object at that location. It is an entirely different account (it is the first value from my database of Users, which is authenticated using local authentication).
How do I get the user object back to my Express callback endpoint that I supplied to Google in the developer console as the authorized redirect URI?
As a general question, what happens after the strategy calls return done(null, existingUser);? I have no callback in the /auth/google route after the passport.authenticate() middleware is called so what happens next?
I am using "passport-google-oauth20": "^2.0.0"
My let existingUser = await User.findOne({ 'google.id': profile.id });
line was incorrect and was essentially returning no user. Mongoose does not complain and hence the strategy was just returning the first user from my database rather than the authenticated google user.
I am using a React/Next.Js Frontend and am trying to implement authentication with the Oauth2 strategy with Google.
I am very confused by the process.
Currently on the client, I have a Google sign in component that has a Client ID with in it and can retrieve an access token.
<GoogleLogin
clientId="myclientid"
buttonText="Login"
onSuccess={userLogin}
onFailure={userLogin}
cookiePolicy={'single_host_origin'}
/>
I then have a function, which on success sends a post message to my backend with an access token, such as this:
export function googleAuthenticate(accessToken : string) : any{
axios({
method: 'post',
url: "http://localhost:4000/auth/google",
data: {
accessToken: accessToken
}
})
.then(res => {
console.log(res);
})
.catch(err => {
console.log("Failure!");
console.log(err);
})
};
On the backend I am using passport, and the routes look like this:
import express from 'express';
import passport from 'passport';
import Logger from '../logger/index';
const router = express.Router();
export function isAuthenticated(req:express.Request, res:express.Response, next : any) {
return req.isAuthenticated() ?
next() :
res.sendStatus(401);
}
router.get('/fail', (_req:express.Request, res:express.Response) => {
res.json({ loginFailed: true });
});
router.post('/google', passport.authenticate('google', { scope: ['profile']}), (_req:express.Request, _res:express.Response) => {
Logger.info("GET Request at Google Authentication endpoint received.");
});
router.get(
'/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(_req:express.Request, res:express.Response) => {
res.redirect('/graphql');
}
);
export default router;
My passport module looks like this:
module.exports = function(passport : any, GoogleStrategy : any){
passport.use(new GoogleStrategy({
clientID: config.google.client_id,
clientSecret: config.google.client_secret,
callbackURL: config.google.redirect_url
},
function(accessToken : string, profile : Profile, refreshToken : string, cb : any) {
return cb(null, {
id: profile.googleId,
username: profile.email,
image: profile.imageUrl,
firstName: profile.givenName,
surname: profile.familyName,
accessToken: accessToken,
refreshToken: refreshToken
})
}
));
}
Since Next.js is a server side rendered, I am not able to use save a token. I understand I have to use a cookie. But how does this work? I cannot redirect the client browser from the express backend.
Currently I'm just seeing these 2 errors:
OPTIONS https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2localhost:3000%2Fdashboard&scope=profile&client_id=687602672235-l0uocpfchbjp34j1jjlv8tqv7jadb8og.apps.googleusercontent.com 405
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fbackoffice.dev.myos.co%2Fdashboard&scope=profile&client_id=687602672235-l0uocpfchbjp34j1jjlv8tqv7jadb8og.apps.googleusercontent.com' (redirected from 'http://localhost:4000/auth/google') from origin 'null' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Firstly i think google auth will not work on localhost.
If i understand correctly in your serverside logic you can easily save your token as a cookie and then read them in the client.
Not sure with passport, but you can do something similar to this :
(my app is working with an implementation of this code)
frontend :
<GoogleLogin
clientId="myclientid"
buttonText="Login"
onSuccess={userLogin}
onFailure={userLogin}
cookiePolicy={'single_host_origin'}
/>
userLogin:
async userLogin(response){
var url = '/google-login/'+response.tokenObj.id_token
fetch(url).then(/* i will handle response*/)
}
Then in the backend you can use google-auth-library to login or register.
server.js:
const {OAuth2Client} = require('google-auth-library');
const GOOGLEID = "mygoogleid.apps.googleusercontent.com"
const client = new OAuth2Client(GOOGLEID);
var cookieParser = require('cookie-parser')
async function verify(userToken) {
const ticket = await client.verifyIdToken({
idToken: userToken,
audience: "clientid.apps.googleusercontent.com", // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
const payload = ticket.getPayload();
const userid = payload['sub'];
return payload
// If request specified a G Suite domain:
//const domain = payload['hd'];
}
In server.js a route similar to this :
server.get('/google-login/:token',(req,res) => {
const userToken = req.params.token
var result = verify(userToken).then(function(result){
var userName = result.given_name
var userSurname = result.family_name
var userEmail = result.email
/*
Now user is authenticated i can send to the frontend
user info or user token o save the token to session
*/
}).catch(function(err){
// error handling
})
})
You could use NextAuth.js to handle this for you.
In order to test localhost you should use ngrok to expose your localhost server to the web and configure the given url in google platform
I am currently trying to convert a VueJS page to NuxtJS with VueJS. Unfortunately I have some problems with authenticating the user and I can't find a solution in Google. I only use Nuxt for the client. The API is completely separate in express and works with the existing VueJS site.
In Nuxt I send now with the Auth module a request with username and password to my express Server/Api. The Api receives the data, checks it, and finds the account in MongoDB. This works exactly as it should. Or as I think it should. Now I take the user object and generate the jwt from it. I can debug everything up to here and it works.
Now I probably just don't know how to keep debugging it. I send an answer with res.json(user, token) back to the Nuxt client (code follows below). As I said, in my current VueJS page I can handle this as well. Also in the Nuxt page I see the answer in the dev console and to my knowledge the answer fits.
Now some code.
The login part on the express Api:
const User = require('../models/User')
const jwt = require('jsonwebtoken')
const config = require('../config/config')
function jwtSignUser(user){
const ONE_YEAR = 60 * 60 * 24 * 365
return jwt.sign(user,config.authentication.jwtSecret, {
expiresIn: ONE_YEAR
})
}
module.exports = {
async login (req, res){
console.log(req.body)
try{
const {username, password} = req.body
const user = await User.findOne({
username: username
})
if(!user){
return res.status(403).send({
error: `The login information was incorrect.`
})
}
const isPasswordValid = await user.comparePassword(password)
if(!isPasswordValid) {
return res.status(403).send({
error: `The login information was incorrect.`
})
}
const userJson = user.toJSON()
res.json({
user: userJson,
token: jwtSignUser(userJson)
})
} catch (err) {
console.log(err)
res.status(500).send({
error: `An error has occured trying to log in.`
})
}
}
}
nuxt.config.js:
auth: {
strategies: {
local: {
endpoints: {
login: {url: '/login', method: 'post' },
user: {url: '/user', method: 'get' },
logout: false,
}
}
},
redirect: {
login: '/profile',
logout: '/',
user: '/profile',
callback:'/'
}
}
even tried it with nearly any possible "propertyName".
and, last but not least, the method on my login.vue:
async login() {
try {
console.log('Logging in...')
await this.$auth.loginWith('local', {
data: {
"username": this.username,
"password": this.password
}
}).catch(e => {
console.log('Failed Logging In');
})
if (this.$auth.loggedIn) {
console.log('Successfully Logged In');
}
}catch (e) {
console.log('Username or Password wrong');
console.log('Error: ', e);
}
}
What I really don't understand here... I always get "Loggin in..." displayed in the console. None of the error messages.
I get 4 new entries in the "Network" Tag in Chrome Dev Tools every time I make a request (press the Login Button). Two times "login" and directly afterwards two times "user".
The first "login" entry is as follow (in the General Headers):
Request URL: http://localhost:3001/login
Request Method: OPTIONS
Status Code: 204 No Content
Remote Address: [::1]:3001
Referrer Policy: no-referrer-when-downgrade
The first "user" entry:
Request URL: http://localhost:3001/user
Request Method: OPTIONS
Status Code: 204 No Content
Remote Address: [::1]:3001
Referrer Policy: no-referrer-when-downgrade
Both without any Response.
The second login entry:
Request URL: http://localhost:3001/login
Request Method: POST
Status Code: 200 OK
Remote Address: [::1]:3001
Referrer Policy: no-referrer-when-downgrade
and the Response is the object with the token and the user object.
The second user entry:
Request URL: http://localhost:3001/user
Request Method: GET
Status Code: 200 OK
Remote Address: [::1]:3001
Referrer Policy: no-referrer-when-downgrade
and the Response is the user object.
I think for the login should only the login request be relevant, or I'm wrong? And the user request works because the client has asked for the user route and the user route, always send the answer with the actual user object in my Express API.
Because I think, the problem is in the login response? Here some screenshots from the Network Tab in Chrome Dev Tools with the Request/Response for login.
First login request without response
Second login request
Response to second login request
Do I have to do something with my Vuex Store? I never found any configured Vuex Stores in examples for using the Auth Module while using google so I thougt I do not have to change here anything.
Thats my Vuex Store (Vue Dev Tools in Chrome) after trying to login without success:
{"navbar":false,"token":null,"user":null,"isUserLoggedIn":false,"access":false,"auth":{"user":"__vue_devtool_undefined__","loggedIn":false,"strategy":"local","busy":false},"feedType":"popular"}
There is also some logic I use for my actual VueJS site. I will remove that when the Auth Module is working.
Asked by #imreBoersma :
My /user endpoint on Express looks like:
app.get('/user',
isAuthenticated,
UsersController.getUser)
I first check if the User is authenticated:
const passport = require('passport')
module.exports = function (req, res, next) {
passport.authenticate('jwt', function (err, user) {
if(err || !user) {
res.status(403).send({
error: 'You are not authorized to do this.'
})
} else {
req.user = user
next()
}
})(req, res, next)
}
After that I search the User document in MongoDB and send the document to the client:
const User = require('../models/User')
module.exports = {
[...]
getUser (req, res) {
User.findById(req.user._id, function (error, user){
if (error) { console.error(error); }
res.send(user)
})
}
[...]
}
Feel free to ask for more information.
I think I can answer my own question.
I searched the whole time for an error regarding to my api response.
The problem was the "propertyName" on user endpoint in the nuxt.config.js.
It is set to "user" as default. When I set it to "propertyName: false", than everything works as it should.
auth: {
strategies: {
local: {
endpoints: {
login: {url: '/login', method: 'post', propertyName: 'token' },
user: {url: '/user', method: 'get', propertyName: false },
logout: false,
}
}
}
},
I'm working in an application which uses a REST api using the MEAN stack and Passport JS to manage the authentication.
The authentication, we use JTW tokens for the communication between the backend and frontend. The token is generated based on local username and passwords.
Now I want to 'add' (authorize) the user's google account to the profile to use with google calendar API. (using this-> https://github.com/wanasit/google-calendar)
I've already have managed to send the user to the google authorization page, and get the token back from it. The problem is that when the user gets redirected to the page, it looses the JWT token where I check the user for the request.
Is there any other way to get the current logged in user, or to pass some custom callback authorization header/param when calling the authorize method?
auth.js:
var googleParams = {
clientID: config.auth.google.clientID,
clientSecret: config.auth.google.clientSecret,
callbackURL: config.auth.google.callbackURL
}
var googleStrategy = new GoogleStrategy(googleParams, function (token, refreshToken, profile, done) {
profile.token = token;
return done(null, profile);
});
routes:
rotas.get(
'/google',
auth.authenticate(), // will check the current user
auth.isLoggedIn, // make sure the user is really logged in
auth.authorize('google', { scope: googleScope, passReqToCallback: true }) // redirects to google to get the token
);
rotas.get('/callback/google',
auth.authorize('google', { scope: googleScope, passReqToCallback: true })
auth.authRedirect()
);
the auth.authRedirect() function above is the closest solution I've found. It's a Express middleware wich redirects the user to a known route in the frontend where the user IS authenticated... but then I would not be able to fetch all his Google profile and information i need...
You have to be sure the app.use(session) its been called before any route.
...
app.use(session({
secret: 'secret'
}))
app.use(passport.initialize())
app.use(passport.session())
...
rotas.get(
'/google',
auth.authenticate(), // will check the current user
auth.isLoggedIn, // make sure the user is really logged in
auth.authorize('google', { scope: googleScope, passReqToCallback: true }) // redirects to google to get the token
);
rotas.get('/callback/google',
auth.authorize('google', { scope: googleScope, passReqToCallback: true })
auth.authRedirect()
);
Your req.user won't be undefined in this case.
If it doen't work right way, I can put my whole code that I've created here.
Hope it help you! :)
So what I ended up doing was:
Authenticate the user making the request via JWT access_token
Get the user's ID and set it to the state option's property
The user is redirected to the google authorization page and choose the account (s)he wants to connect
(S)He gets redirected to my callback url with the state query param having the user's id
Now I just have to get that id, search the user in the database, and set the data I need from req.account which contains the user's openid profile.
var googleScope = ['openid', 'email', 'https://www.googleapis.com/auth/calendar'];
routes.get(
'/google',
auth.authenticate(),
auth.isLoggedIn,
function (req, res, next) {
var _id = '' + req.user._id; // convert to String... _id is an mongoose object
return auth.authorize('google', { session: false, scope: googleScope, passReqToCallback: true, state: _id })(req, res, next)
}
);
routes.get('/callback/google',
function (req, res, next) {
auth.authorize('google', { session: false, scope: googleScope, passReqToCallback: true })(req, res, next);
},
auth.saveUserData()
);
saveUserData= function () {
return function (req, res, next) {
if (req.query.state) {
var _id = req.query.state;
User.findOne({ _id, deleted: false, active: true })
.exec(function (err, user) {
if (err) {
res.send(err);
}
if (user) {
user.auth.google = {
id: req.account.id,
token: req.account.token,
email: (req.account.emails.length ? req.account.emails[0].value : null),
name: req.account.displayName
}
user.save(function (err, data) {
if (err) {
res.send(err);
} else {
res.redirect('/')
}
})
} else {
res.sendStatus(401);
}
})
} else {
res.sendStatus(400)
}
}
I've got a relatively unique use-case here and I can't find the perfect solution.
I have a website that has user log-in via facebook and linkedin, which works flawlessly via passportjs.
However I also need to be able to login via an offsite API which is not OAuth but rather offers a standard JSON response.
I currently use angular to hit the API and return the user object but then I have to store that in sessionStorage to persist the log in. This isn't ideal as it means there is a server session and a local session. To log out I have to manually clear the sessionStorage and this hit a log out path for the server.
What I want to do is create a local log in path on the ExpressJS website and the route then hits the API to log in and then the user is stored in the server session.
In the end I hacked the local strategy.
The key was adding passReqToCallback so I could manipulate the request.
This adds the response to req.user and starts a session.
Strategy:
var passport = require('passport'),
LocalStrategy = require('passport-local').Strategy,
request = require('request')
module.exports = function() {
passport.use(new LocalStrategy({
passReqToCallback: true
},
function(req, username, password, done) {
request.post({
url: 'http://api/',
form: {
username: username,
password: password
},
json: true
}, function(err, httpResponse, body) {
if (err || !body.success) done();
else done(null, body);
});
}
));
};
Route:
app.post('/auth/local',
passport.authenticate('local', {
failureRedirect: '/fail'
}),
function(req, res) {
res.redirect('/success');
});