Passing JWT to UI application after Google Oauth2 login - node.js

I have created a MERN application with a separate backend and frontend. I have added support for Google Oauth2 login using passport-google-oauth20 npm package.
So I have exposed an end point in the backend as follows:
class AccountAPIs {
constructor() { }
redirectToSuccess(req, res) {
const accountServiceInst = AccountService.getInst();
let account = req.session.passport.user;
let filePath = path.join(__dirname + '../../../public/views/loginSuccess.html');
let jwt = accountServiceInst.generateJWT(account);
// how do I send this jwt to ui application
res.sendFile(filePath);
}
loadMappings() {
return {
'/api/auth': {
'/google': {
get: {
callbacks: [
passport.authenticate('google', { scope: ['profile', 'email'] })
]
},
'/callback': {
get: {
callbacks: [
passport.authenticate('google', { failureRedirect: '/api/auth/google/failed' }),
this.redirectToSuccess
]
}
},
'/success': {
get: {
callbacks: [this.successfulLogin]
}
}
}
}
};
}
}
Here is the passport setup for reference:
let verifyCallback = (accessToken, refreshToken, profile, done) => {
const accountServiceInst = AccountService.getInst();
return accountServiceInst.findOrCreate(profile)
.then(account => {
return done(null, account);
})
.catch(err => {
return done(err);
});
};
let googleStrategyInst = new GoogleStrategy({
clientID: serverConfig.auth.google.clientId,
clientSecret: serverConfig.auth.google.clientSecret,
callbackURL: 'http://localhost/api/auth/google/callback'
}, verifyCallback);
passport.use(googleStrategyInst);
In the UI application, on button click I am opening a new window which opens the '/api/auth/google' backend API. After authenticating with a google account, the window redirects to the '/api/auth/google/callback' backend API where I am able to generate a JWT. I am unsure about how to transfer this JWT to the frontend application since this is being opened in a separate window.
I know that res.cookie('jwt', jwt) is one way to do it. Please suggest the best practices here..

There are two ways to pass the token to the client :
1- you put the token into a cookie as you have mentioned
2-you pass the token in the redirect URL to the client as a parameter "CLIENT_URL/login/token", that you can extract the token in your front-end client

Related

Angular/NodeJS share frontend access token to fetch data from MS graph by backend

