From e969900c6055ed83219f5e7f02d26448a38ecd0c Mon Sep 17 00:00:00 2001 From: Shrenik Deep Date: Tue, 7 Nov 2023 13:39:27 +0530 Subject: [PATCH] LinkedIn authentication with passport.js --- package-lock.json | 121 ++++++++++++++++++++++++++++++++++ package.json | 4 ++ src/app.ts | 5 +- src/configs/envConfig.ts | 3 + src/configs/passport.ts | 26 +++++++- src/routes/auth/auth.route.ts | 13 +++- src/types.ts | 11 ++++ 7 files changed, 180 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01c58d98..9ea238c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,11 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-session": "^1.17.3", "jsonwebtoken": "^9.0.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", + "passport-linkedin-oauth2": "^2.0.0", "pg": "^8.10.0", "reflect-metadata": "^0.1.13", "ts-node": "^10.9.1", @@ -29,11 +31,13 @@ "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.13", "@types/express": "^4.17.17", + "@types/express-session": "^1.17.10", "@types/jest": "^29.5.3", "@types/jsonwebtoken": "^9.0.2", "@types/node": "^20.1.4", "@types/passport": "^1.0.12", "@types/passport-jwt": "^3.0.9", + "@types/passport-linkedin-oauth2": "^1.5.5", "@types/pg": "^8.10.1", "@types/prettier": "^2.7.2", "@types/supertest": "^2.0.12", @@ -1630,6 +1634,15 @@ "@types/send": "*" } }, + "node_modules/@types/express-session": { + "version": "1.17.10", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.10.tgz", + "integrity": "sha512-U32bC/s0ejXijw5MAzyaV4tuZopCh/K7fPoUDyNbsRXHvPSeymygYD1RFL99YOLhF5PNOkzswvOTRaVHdL1zMw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -1725,6 +1738,16 @@ "@types/passport-strategy": "*" } }, + "node_modules/@types/passport-linkedin-oauth2": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/passport-linkedin-oauth2/-/passport-linkedin-oauth2-1.5.5.tgz", + "integrity": "sha512-N9h7f838AV3fmHmHgiyGjxPAiUTw0IrHLghDvAzRxpvQ5tNvyucSdsTTxhmm42aQR3VQf9YpqwtIBpajDQDCVw==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/passport-strategy": { "version": "0.2.35", "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz", @@ -2965,6 +2988,14 @@ } ] }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcrypt": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", @@ -4835,6 +4866,32 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/express/node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -7300,6 +7357,11 @@ "set-blocking": "^2.0.0" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7406,6 +7468,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7602,6 +7672,33 @@ "passport-strategy": "^1.0.0" } }, + "node_modules/passport-linkedin-oauth2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-linkedin-oauth2/-/passport-linkedin-oauth2-2.0.0.tgz", + "integrity": "sha512-PnSeq2HzFQ/y1/p2RTF/kG2zhJ7kwGVg4xO3E+JNxz2aI0pFJGAqC503FVpUksYbhQdNhL6QYlK9qrEXD7ZYCg==", + "dependencies": { + "passport-oauth2": "1.x.x" + } + }, + "node_modules/passport-oauth2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", + "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -7976,6 +8073,14 @@ } ] }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -9370,6 +9475,22 @@ "node": ">=14.17" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 22c81c97..2c0ec5f5 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,11 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-session": "^1.17.3", "jsonwebtoken": "^9.0.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", + "passport-linkedin-oauth2": "^2.0.0", "pg": "^8.10.0", "reflect-metadata": "^0.1.13", "ts-node": "^10.9.1", @@ -34,11 +36,13 @@ "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.13", "@types/express": "^4.17.17", + "@types/express-session": "^1.17.10", "@types/jest": "^29.5.3", "@types/jsonwebtoken": "^9.0.2", "@types/node": "^20.1.4", "@types/passport": "^1.0.12", "@types/passport-jwt": "^3.0.9", + "@types/passport-linkedin-oauth2": "^1.5.5", "@types/pg": "^8.10.1", "@types/prettier": "^2.7.2", "@types/supertest": "^2.0.12", diff --git a/src/app.ts b/src/app.ts index a5336096..5a4fbdd0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,13 +11,16 @@ import categoryRouter from './routes/category/category.route' import passport from 'passport' import './configs/passport' import cookieParser from 'cookie-parser' - +import session from 'express-session' +import { SESSION_SECRET } from './configs/envConfig' const app = express() app.use(cookieParser()) app.use(bodyParser.json()) app.use(cors()) +app.use(session({ secret: SESSION_SECRET })) app.use(passport.initialize()) +app.use(passport.session()) app.get('/', (req, res) => { res.send('ScholarX Backend') diff --git a/src/configs/envConfig.ts b/src/configs/envConfig.ts index 7d5fad3b..18ae23ad 100644 --- a/src/configs/envConfig.ts +++ b/src/configs/envConfig.ts @@ -9,3 +9,6 @@ export const DB_PASSWORD = process.env.DB_PASSWORD export const DB_PORT = process.env.DB_PORT export const SERVER_PORT = process.env.SERVER_PORT ?? 3000 export const JWT_SECRET = process.env.JWT_SECRET ?? '' +export const LINKEDIN_KEY = process.env.LINKEDIN_KEY ?? '' +export const LINKEDIN_SECRET = process.env.LINKEDIN_SECRET ?? '' +export const SESSION_SECRET = process.env.SESSION_SECRET ?? '' diff --git a/src/configs/passport.ts b/src/configs/passport.ts index de6a614a..6b27b82b 100644 --- a/src/configs/passport.ts +++ b/src/configs/passport.ts @@ -2,8 +2,10 @@ import passport from 'passport' import { Strategy as JwtStrategy } from 'passport-jwt' import { dataSource } from './dbConfig' import Profile from '../entities/profile.entity' -import { JWT_SECRET } from './envConfig' +import { JWT_SECRET, LINKEDIN_KEY, LINKEDIN_SECRET } from './envConfig' import type { Request } from 'express' +import { Strategy as LinkedInStrategy } from 'passport-linkedin-oauth2' +import type { LinkedInUser } from '../types' const cookieExtractor = (req: Request): string => { let token = null @@ -37,4 +39,26 @@ passport.use( }) ) +passport.use( + new LinkedInStrategy( + { + clientID: LINKEDIN_KEY, + clientSecret: LINKEDIN_SECRET, + callbackURL: 'http://localhost:3000/api/auth/linkedin/callback', + scope: ['r_emailaddress', 'r_liteprofile'] + }, + (accessToken, refreshToken, profile, done) => { + done(null, profile) + } + ) +) + +passport.serializeUser((user, done) => { + done(null, user) +}) + +passport.deserializeUser((user: LinkedInUser, done) => { + done(null, user) +}) + export default passport diff --git a/src/routes/auth/auth.route.ts b/src/routes/auth/auth.route.ts index fe97de5b..9676d818 100644 --- a/src/routes/auth/auth.route.ts +++ b/src/routes/auth/auth.route.ts @@ -1,10 +1,21 @@ import express from 'express' import { register, login, logout } from '../../controllers/auth.controller' +import passport from 'passport' const authRouter = express.Router() authRouter.post('/register', register) authRouter.post('/login', login) authRouter.get('/logout', logout) - +authRouter.get( + '/linkedin', + passport.authenticate('linkedin', { state: 'SOME STATE' }) +) +authRouter.get( + '/linkedin/callback', + passport.authenticate('linkedin', { + successRedirect: '/', + failureRedirect: '/' + }) +) export default authRouter diff --git a/src/types.ts b/src/types.ts index 8d017dd0..0a4f7eb7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,3 +22,14 @@ export interface ApiResponse { message?: string data?: T | null } + +export interface LinkedInUser { + id: string + displayName: string + name: { + familyName: string + givenName: string + } + emails: [{ value: string }] + photos: [{ value: string }] +}