refactor(backend): enhance auth strategies with type safety and better error handling (#5066)

Co-authored-by: mirarifhasan <arif.ishan05@gmail.com>
This commit is contained in:
Krish Prajapati 2025-06-25 14:02:01 +05:30 committed by GitHub
parent 19362a4291
commit 08e5fa974c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 74 additions and 41 deletions

View file

@ -1,4 +1,4 @@
import { Strategy } from 'passport-github2';
import { Strategy, Profile } from 'passport-github2';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';
@ -6,6 +6,8 @@ import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
import { validateEmail } from 'src/utils';
import { AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH } from 'src/errors';
@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy) {
@ -15,18 +17,26 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
private configService: ConfigService,
) {
super({
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
callbackURL: configService.get('INFRA.GITHUB_CALLBACK_URL'),
scope: [configService.get('INFRA.GITHUB_SCOPE')],
clientID: configService.get<string>('INFRA.GITHUB_CLIENT_ID'),
clientSecret: configService.get<string>('INFRA.GITHUB_CLIENT_SECRET'),
callbackURL: configService.get<string>('INFRA.GITHUB_CALLBACK_URL'),
scope: [configService.get<string>('INFRA.GITHUB_SCOPE')],
store: true,
});
}
async validate(accessToken, refreshToken, profile, done) {
const user = await this.usersService.findUserByEmail(
profile.emails[0].value,
);
async validate(
accessToken: string,
refreshToken: string,
profile: Profile,
done,
) {
const email = profile.emails?.[0].value;
if (!validateEmail(email))
throw new UnauthorizedException(AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH);
const user = await this.usersService.findUserByEmail(email);
if (O.isNone(user)) {
const createdUser = await this.usersService.createUserSSO(
@ -38,7 +48,7 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
}
/**
* * displayName and photoURL maybe null if user logged-in via magic-link before SSO
* displayName and photoURL maybe null if user logged-in via magic-link before SSO
*/
if (!user.value.displayName || !user.value.photoURL) {
const updatedUser = await this.usersService.updateUserDetails(
@ -51,8 +61,8 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
}
/**
* * Check to see if entry for Github is present in the Account table for user
* * If user was created with another provider findUserByEmail may return true
* Check to see if entry for Github is present in the Account table for user
* If user was created with another provider findUserByEmail may return true
*/
const providerAccountExists =
await this.authService.checkIfProviderAccountExists(user.value, profile);

View file

@ -1,4 +1,4 @@
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Strategy, VerifyCallback, Profile } from 'passport-google-oauth20';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserService } from 'src/user/user.service';
@ -6,6 +6,9 @@ import * as O from 'fp-ts/Option';
import { AuthService } from '../auth.service';
import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
import { validateEmail } from 'src/utils';
import { AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH } from 'src/errors';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
@ -15,10 +18,10 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
private configService: ConfigService,
) {
super({
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
callbackURL: configService.get('INFRA.GOOGLE_CALLBACK_URL'),
scope: configService.get('INFRA.GOOGLE_SCOPE').split(','),
clientID: configService.get<string>('INFRA.GOOGLE_CLIENT_ID'),
clientSecret: configService.get<string>('INFRA.GOOGLE_CLIENT_SECRET'),
callbackURL: configService.get<string>('INFRA.GOOGLE_CALLBACK_URL'),
scope: configService.get<string>('INFRA.GOOGLE_SCOPE').split(','),
passReqToCallback: true,
store: true,
});
@ -26,14 +29,17 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
async validate(
req: Request,
accessToken,
refreshToken,
profile,
accessToken: string,
refreshToken: string,
profile: Profile,
done: VerifyCallback,
) {
const user = await this.usersService.findUserByEmail(
profile.emails[0].value,
);
const email = profile.emails?.[0].value;
if (!validateEmail(email))
throw new UnauthorizedException(AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH);
const user = await this.usersService.findUserByEmail(email);
if (O.isNone(user)) {
const createdUser = await this.usersService.createUserSSO(
@ -45,7 +51,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
}
/**
* * displayName and photoURL maybe null if user logged-in via magic-link before SSO
* displayName and photoURL maybe null if user logged-in via magic-link before SSO
*/
if (!user.value.displayName || !user.value.photoURL) {
const updatedUser = await this.usersService.updateUserDetails(
@ -58,8 +64,8 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
}
/**
* * Check to see if entry for Google is present in the Account table for user
* * If user was created with another provider findUserByEmail may return true
* Check to see if entry for Google is present in the Account table for user
* If user was created with another provider findUserByEmail may return true
*/
const providerAccountExists =
await this.authService.checkIfProviderAccountExists(user.value, profile);

View file

@ -93,7 +93,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
),
),
]),
secretOrKey: configService.get('JWT_SECRET'),
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}