I have a small MEAN stack running (Angular in Frontend and NodeJS in Backend). The Frontend is protected by MSAL (#azure/msal-angular).
This part is working fine. The user gets authorized for the frontend and Angular is able to request data from MS Graph (the msal interceptor adds the token to all requests to the MS Graph and the backend):
app.module.ts
MSalModule.forRoot( new PublicClientApplication({ // MSAL Configuration
auth: {
clientId: environment.aad_client_id,
authority: 'https://login.microsoftonline.com/' + environment.aad_tenant_id + '/',
redirectUri: window.location.origin,
},
cache: {
cacheLocation : BrowserCacheLocation.LocalStorage,
storeAuthStateInCookie: isIE,
}
}), {
// MSAL Guard Configuration
interactionType: InteractionType.Redirect,
authRequest: {
scopes: ['user.read', environment.aad_scope_api]
}
}, {
// MSAL Interceptor Configuration
interactionType: InteractionType.Redirect,
protectedResourceMap: new Map([
['https://graph.microsoft.com/v1.0', ['user.read']],
[environment.apiUrl, [environment.aad_scope_api]],
])
})
After redirect from MS login I send a post request to my NodeJS Backend to establish a session.
The login route of the Backend should extract the token from the header, and send some request to the graph, to store the user details from there in the user session.
login.js
router.post('/login', (req, res) => {
if (req.session.user) {
res.json(req.session.user);
} else {
fetchUser(req, mongodb).then(result => {
req.session.user = result;
res.json(result);
}).catch(err => {
res.status(401).json(err);
})
}
});
...
async function fetchUser(token) {
try {
const token = req.headers.authorization;
request({
headers: { 'Authorization': token },
uri: 'https://graph.microsoft.com/v1.0/me',
method: 'GET'
}, { json: true }, (err, res, body) => {
if (err) { throw err; }
const obj = ...do some things
return obj;
});
} catch(err) {
throw err;
}
}
The issue is, that the token is only valid from Frontend. MS recommend the on-behalf-of-flow for that, but I'm not able to find any way to solve this. So how can I request a new token for my backend?
You can request a token for the backend to access Graph using the client credentials authentication, and set the scopes for Graph as Application Permissions on the App Registration, such as User.Read.All.
You would instead read the "oid" from the AAD access token passed from frontend to backend for discovering the user for formatting requests to Graph. Microsoft created a tutorial on implementing which you may find helpful.

Secure a GraphQL API with passport + JWT's or sessions? (with example)

To give a bit of context: I am writing an API to serve a internal CMS in React that requires Google login and a React Native app that should support SMS, email and Apple login, I am stuck on what way of authentication would be the best, I currently have an example auth flow below where a team member signs in using Google, a refresh token gets sent in a httpOnly cookie and is stored in a variable in the client, then the token can be exchanged for an accessToken, the refresh token in the cookie also has a tokenVersion which is checked before sending an accessToken which does add some extra load to the database but can be incremented if somebody got their account stolen, before any GraphQL queries / mutations are allowed, the user's token is decoded and added to the GraphQL context so I can check the roles using graphql-shield and access the user for db operations in my queries / mutations if needed
Because I am still hitting the database even if it's only one once on page / app load I wonder if this is a good approach or if I would be better off using sessions instead
// index.ts
import "./passport"
const main = () => {
const server = fastify({ logger })
const prisma = new PrismaClient()
const apolloServer = new ApolloServer({
schema: applyMiddleware(schema, permissions),
context: (request: Omit<Context, "prisma">) => ({ ...request, prisma }),
tracing: __DEV__,
})
server.register(fastifyCookie)
server.register(apolloServer.createHandler())
server.register(fastifyPassport.initialize())
server.get(
"/auth/google",
{
preValidation: fastifyPassport.authenticate("google", {
scope: ["profile", "email"],
session: false,
}),
},
// eslint-disable-next-line #typescript-eslint/no-empty-function
async () => {}
)
server.get(
"/auth/google/callback",
{
preValidation: fastifyPassport.authorize("google", { session: false }),
},
async (request, reply) => {
// Store user in database
// const user = existingOrCreatedUser
// sendRefreshToken(user, reply) < send httpOnly cookie to client
// const accessToken = createAccessToken(user)
// reply.send({ accessToken, user }) < send accessToken
}
)
server.get("/refresh_token", async (request, reply) => {
const token = request.cookies.fid
if (!token) {
return reply.send({ accessToken: "" })
}
let payload
try {
payload = verify(token, secret)
} catch {
return reply.send({ accessToken: "" })
}
const user = await prisma.user.findUnique({
where: { id: payload.userId },
})
if (!user) {
return reply.send({ accessToken: "" })
}
// Check live tokenVersion against user's one in case it was incremented
if (user.tokenVersion !== payload.tokenVersion) {
return reply.send({ accessToken: "" })
}
sendRefreshToken(user, reply)
return reply.send({ accessToken: createAccessToken(user) })
})
server.listen(port)
}
// passport.ts
import fastifyPassport from "fastify-passport"
import { OAuth2Strategy } from "passport-google-oauth"
fastifyPassport.registerUserSerializer(async (user) => user)
fastifyPassport.registerUserDeserializer(async (user) => user)
fastifyPassport.use(
new OAuth2Strategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:4000/auth/google/callback",
},
(_accessToken, _refreshToken, profile, done) => done(undefined, profile)
)
)
// permissions/index.ts
import { shield } from "graphql-shield"
import { rules } from "./rules"
export const permissions = shield({
Mutation: {
createOneShopLocation: rules.isAuthenticatedUser,
},
})
// permissions/rules.ts
import { rule } from "graphql-shield"
import { Context } from "../context"
export const rules = {
isAuthenticatedUser: rule()(async (_parent, _args, ctx: Context) => {
const authorization = ctx.request.headers.authorization
if (!authorization) {
return false
}
try {
const token = authorization.replace("Bearer", "")
const payload = verify(token, secret)
// mutative
ctx.payload = payload
return true
} catch {
return false
}
}),
}
To answer your question directly, you want to be using jwts for access and that's it. These jwts should be created tied to a user session, but you don't want to have to manage them. You want a user identity aggregator to do it.
You are better off removing most of the code to handle user login/refresh and use a user identity aggregator. You are running into common problems of the complexity when handling the user auth flow which is why these exist.
The most common is Auth0, but the price and complexity may not match your expectations. I would suggest going through the list and picking the one that best supports your use cases:
Auth0
Okta
Firebase
Cognito
Authress
Or you can check out this article which suggests a bunch of different alternatives as well as what they focus on

How to handle twitch passport.js auth code?

