Skip to content

Commit

Permalink
Implement linkedin login (#131)
Browse files Browse the repository at this point in the history
Co-authored-by: Dileepa Mabulage <[email protected]>
  • Loading branch information
dsmabulage and dileepainivossl authored Jul 14, 2024
1 parent 519c6d3 commit b6ff45c
Show file tree
Hide file tree
Showing 12 changed files with 236 additions and 26 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ CLIENT_URL=http://localhost:5173
IMG_HOST=http://localhost:${SERVER_PORT}
SMTP_MAIL=your_smtp_mail
SMTP_PASSWORD=your_smtp_password
LINKEDIN_CLIENT_ID=your_linkedin_client_id
LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret
LINKEDIN_REDIRECT_URL=http://localhost:${SERVER_PORT}/api/auth/linkedin/callback
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,26 @@ process.env.GOOGLE_CLIENT_SECRET = 'your-client-secret';
process.env.GOOGLE_REDIRECT_URL = 'your-redirect-uri';


We appreciate your interest in ScholarX. Happy contributing! If you have any questions or need assistance, please don't hesitate to reach out to us.
We appreciate your interest in ScholarX. Happy contributing! If you have any questions or need assistance, please don't hesitate to reach out to us.

## Setting up LinkedIn Authentication

1. Create LinkedIn page with the mandatory information.

2. Navigate to https://developer.linkedin.com/

3. Select "Create App":
- Add App name.
- Search for the LinkedIn page that was previously created.
- Upload an image as a Logo.
- Create the App.

4. In Products section select `Share on LinkedIn` and `Sign In with LinkedIn using OpenID Connect` request access.

5. In Auth section edit the `Authorized redirect URLs for your app` and add the redirect url. `http://localhost:3000/api/auth/linkedin/callback`

6. Copy Client Id and Client Secret from the Auth Section.

6. In setting section verify the LinkedIn Page and generate URL.

7. Verify it from your account.
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-linkedin-oauth2": "github:auth0/passport-linkedin-oauth2#v3.0.0",
"pg": "^8.10.0",
"reflect-metadata": "^0.1.13",
"ts-node": "^10.9.1",
Expand All @@ -53,6 +54,7 @@
"@types/passport": "^1.0.12",
"@types/passport-google-oauth20": "^2.0.14",
"@types/passport-jwt": "^3.0.9",
"@types/passport-linkedin-oauth2": "^1.5.6",
"@types/pg": "^8.10.1",
"@types/prettier": "^2.7.2",
"@types/supertest": "^2.0.12",
Expand Down
3 changes: 2 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import adminRouter from './routes/admin/admin.route'
import mentorRouter from './routes/mentor/mentor.route'
import categoryRouter from './routes/category/category.route'
import passport from 'passport'
import './configs/passport'
import './configs/google-passport'
import './configs/linkedin-passport'
import { CLIENT_URL } from './configs/envConfig'
import cookieParser from 'cookie-parser'
import menteeRouter from './routes/mentee/mentee.route'
Expand Down
3 changes: 3 additions & 0 deletions src/configs/envConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ export const CLIENT_URL = process.env.CLIENT_URL ?? ''
export const IMG_HOST = process.env.IMG_HOST ?? ''
export const SMTP_MAIL = process.env.SMTP_MAIL ?? ''
export const SMTP_PASS = process.env.SMTP_PASS ?? ''
export const LINKEDIN_CLIENT_ID = process.env.LINKEDIN_CLIENT_ID ?? ''
export const LINKEDIN_CLIENT_SECRET = process.env.LINKEDIN_CLIENT_SECRET ?? ''
export const LINKEDIN_REDIRECT_URL = process.env.LINKEDIN_REDIRECT_URL ?? ''
19 changes: 13 additions & 6 deletions src/configs/passport.ts → src/configs/google-passport.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type { Request } from 'express'
import passport from 'passport'
import { Strategy as JwtStrategy } from 'passport-jwt'
import { dataSource } from './dbConfig'
import Profile from '../entities/profile.entity'
import { dataSource } from './dbConfig'
import {
JWT_SECRET,
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
GOOGLE_REDIRECT_URL,
GOOGLE_CLIENT_SECRET
JWT_SECRET
} from './envConfig'
import type { Request } from 'express'

import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
import { findOrCreateUser } from '../services/auth.service'
import { type User } from '../types'
import { type CreateProfile, type User } from '../types'

passport.use(
new GoogleStrategy(
Expand All @@ -31,7 +31,14 @@ passport.use(
done: (err: Error | null, user?: Profile) => void
) {
try {
const user = await findOrCreateUser(profile)
const createProfile: CreateProfile = {
id: profile.id,
primary_email: profile.emails?.[0]?.value ?? '',
first_name: profile.name?.givenName ?? '',
last_name: profile.name?.familyName ?? '',
image_url: profile.photos?.[0]?.value ?? ''
}
const user = await findOrCreateUser(createProfile)
done(null, user)
} catch (err) {
done(err as Error)
Expand Down
101 changes: 101 additions & 0 deletions src/configs/linkedin-passport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { Request } from 'express'
import passport from 'passport'
import { Strategy as JwtStrategy } from 'passport-jwt'
import Profile from '../entities/profile.entity'
import { dataSource } from './dbConfig'
import {
JWT_SECRET,
LINKEDIN_CLIENT_ID,
LINKEDIN_CLIENT_SECRET,
LINKEDIN_REDIRECT_URL
} from './envConfig'

import { Strategy as LinkedInStrategy } from 'passport-linkedin-oauth2'
import { findOrCreateUser } from '../services/auth.service'
import { type CreateProfile, type LinkedInProfile, type User } from '../types'

passport.use(
new LinkedInStrategy(
{
clientID: LINKEDIN_CLIENT_ID,
clientSecret: LINKEDIN_CLIENT_SECRET,
callbackURL: LINKEDIN_REDIRECT_URL,
scope: ['openid', 'email', 'profile'],
passReqToCallback: true
},
async function (
req: Request,
accessToken: string,
refreshToken: string,
profile: passport.Profile,
done: (err: Error | null, user?: Profile) => void
) {
try {
const data = profile as unknown as LinkedInProfile
const createProfile: CreateProfile = {
id: data.id,
primary_email: data?.email ?? '',
first_name: data?.givenName ?? '',
last_name: data?.familyName ?? '',
image_url: data?.picture ?? ''
}
const user = await findOrCreateUser(createProfile)
done(null, user)
} catch (err) {
done(err as Error)
}
}
)
)

passport.serializeUser((user: Express.User, done) => {
done(null, (user as User).primary_email)
})

passport.deserializeUser(async (primary_email: string, done) => {
try {
const profileRepository = dataSource.getRepository(Profile)
const user = await profileRepository.findOne({
where: { primary_email },
relations: ['mentor', 'mentee']
})
done(null, user)
} catch (err) {
done(err)
}
})

const cookieExtractor = (req: Request): string => {
let token = null
if (req?.cookies) {
token = req.cookies.jwt
}
return token
}

const options = {
jwtFromRequest: cookieExtractor,
secretOrKey: JWT_SECRET
}

passport.use(
new JwtStrategy(options, async (jwtPayload, done) => {
try {
const profileRepository = dataSource.getRepository(Profile)
const profile = await profileRepository.findOne({
where: { uuid: jwtPayload.userId },
relations: ['mentor', 'mentee']
})

if (!profile) {
done(null, false)
} else {
done(null, profile)
}
} catch (error) {
done(error, false)
}
})
)

export default passport
24 changes: 24 additions & 0 deletions src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ export const googleRedirect = async (
)(req, res, next)
}

export const linkedinRedirect = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
passport.authenticate(
'linkedin',
{ failureRedirect: '/login' },
(err: Error, user: Profile) => {
if (err) {
next(err)
return
}
if (!user) {
res.redirect('/login')
return
}
signAndSetCookie(res, user.uuid)

res.redirect(process.env.CLIENT_URL ?? '/')
}
)(req, res, next)
}

export const register = async (
req: Request,
res: Response
Expand Down
17 changes: 13 additions & 4 deletions src/routes/auth/auth.route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import express from 'express'
import passport from 'passport'
import {
register,
googleRedirect,
linkedinRedirect,
login,
logout,
googleRedirect,
passwordReset,
passwordResetRequest,
passwordReset
register
} from '../../controllers/auth.controller'
import passport from 'passport'

const authRouter = express.Router()

Expand All @@ -22,7 +23,15 @@ authRouter.get(
})
)

authRouter.get(
'/linkedin',
passport.authenticate('linkedin', {
scope: ['openid', 'email', 'profile']
})
)

authRouter.get('/google/callback', googleRedirect)
authRouter.get('/linkedin/callback', linkedinRedirect)
authRouter.post('/password-reset-request', passwordResetRequest)
authRouter.put('/passwordreset', passwordReset)
export default authRouter
28 changes: 14 additions & 14 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { dataSource } from '../configs/dbConfig'
import bcrypt from 'bcrypt'
import Profile from '../entities/profile.entity'
import type passport from 'passport'
import jwt from 'jsonwebtoken'
import { dataSource } from '../configs/dbConfig'
import { JWT_SECRET } from '../configs/envConfig'
import Profile from '../entities/profile.entity'
import { type CreateProfile, type ApiResponse } from '../types'
import {
getPasswordResetEmailContent,
getPasswordChangedEmailContent
getPasswordChangedEmailContent,
getPasswordResetEmailContent
} from '../utils'
import { sendResetPasswordEmail } from './admin/email.service'
import { type ApiResponse } from '../types'

export const registerUser = async (
email: string,
Expand Down Expand Up @@ -88,21 +87,22 @@ export const loginUser = async (
}

export const findOrCreateUser = async (
profile: passport.Profile
createProfileDto: CreateProfile
): Promise<Profile> => {
const profileRepository = dataSource.getRepository(Profile)

let user = await profileRepository.findOne({
where: { primary_email: profile.emails?.[0]?.value ?? '' },
relations: ['mentor', 'mentee']
where: { primary_email: createProfileDto.primary_email }
})

if (!user) {
const hashedPassword = await bcrypt.hash(profile.id, 10) // Use Google ID as password
const hashedPassword = await bcrypt.hash(createProfileDto.id, 10)
user = profileRepository.create({
primary_email: profile.emails?.[0]?.value ?? '',
primary_email: createProfileDto.primary_email,
password: hashedPassword,
first_name: profile.name?.givenName,
last_name: profile.name?.familyName,
image_url: profile.photos?.[0]?.value ?? ''
first_name: createProfileDto.first_name,
last_name: createProfileDto.last_name,
image_url: createProfileDto.image_url
})
await profileRepository.save(user)
}
Expand Down
16 changes: 16 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,19 @@ export interface ApiResponse<T> {
export interface User extends Express.User {
primary_email: string
}

export interface CreateProfile {
primary_email: string
id: string
first_name: string
last_name: string
image_url: string
}

export interface LinkedInProfile {
id: string
givenName: string
familyName: string
picture: string
email: string
}

0 comments on commit b6ff45c

Please sign in to comment.