Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Port number
PORT=3000

# Client URL
CLIENT_URL=http://localhost:5173,http://localhost:5174

# URL of the Mongo DB
MONGODB_URL=mongodb://127.0.0.1:27017/node-express-server

Expand All @@ -23,3 +26,10 @@ SMTP_PORT=587
SMTP_USERNAME=email-server-username
SMTP_PASSWORD=email-server-password
EMAIL_FROM=support@yourapp.com

# Google OAuth
# You can get the client ID and secret from the Google Developers Console: https://console.developers.google.com/
# Use the same port as the one used in the `PORT` environment variable. By default, the callback URL is set to `http://localhost:3000/v1/auth/google/callback`
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_CALLBACK_URL=http://localhost:3000/v1/auth/google/callback
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@faker-js/faker": "^8.4.1",
"bcryptjs": "^2.4.3",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"coveralls": "^3.1.1",
"cross-env": "^7.0.0",
Expand All @@ -64,6 +65,7 @@
"morgan": "^1.9.1",
"nodemailer": "^6.3.1",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.0",
"pm2": "^5.1.0",
"swagger-jsdoc": "^6.0.8",
Expand Down
19 changes: 17 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ const passport = require('passport');
const httpStatus = require('http-status');
const config = require('./config/config');
const morgan = require('./config/morgan');
const { jwtStrategy } = require('./config/passport');
const { jwtStrategy, googleStrategy } = require('./config/passport');
const { authLimiter } = require('./middlewares/rateLimiter');
const routes = require('./routes/v1');
const { errorConverter, errorHandler } = require('./middlewares/error');
const ApiError = require('./utils/ApiError');
const cookieParser = require('cookie-parser');

const app = express();

app.use(cookieParser());

