I use NestJS with passport for my authentication via Google. Unfortunately, I have a problem with req.user as I can get it only under the callback route, but not anywhere else.
So the question is: How to add req.user to all other routes within the module?
To demonstrate it better: In my Google.strategy.ts I handle the req and all - and of course I have the access to the google's profile.
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(private readonly authService: AuthService) {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET_KEY,
callbackURL: '/api/v1/auth/google/callback',
scope: ['email', 'profile'],
});
}
async validate(accessToken, refreshToken, profile, done) {
const user = await this.authService.verifyUser(profile.id);
if (user) {
done(null, user);
console.log('user already exists');
} else {
const user = this.authService.save({
googleId: profile.id,
displayName: profile.displayName,
});
done(null, user);
console.log('user created');
}
}
}
Here lays the problem though, after the validation of the request I can access req.user ONLY in the callback route.
#Controller('/api/v1/auth/google/')
export class AuthController {
constructor(private readonly authService: AuthService, private readonly googleStrategy: GoogleStrategy) {
}
#Get('')
#UseGuards(AuthGuard('google'))
// eslint-disable-next-line #typescript-eslint/no-empty-function
async googleAuth(#Req() req) {}
#Get('callback')
#UseGuards(AuthGuard('google'))
async googleAuthRedirect(#Res() res, #Req() req) {
//HERE IT WORKS AND I CAN ACCESS REQ.USER WITH ALL THE DETAILS
console.log(req.user)
}
#Get('login')
async info(#Req() req, #Res() res) {
//HERE I GET 'UNDEFINED'
console.log(req.user)
}
}
I found this problem today when I was actually trying to grab the data from the callback route, but... I cannot!
I always get:
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2...' (redirected from 'http://localhost:4000/api/v1/auth/google/callback') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
I tried to enableCors, add options, headers... literally everything, but I'm still getting this error, so I wanted to use a different route with the req.user (and with plain express.js of course it worked). Any ideas, guys?
Related
I'm implementing facebook-auth using https://www.npmjs.com/package/passport and https://www.npmjs.com/package/passport-facebook-token npm packages
this is my code in the passport config file:
passport.use(
new FacebookTokenStrategy(
{
clientID: config.FACEBOOK_CLIENT_ID,
clientSecret: config.FACEBOOK_CLIENT_SECRET,
fbGraphVersion: 'v3.0',
},
(accessToken, refreshToken, profile, done) => {
User.findOne(
{ 'facebookProvider.id': profile.id },
(error: any, user: any) => {
if (!user) {
const newUser = new User({
name: profile.displayName,
email: profile.emails[0].value,
facebookProvider: { id: profile.id, token: accessToken },
imageProfile: profile.photos[0].value,
});
newUser.save((error, result) => {
if (error) {
return done(error);
}
return done(null, result);
});
}
return done(error, user);
}
);
}
)
);
and below is my route handler:
const authRouter = express.Router();
authRouter
.route('/facebook')
.get(
passport.authenticate('facebook-token'),
async (req: Request, res: Response) => {
if (req.user) {
const accessToken: String = jwt.sign(
{ user: req.user },
config.JWT_SECRET_KEY,
{
expiresIn: config.TOKEN_LIFE_TIME,
}
);
return sendSuccesResponse(res, 200, accessToken);
}
return sendErrorResponse(res, 400, 'Something went wrong');
}
);
export default authRouter;
also, I initialized the passport in the index.ts file which is the entry point of API and imported the passport config file:
import './config/passport';
.
.
.
app.use(passport.initialize());
app.use('/api/v1/oauth2', authRouter);
Moreover, I got the token from Facebook and when I request my API with the provided token, Facebook can authenticate and return the user profile but after authentication and creating a new user or if the user already exists in DB, then it does not go to the route handler where I want to generate JWT token for the user and return to the user.
I got the following error in postman:
500 internal server error
After a lot of struggling, I found that I must provide a second parameter { session: false } for passport.authenticate function.
The below code is working just fine:
passport.authenticate('facebook-token', { session: false })
This is very odd behavior. Everytime i get a new credentials, I click on the link, I get the consent screen but after that consent screen does not show anymore but request to google server and and response to callback url is happening behind the scene. I logout user "/auth/logout", I also delete all the cookies stored manually, When I clieck on the button, I am automatically signed in again.
I belive there is nothing wrong with coding and I checked the consent screen of console.developers but there is nothing related to this issue.
this is a typescript project. here is the routes
import passport from "passport";
import { Application, Request, Response } from "express";
export const authRoutes = (app: Application) => {
app.get(
"/auth/google",
passport.authenticate("google", {
scope: ["profile", "email"],
})
);
app.get(
"/auth/google/callback",
passport.authenticate("google"),
(req: Request, res: Response) => {
res.redirect("/");
}
);
// passport sees the code here and it knows that it has to use the code to get user
app.get("/auth/current_user", (req: Request, res: Response) => {
res.send(req.user);
});
app.get("/auth/logout", (req: Request, res: Response) => {
req.logout();
res.json({ user: req.user });
});
};
here is the passport setting:
import GoogleStrategy from "passport-google-oauth20";
import passport from "passport";
import { User, UserDoc } from "../database/models/User";
// passport sets up the cookie and stuffs the user's database id not the googleId.
passport.serializeUser(
(user: UserDoc, done: (err: any, user: UserDoc) => void) => {
done(null, user.id);
}
);
passport.deserializeUser(
async (id: string, done: (err: any, user: UserDoc) => void) => {
const user = await User.findById(id);
if (user) {
done(null, user);
}
}
);
passport.use(
new GoogleStrategy.Strategy(
{
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: "http://localhost:4500/auth/google/callback",
proxy: true,
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({ googleId: profile.id });
if (existingUser) {
done(undefined, existingUser);
}
const user = await new User({ googleId: profile.id }).save();
done(undefined, user);
}
)
);
What you're seeing is the desired behavior - once the user has granted authorization to the scopes you're asking for (that are shown in the consent screen), Google doesn't need to get the user's consent again. It keeps track of that.
Users can review and revoke access to your app (mobile or web-based) at https://myaccount.google.com/permissions. This is particularly useful during development so you don't have to constantly create new accounts to test with.
I am trying to use Passport for SSO. My problem is that when I log in with any of the options everything is fine, except the data saving... I think the functions in the strategy files are not called (the log is not working neither).
For example the Google strategy:
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(private userService: UserService) {
super({
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:4200',
scope: ['email', 'profile'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
try {
console.log(profile);
const user = profile;
this.userService.FindOrCreate(profile);
done(null, user);
} catch (err) {
done(err, null);
}
}
}
Controller:
#Get('google')
#UseGuards(AuthGuard('google'))
async twitterauth(#Req() req) {
return await this.authService.login(req.user);
}
AuthService:
#Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor(
private userService: UserService,
private readonly jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<User> {
const user: User = await this.userService.findOne({
where: { email },
});
if (!user) {
return null;
} else {
if (await bcrypt.compare(password, user.password)) {
return user;
} else {
this.logger.error('Password is incorrect.');
return null;
}
}
}
async login(user: any) {
const payload = { email: user.email, role: user.role };
return {
// eslint-disable-next-line #typescript-eslint/camelcase
access_token: this.jwtService.sign(payload),
};
}
}
The other strategies (fb, linkedin, instagram, github) are quite the same and the problem is the same.
The problem, as found in chat, was that the callback that Google was calling to in the OAuth flow, was not a part of the same server, and as such, the NestJS server could not react to the incoming data, hence why the validate was never called.
That callback route needs to point to your NestJS server so that it can handle the saving logic for the database,OR the angular applications needs to re-route the return to it back to the NestJS server. Either way, your validations aren't being called because your Nest server never gets the callback with all the sensitive information from Google
More than likely, it will be better to have the callback pointed at your server so that the data is formatted as Passport is expected.
I am currently stuck to handle a google oAuth login in a vue app which is connecting to my own node express api server.
On the express api server i am using passport as a middleware to handle google oauth and after succesfully logged in through google i am generating a jwt in the callback on my backend.
passport.use(new GoogleStrategy({
clientID: config.get('google.clientID'),
clientSecret: config.get('google.clientSecret'),
callbackURL: config.get('google.callbackUrl'),
},
function(accessToken, refreshToken, profile, done) {
User.findOne(
{ socialID: profile.id },
function (err, user) {
if (err) {
return done(err);
}
//No user was found... so create a new user with values from Facebook (all the profile. stuff)
if (!user) {
user = new User({
name: profile.displayName,
email: profile.emails[0].value,
provider: profile.provider,
socialID: profile.id,
});
user.save(function(err) {
if (err) console.log(err);
});
}
// the information which shall be inside the jsonwebtoken
const payload = {
user: {
id: user.id
}
};
// create jsonwebtoken and return it
jwt.sign(
payload,
config.get('jwt.secret'), // get the secret from default.json to hash jsonwebtoken
{ expiresIn: config.get('jwt.lifetime') },
(err, token) => {
if(err) throw err; // if there is error, throw it and exit
return done(JSON.stringify(token)); // return jwt token
}
);
}
);
}
));
I have theses routes on my api server
// #route GET api/auth/google
// #desc Google auth route - get User From Google, store it if not exists yet
// #access Public
router.get('/google',
passport.authenticate('google', { scope: ['profile', 'email'], session: false })
);
// #route GET api/auth/google/callback
// #desc Google callback route
// #access Public
router.get('/google/callback',
passport.authenticate('google', { failureRedirect: '/', session: false }),
function (req, res) {
res.redirect('http://localhost:8080/?token=' + res);
}
);
When i call my backend api route at /auth/google i successfully get redirected to the google login page. But with my approach i am trying to redirect from the callback url back to my vue app with a get parameter "token" to recieve the token in the frontend. The redirect in my backend callback route is not working. How do i pass the token which is generated in the backend to my frontend?
I came across that the redirect wasn't working because the return done() function expects two parameters to work correctly.
I changed inside the google passport middleware the done function like this
jwt.sign(
payload,
config.get('jwt.secret'), // get the secret from default.json to hash jsonwebtoken
{ expiresIn: config.get('jwt.lifetime') },
(err, token) => {
if(err) throw err; // if there is error, throw it and exit
return done(null, token); // return jwt token
}
);
Now inside my route i can successfully redirect + add the token as a get parameter - so with this workaround i am recieving my jwt which is generated in my backend in my frontend.
// #route GET api/auth/google/callback
// #desc Google callback route
// #access Public
router.get('/google/callback',
passport.authenticate('google', { failureRedirect: '/', session: false }),
function (req, res) {
let token = res.req.user;
res.redirect('//localhost:8080/?token=' + token);
}
);
I am using this passport strategy.
passport.use(
'onlyForRefreshToken',
new JWTStrategy(
{
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtSecretRider,
},
(jwtPayload, done) => {
if (jwtPayload) {
return done(null, jwtPayload);
}
return done(null, false);
},
),
);
My goal is Putting 'jwtPayload' into my rest API of Nodejs that is located at other folder.
That is, I want to use jwtPayload decoded at the code below.
exports.riderRefreshToken = async (req, res) => {
const { email } = req.body;
const exRiderRefreshToken = await Rider.findOne({ email });
}
And this router works by middleware of jwtstrategy.
router.post(
'/refreshToken',
passport.authenticate('onlyForRefreshToken', { session: false }),
authenticationCtrl.riderRefreshToken,
);
In conclusion, when JWT passes from jwtstrategy without problem, that Post router would work.
And I want to use jwtPayload that is in jwtstrategy into Nodejs API as req.params or req.body.
Could you help me this problem?
You need to wrap your strategy into a function that gets req and res :
const isAuthenticated: RequestHandler = (req, res, next) => {
passport.authenticate(
'jwt',
{ session: false, failWithError: true },
(error, user) => {
if (error) {
return next(error)
}
//HERE PUT USER WHERE YOU WANT
//req.data or req.user or req.userInfo
req.user = user
return next()
}
)(req, res, next)
}
I don't recommend putting the user into req.params nor req.body since it might be confusing later on (because technically it doesn't come from those).