I'm trying to build a simple app that authenticates a twitch account and displays a users information. I'm stuck with how to send along my auth code once a user has successfully logged in.
Server-side, my code looks like this:
---auth-routes.js
// auth with twitch
router.get("/twitch", passport.authenticate("twitch", { scope: "user_read" }), (req, res) => {
res.status(200).json({message: 'Authenticating...'});
console.log('Authenticating...')
});
// redirect to home page after successful login via twitch
router.get(
"/twitch/redirect",
passport.authenticate("twitch", {
successRedirect: "/auth/twitch/redirect",
failureRedirect: "/auth/login/failed"
})
);
---config/passport-setup.js
// Override passport profile function to get user profile from Twitch API
OAuth2Strategy.prototype.userProfile = function(accessToken, done) {
var options = {
url: 'https://api.twitch.tv/helix/users',
method: 'GET',
headers: {
'Client-ID': TWITCH_ID,
'Accept': 'application/vnd.twitchtv.v5+json',
'Authorization': 'Bearer ' + accessToken
}
};
request(options, function (error, response, body) {
if (response && response.statusCode == 200) {
done(null, JSON.parse(body));
} else {
done(JSON.parse(body));
}
});
}
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(user, done) {
done(null, user);
});
passport.use('twitch', new OAuth2Strategy({
authorizationURL: 'https://id.twitch.tv/oauth2/authorize',
tokenURL: 'https://id.twitch.tv/oauth2/token',
clientID: TWITCH_ID,
clientSecret: TWITCH_SECRET,
callbackURL: TWITCH_CB,
state: true
},
function(accessToken, refreshToken, profile, done) {
profile.accessToken = accessToken;
profile.refreshToken = refreshToken;
console.log(profile);
// Securely store user profile in your DB
//User.findOrCreate(..., function(err, user) {
// done(err, user);
//});
done(null, profile);
}
))
I also have a simple profile component that displays when auth/twitch/redirect route is hit
export const AppRouter = () => {
return (
<Router>
<div>
<Route exact path='/' component={HomePage} />
<Route path='/auth/twitch/redirect' component={Profile} />
</div>
</Router>
)
}
According to the twitter documentation, you need to take the access code appended to your redirect URI and make a post request with it. I'm having trouble figuring out how and where to pull that code and send it along. Here's what they say in the documentation:
In our example, your user gets redirected to:
http://localhost/?code=394a8bc98028f39660e53025de824134fb46313
&scope=viewing_activity_read
&state=c3ab8aa609ea11e793ae92361f002671
3) On your server, get an access token by making this request:
POST https://id.twitch.tv/oauth2/token
?client_id=<your client ID>
&client_secret=<your client secret>
&code=<authorization code received above>
&grant_type=authorization_code
&redirect_uri=<your registered redirect URI>
Here is a sample request:
POST https://id.twitch.tv/oauth2/token
?client_id=uo6dggojyb8d6soh92zknwmi5ej1q2
&client_secret=nyo51xcdrerl8z9m56w9w6wg
&code=394a8bc98028f39660e53025de824134fb46313
&grant_type=authorization_code
&redirect_uri=http://localhost
Thanks for any help!
I'm struggeling with the same problem. However, I think I might help a little bit.
I also see that this is a bit old post so you might have figured it out already.
To get the hash or fragment from the URL using router, you can:
import React, { useEffect } from "react";
import { useLocation } from "react-router-dom";
const yourCallbackComponent = () => {
let location = useLocation();
useEffect(() => {
console.log(location)
}, [location])
return ()
}
I wrote this in a hurry, sorry if it's bad. But when a route is loaded you can extract information in the location object.
The expected output of the console.log should be somewhere near this:
{
key: 'ac3df4', // not with HashHistory!
pathname: '/somewhere',
search: '?access_token:jf82rjfj02f0f',
hash: '#access_tokenjf82rjfj02f0f',
state: {
[userDefined]: true
}
}
It's either in the hash or search, depending on what you get back from twitch.
Hope this helps if you didn't find a solution already.
I'm still trying to figure out the next steps myself so I can't be of any assistance there.
Let me know if you're still stuck, then I'll post my findings when I figure out how this works.

How can I implement passport with apple authentication