if (config.env !== 'test') {
app.use(morgan.successHandler);
app.use(morgan.errorHandler);
Expand All @@ -38,12 +41,24 @@ app.use(mongoSanitize());
app.use(compression());

// enable cors
app.use(cors());
const clientURLs = config.clientURL.split(',');
const corsOptions = {
origin: (origin, callback) => {
if (clientURLs.indexOf(origin) !== -1 || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
};
app.use(cors(corsOptions));
app.options('*', cors());

// jwt authentication
app.use(passport.initialize());
passport.use('jwt', jwtStrategy);
passport.use('google', googleStrategy);

// limit repeated failed requests to auth endpoints
if (config.env === 'production') {
Expand Down
10 changes: 10 additions & 0 deletions src/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const envVarsSchema = Joi.object()
.keys({
NODE_ENV: Joi.string().valid('production', 'development', 'test').required(),
PORT: Joi.number().default(3000),
CLIENT_URL: Joi.string().required().description('Client URL'),
MONGODB_URL: Joi.string().required().description('Mongo DB url'),
JWT_SECRET: Joi.string().required().description('JWT secret key'),
JWT_ACCESS_EXPIRATION_MINUTES: Joi.number().default(30).description('minutes after which access tokens expire'),
Expand All @@ -23,6 +24,9 @@ const envVarsSchema = Joi.object()
SMTP_USERNAME: Joi.string().description('username for email server'),
SMTP_PASSWORD: Joi.string().description('password for email server'),
EMAIL_FROM: Joi.string().description('the from field in the emails sent by the app'),
GOOGLE_CLIENT_ID: Joi.string().required().description('Google OAuth Client ID'),
GOOGLE_CLIENT_SECRET: Joi.string().required().description('Google OAuth Client Secret'),
GOOGLE_CALLBACK_URL: Joi.string().required().description('Google OAuth Callback URL'),
})
.unknown();

Expand All @@ -35,6 +39,7 @@ if (error) {
module.exports = {
env: envVars.NODE_ENV,
port: envVars.PORT,
clientURL: envVars.CLIENT_URL,
mongoose: {
url: envVars.MONGODB_URL + (envVars.NODE_ENV === 'test' ? '-test' : ''),
options: {
Expand All @@ -61,4 +66,9 @@ module.exports = {
},
from: envVars.EMAIL_FROM,
},
google: {
clientId: envVars.GOOGLE_CLIENT_ID,
clientSecret: envVars.GOOGLE_CLIENT_SECRET,
callbackUrl: envVars.GOOGLE_CALLBACK_URL,
},
};
23 changes: 23 additions & 0 deletions src/config/passport.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const { Strategy: GoogleStrategy } = require('passport-google-oauth20');
const config = require('./config');
const { tokenTypes } = require('./tokens');
const { User } = require('../models');
Expand All @@ -25,6 +26,28 @@ const jwtVerify = async (payload, done) => {

const jwtStrategy = new JwtStrategy(jwtOptions, jwtVerify);

// Google Strategy
const googleStrategy = new GoogleStrategy({
clientID: config.google.clientId,
clientSecret: config.google.clientSecret,
callbackURL: config.google.callbackUrl,
}, async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ email: profile.emails[0].value });
if (!user) {
user = await User.create({
name: profile.displayName,
email: profile.emails[0].value,
password: Math.random().toString(36).slice(-10),
});
}
done(null, user);
} catch (error) {
done(error, false);
}
});

module.exports = {
jwtStrategy,
googleStrategy,
};
74 changes: 66 additions & 8 deletions src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,88 @@
const httpStatus = require('http-status');
const catchAsync = require('../utils/catchAsync');
const { authService, userService, tokenService, emailService } = require('../services');
const ApiError = require('../utils/ApiError');
const config = require('../config/config');

const register = catchAsync(async (req, res) => {
const user = await userService.createUser(req.body);
const tokens = await tokenService.generateAuthTokens(user);
res.cookie('accessToken', tokens.access.token, { httpOnly: true, secure: true });
res.cookie('refreshToken', tokens.refresh.token, { httpOnly: true, secure: true });
res.cookie('refreshToken', tokens.refresh.token, {
maxAge: tokens.refresh.expires,
httpOnly: config.env === "production",
secure: true,
sameSite: 'none',
});
res.status(httpStatus.CREATED).send({ user, tokens });
});

const login = catchAsync(async (req, res) => {
const { email, password } = req.body;
const user = await authService.loginUserWithEmailAndPassword(email, password);
const tokens = await tokenService.generateAuthTokens(user);
res.cookie('accessToken', tokens.access.token, { httpOnly: true, secure: true });
res.cookie('refreshToken', tokens.refresh.token, { httpOnly: true, secure: true });

res.cookie('refreshToken', tokens.refresh.token, {
maxAge: tokens.refresh.expires,
httpOnly: config.env === "production",
secure: true,
sameSite: 'none',
});
res.send({ user, tokens });
});

// Front-end Google Authentification
const googleAuth = catchAsync(async (req, res) => {
const user = await userService.getUserByEmail(req.body.email);
if (!user) {
user = await userService.createUser(req.body);
}
else if(!(await user.isPasswordMatch(req.body.password))) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Google authentification failed');
}
const tokens = await tokenService.generateAuthTokens(user);

res.cookie('refreshToken', tokens.refresh.token, {
maxAge: tokens.refresh.expires,
httpOnly: config.env === "production",
secure: true,
sameSite: 'none',
});
res.send({ user, tokens });
});

// Back-end Google Authentification
const googleSignIn = catchAsync(async (req, res) => {
const user = req.user;
const tokens = await tokenService.generateAuthTokens(user);
res.status(httpStatus.OK).send({ user, tokens });
});

const logout = catchAsync(async (req, res) => {
await authService.logout(req.body.refreshToken);
const refreshToken = req.body.refreshToken || req.cookies.refreshToken;

if (!refreshToken) {
return res.status(httpStatus.BAD_REQUEST).send('Please authenticate');
}

await authService.logout(refreshToken);
res.status(httpStatus.NO_CONTENT).send();
});

const refreshTokens = catchAsync(async (req, res) => {
const tokens = await authService.refreshAuth(req.body.refreshToken);
res.send({ ...tokens });
const refreshToken = req.body.refreshToken || req.cookies.refreshToken;

if (!refreshToken) {
return res.status(httpStatus.BAD_REQUEST).send('Please authenticate');
}
const tokens = await authService.refreshAuth(refreshToken);

res.cookie('refreshToken', tokens.refresh.token, {
maxAge: tokens.refresh.expires,
httpOnly: config.env === "production",
secure: true,
sameSite: 'none',
});
res.status(200).send({ ...tokens });
});

const forgotPassword = catchAsync(async (req, res) => {
Expand Down Expand Up @@ -54,10 +110,12 @@ const verifyEmail = catchAsync(async (req, res) => {
module.exports = {
register,
login,
googleAuth,
googleSignIn,
logout,
refreshTokens,
forgotPassword,
resetPassword,
sendVerificationEmail,
verifyEmail,
};
};
2 changes: 1 addition & 1 deletion src/middlewares/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const pick = require('../utils/pick');
const ApiError = require('../utils/ApiError');

const validate = (schema) => (req, res, next) => {
const validSchema = pick(schema, ['params', 'query', 'body']);
const validSchema = pick(schema, ['params', 'query', 'body', 'cookies']);
const object = pick(req, Object.keys(validSchema));
const { value, error } = Joi.compile(validSchema)
.prefs({ errors: { label: 'key' }, abortEarly: false })
Expand Down
65 changes: 65 additions & 0 deletions src/routes/v1/auth.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const validate = require('../../middlewares/validate');
const authValidation = require('../../validations/auth.validation');
const authController = require('../../controllers/auth.controller');
const auth = require('../../middlewares/auth');
const passport = require('passport');
const config = require('../../config/config');

const router = express.Router();

Expand All @@ -15,6 +17,11 @@ router.post('/reset-password', validate(authValidation.resetPassword), authContr
router.post('/send-verification-email', auth(), authController.sendVerificationEmail);
router.post('/verify-email', validate(authValidation.verifyEmail), authController.verifyEmail);

// Google Auth route
router.post('/google', validate(authValidation.register), authController.googleAuth);
router.get('/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
router.get('/google/callback', passport.authenticate('google', { session: false }), authController.googleSignIn);

module.exports = router;

/**
Expand All @@ -23,6 +30,64 @@ module.exports = router;
* name: Auth
* description: Authentication
*/
/**
* @swagger
* /auth/google:
* get:
* summary: Google Authentication
* tags: [Auth]
* description: |
* Initiates the Google authentication process.
* Visit: [http://localhost:3000/v1/auth/google/](http://localhost:3000/v1/auth/google/)
* Note: If you are using port other than 3000, replace it with your port number.
* responses:
* '200':
* description: Successful response
* content:
* application/json:
* schema:
* type: object
* properties:
* user:
* type: object
* properties:
* id:
* type: string
* example: "5ebac534954b54139806c112"
* email:
* type: string
* example: "fake@example.com"
* name:
* type: string
* example: "fake name"
* role:
* type: string
* example: "user"
* tokens:
* type: object
* properties:
* access:
* type: object
* properties:
* token:
* type: string
* example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg"
* expires:
* type: string
* format: date-time
* example: "2020-05-12T16:18:04.793Z"
* refresh:
* type: object
* properties:
* token:
* type: string
* example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg"
* expires:
* type: string
* format: date-time
* example: "2020-05-12T16:18:04.793Z"
*/


/**
* @swagger
Expand Down
21 changes: 19 additions & 2 deletions src/validations/auth.validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,33 @@ const login = {
}),
};

const customValidation = (value, helpers) => {
if (!value.body.refreshToken && !value.cookies.refreshToken) {
return helpers.response({ statusCode: 400, message: 'Custom error message' });
}
return value;
};

const logout = {
body: Joi.object().keys({
refreshToken: Joi.string().required(),
refreshToken: Joi.string().optional(),
}),
cookies: Joi.object().keys({
refreshToken: Joi.string().optional(),

}),
custom: Joi.object().custom(customValidation, 'Custom validation').required(),
};

const refreshTokens = {
body: Joi.object().keys({
refreshToken: Joi.string().required(),
refreshToken: Joi.string().optional(),
}),
cookies: Joi.object().keys({
refreshToken: Joi.string().optional(),

}),
custom: Joi.object().custom(customValidation, 'Custom validation').required(),
};

const forgotPassword = {
Expand Down
Loading