View file

@ -6,6 +6,8 @@ import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
import { validateEmail } from 'src/utils';
import { AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH } from 'src/errors';
@Injectable()
export class MicrosoftStrategy extends PassportStrategy(Strategy) {
@ -15,19 +17,27 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
private configService: ConfigService,
) {
super({
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
callbackURL: configService.get('INFRA.MICROSOFT_CALLBACK_URL'),
scope: configService.get('INFRA.MICROSOFT_SCOPE').split(','),
tenant: configService.get('INFRA.MICROSOFT_TENANT'),
clientID: configService.get<string>('INFRA.MICROSOFT_CLIENT_ID'),
clientSecret: configService.get<string>('INFRA.MICROSOFT_CLIENT_SECRET'),
callbackURL: configService.get<string>('INFRA.MICROSOFT_CALLBACK_URL'),
scope: configService.get<string>('INFRA.MICROSOFT_SCOPE').split(','),
tenant: configService.get<string>('INFRA.MICROSOFT_TENANT'),
store: true,
});
}
async validate(accessToken: string, refreshToken: string, profile, done) {
const user = await this.usersService.findUserByEmail(
profile.emails[0].value,
);
async validate(
accessToken: string,
refreshToken: string,
profile,
done,
) {
const email = profile?.emails?.[0]?.value;
if (!validateEmail(email))
throw new UnauthorizedException(AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH);
const user = await this.usersService.findUserByEmail(email);
if (O.isNone(user)) {
const createdUser = await this.usersService.createUserSSO(
@ -39,7 +49,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
}
/**
* * displayName and photoURL maybe null if user logged-in via magic-link before SSO
* displayName and photoURL maybe null if user logged-in via magic-link before SSO
*/
if (!user.value.displayName || !user.value.photoURL) {
const updatedUser = await this.usersService.updateUserDetails(
@ -52,8 +62,8 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
}
/**
* * Check to see if entry for Microsoft is present in the Account table for user
* * If user was created with another provider findUserByEmail may return true
* Check to see if entry for Microsoft is present in the Account table for user
* If user was created with another provider findUserByEmail may return true
*/
const providerAccountExists =
await this.authService.checkIfProviderAccountExists(user.value, profile);

View file

@ -25,7 +25,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
const RTCookie = request.cookies['refresh_token'];
const RTCookie = request.cookies?.['refresh_token'];
if (!RTCookie) {
console.error('`refresh_token` not found');
throw new ForbiddenException(COOKIES_NOT_FOUND);
@ -33,7 +33,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
return RTCookie;
},
]),
secretOrKey: configService.get('JWT_SECRET'),
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}

View file

@ -43,6 +43,13 @@ export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
export const AUTH_PROVIDER_NOT_CONFIGURED =
'auth/provider_not_configured_correctly';
/**
* Email not provided by OAuth provider
* (SSO Strategies)
*/
export const AUTH_EMAIL_NOT_PROVIDED_BY_OAUTH =
'auth/email_not_provided_by_oauth';
/**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
*/