I have an iOS app and nodeJS backend. Currently I have implemented passport-facebook strategy. From the app I get the facebook token, and I send it to backend where I authorise the user.
// config
var FacebookTokenStrategy = require('passport-facebook-token');
const passport = require('passport')
const { facebook_client_id, facebook_client_secret } = require('../config')
passport.use(new FacebookTokenStrategy({
clientID: facebook_client_id,
clientSecret: facebook_client_secret,
}, function (accessToken, refreshToken, profile, done) {
done(null, profile)
}
));
And the middleware
const passport = require('passport')
require('../config/passport-facebook')
require('../config/passport-apple')
require('../config/passport')
const { INVALID_TOKEN, UNAUTHORIZED } = require('../config/constants')
module.exports = (req, res, next) => {
passport.authenticate(['apple','facebook-token', 'jwt'], function (err, user, info) {
if (err) {
if (err.oauthError) {
res
.status(400)
.json({ message: INVALID_TOKEN })
}
} else if (!user) {
res
.status(401)
.json({ message: UNAUTHORIZED })
} else {
req.user = user
next()
}
})(req, res, next);
}
Now I need to implement apple login. I tried using this library passport-apple
But I can not make it work. I am receiving the token from the app, send it to the back, but I only get
GET - /api/v1/shirts/?sorted%5BcreatedAt%5D=-1&filtered%5Bstate%5D=&pageNum=1&pageSize=10 - 302 - Found - 0b sent - 15 ms
I don't know if this is the correct approach. Should I get the user info from the app, send it to the backend and assign a JWT token to the created user? Or how can I do the same as I did with facebook?
After several try I find the solution thanks to this documentation https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
You need to send that in your body POST:
{
"grant_type": "authorization_code",
"code": "YOUR_CODE",
}
code:
"The authorization code received in an authorization response sent to your app. The code is single-use only and valid for five minutes. This parameter is required for authorization code validation requests." Apple Documentation

Passport & JWT & Google Strategy - Disable session & res.send() after google callback

Using: passport-google-oauth2.
I want to use JWT with Google login - for that I need to disable session and somehow pass the user model back to client.
All the examples are using google callback that magically redirect to '/'.
How do I:
1. Disable session while using passport-google-oauth2.
2. res.send() user to client after google authentication.
Feel free to suggest alternatives if I'm not on the right direction.
Manage to overcome this with some insights:
1. disable session in express - just remove the middleware of the session
// app.use(session({secret: config.secret}))
2. when using Google authentication what actually happens is that there is a redirection to google login page and if login is successful it redirect you back with the url have you provided.
This actually mean that once google call your callback you cannot do res.send(token, user) - its simply does not work (anyone can elaborate why?). So you are force to do a redirect to the client by doing res.redirect("/").
But the whole purpose is to pass the token so you can also do res.redirect("/?token=" + token).
app.get( '/auth/google/callback',
passport.authenticate('google', {
//successRedirect: '/',
failureRedirect: '/'
, session: false
}),
function(req, res) {
var token = AuthService.encode(req.user);
res.redirect("/home?token=" + token);
});
But how the client will get the user entity?
So you can also pass the user in the same way but it didn't felt right for me (passing the whole user entity in the parameter list...).
So what I did is make the client use the token and retrieve the user.
function handleNewToken(token) {
if (!token)
return;
localStorageService.set('token', token);
// Fetch activeUser
$http.get("/api/authenticate/" + token)
.then(function (result) {
setActiveUser(result.data);
});
}
Which mean another http request - This make me think that maybe I didnt get right the token concept.
Feel free to enlighten me.
Initialize passport in index.js:
app.use(passport.initialize());
In your passport.js file:
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL:
'http://localhost:3000/auth/google/redirect',
},
async (accessToken, refreshToken, profile,
callback) => {
// Extract email from profile
const email = profile.emails![0].value;
if (!email) {
throw new BadRequestError('Login failed');
}
// Check if user already exist in database
const existingUser = await User.findOne({ email
});
if (existingUser) {
// Generate JWT
const jwt = jwt.sign(
{ id: existingUser.id },
process.env.JWT_KEY,
{ expiresIn: '10m' }
);
// Update existing user
existingUser.token = jwt
await existingUser.save();
return callback(null, existingUser);
} else {
// Build a new User
const user = User.build({
email,
googleId: profile.id,
token?: undefined
});
// Generate JWT for new user
const jwt = jwt.sign(
{ id: user.id },
process.env.JWT_KEY,
{ expiresIn: '10m' }
);
// Update new user
user.token = jwt;
await auth.save();
return callback(null, auth);
}
}));
Receive this JWT in route via req.user
app.get('/google/redirect', passport.authenticate('google',
{failureRedirect: '/api/relogin', session: false}), (req, res) => {
// Fetch JWT from req.user
const jwt = req.user.token;
req.session = {jwt}
// Successful authentication, redirect home
res.status(200).redirect('/home');
}

Resources