Skip to content

Commit 2ce60c7

Browse files
Merge pull request #308 from abdeljalil-salhi/305-add-xtwitter-oauth
Add X/Twitter OAuth with passport-twitter in the NestJS session
2 parents f2e6b32 + c713ab3 commit 2ce60c7

File tree

12 files changed

+496
-114
lines changed

12 files changed

+496
-114
lines changed

backend-nestjs/package-lock.json

Lines changed: 257 additions & 57 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend-nestjs/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@
4242
"class-validator": "^0.14.1",
4343
"graphql": "^16.8.1",
4444
"multer": "^1.4.5-lts.1",
45+
"nestjs-session": "^3.0.1",
4546
"nodemailer": "^6.9.13",
4647
"otplib": "^12.0.1",
4748
"passport": "^0.7.0",
4849
"passport-google-oauth2": "^0.2.0",
4950
"passport-jwt": "^4.0.1",
5051
"passport-local": "^1.0.0",
52+
"passport-twitter": "^1.0.4",
5153
"qrcode": "^1.5.3",
5254
"reflect-metadata": "^0.2.1",
5355
"rxjs": "^7.8.1",
@@ -66,6 +68,7 @@
6668
"@types/passport-google-oauth2": "^0.1.8",
6769
"@types/passport-jwt": "^4.0.1",
6870
"@types/passport-local": "^1.0.38",
71+
"@types/passport-twitter": "^1.0.40",
6972
"@types/supertest": "^6.0.2",
7073
"@types/uuid": "^9.0.8",
7174
"@typescript-eslint/eslint-plugin": "^7.2.0",

backend-nestjs/src/app/app.module.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { mailerConfig } from 'src/config/mailer.config';
3636
import { graphQLErrorFormatter } from './utils/graphql-error-formatter';
3737
import { SocketGateway } from 'src/socket/socket.gateway';
3838
import { SocketService } from 'src/socket/socket.service';
39+
import { SessionModule } from 'nestjs-session';
3940

4041
/**
4142
* The root module of the application.
@@ -66,6 +67,18 @@ import { SocketService } from 'src/socket/socket.service';
6667
// Enable the scheduling module
6768
ScheduleModule.forRoot(),
6869

70+
// Configure the session module
71+
SessionModule.forRoot({
72+
session: {
73+
secret: process.env.SESSION_SECRET, // Use the session secret from the environment variables
74+
resave: false, // Do not save the session if it has not been modified
75+
saveUninitialized: true, // Save the session even if it is new
76+
cookie: {
77+
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
78+
},
79+
},
80+
}),
81+
6982
AuthModule,
7083
UserModule,
7184
AvatarModule,

backend-nestjs/src/auth/auth.controller.ts

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ import {
1313
import { AuthService } from './auth.service';
1414
// Guards
1515
import { GoogleOAuthGuard } from './guards/google-oauth.guard';
16+
import { XTwitterOAuthGuard } from './guards/xtwitter-oauth.guard';
1617
// Decorators
1718
import { Public } from './decorators/public.decorator';
1819
// Interfaces
19-
import { GoogleUser } from './interfaces/google-user.interface';
20+
import { OAuthUser } from './interfaces/oauth-user.interface';
21+
// DTOs
22+
import { AuthResponse } from './dtos/auth.response';
2023

2124
/**
2225
* The controller that handles the authentication routes.
@@ -45,7 +48,7 @@ export class AuthController {
4548
@Public()
4649
@Get('google')
4750
@UseGuards(GoogleOAuthGuard)
48-
public async auth(): Promise<void> {}
51+
public async googleAuth(): Promise<void> {}
4952

5053
/**
5154
* Handles the Google OAuth callback.
@@ -68,8 +71,8 @@ export class AuthController {
6871
/**
6972
* The response containing the tokens and the user information.
7073
*/
71-
const response = await this.authService.loginWithGoogle(
72-
req.user as GoogleUser,
74+
const response: AuthResponse = await this.authService.loginWithOAuth(
75+
req.user as OAuthUser,
7376
);
7477

7578
// Set the access token cookie
@@ -106,4 +109,77 @@ export class AuthController {
106109
res.redirect(`${process.env.FRONTEND_URL}/google?error=${errorCode}`);
107110
}
108111
}
112+
113+
/**
114+
* Authenticates the user using Twitter OAuth.
115+
* This route redirects the user to the Twitter OAuth page.
116+
*
117+
* @public
118+
* @useGuards XTwitterOAuthGuard
119+
* @returns {Promise<void>} - The response
120+
*/
121+
@Public()
122+
@Get('twitter')
123+
@UseGuards(XTwitterOAuthGuard)
124+
public async twitterAuth(): Promise<void> {}
125+
126+
/**
127+
* Handles the Twitter OAuth callback.
128+
* This route is called after the user authenticates with Twitter.
129+
*
130+
* @public
131+
* @useGuards XTwitterOAuthGuard
132+
* @param {Request} req - The request object.
133+
* @param {Response} res - The response object.
134+
* @returns {Promise<void>} - The response
135+
*/
136+
@Public()
137+
@Get('twitter/callback')
138+
@UseGuards(XTwitterOAuthGuard)
139+
public async twitterAuthCallback(
140+
@Req() req: Request,
141+
@Res() res: Response,
142+
): Promise<void> {
143+
try {
144+
/**
145+
* The response containing the tokens and the user information.
146+
*/
147+
const response: AuthResponse = await this.authService.loginWithOAuth(
148+
req.user as OAuthUser,
149+
);
150+
151+
// Set the access token cookie
152+
res.cookie('access_token', response.accessToken, {
153+
maxAge: 60 * 60 * 24 * 7 * 1000, // 7 days
154+
sameSite: true,
155+
secure: process.env.NODE_ENV === 'production',
156+
});
157+
158+
if (response.is2faEnabled)
159+
// Set the short-lived token cookie
160+
res.cookie('short_lived_token', response.shortLivedToken, {
161+
maxAge: 60 * 30 * 1000, // 30 minutes
162+
sameSite: true,
163+
secure: process.env.NODE_ENV === 'production',
164+
});
165+
166+
// Redirect the user to the frontend with the 2FA status
167+
res.redirect(
168+
`${process.env.FRONTEND_URL}/x?2fa=${response.is2faEnabled ? 1 : 0}`,
169+
);
170+
} catch (e) {
171+
console.log(e);
172+
173+
let errorCode: string = '0';
174+
175+
if (e instanceof HttpException) {
176+
const response: string | object = e.getResponse();
177+
if (typeof response === 'object' && response !== null)
178+
errorCode = response['error'] || errorCode;
179+
}
180+
181+
// Redirect the user to the frontend with the error code
182+
res.redirect(`${process.env.FRONTEND_URL}/x?error=${errorCode}`);
183+
}
184+
}
109185
}

backend-nestjs/src/auth/auth.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { AccessTokenStrategy } from './strategies/access-token.strategy';
1717
import { RefreshTokenStrategy } from './strategies/refresh-token.strategy';
1818
import { ShortLivedTokenStrategy } from './strategies/short-lived-token.strategy';
1919
import { GoogleStrategy } from './strategies/google.strategy';
20+
import { XTwitterStrategy } from './strategies/xtwitter.strategy';
2021
// Controllers
2122
import { AuthController } from './auth.controller';
2223

@@ -46,6 +47,7 @@ import { AuthController } from './auth.controller';
4647
RefreshTokenStrategy,
4748
ShortLivedTokenStrategy,
4849
GoogleStrategy,
50+
XTwitterStrategy,
4951
],
5052
controllers: [AuthController],
5153
})

backend-nestjs/src/auth/auth.service.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ import { ForgotPasswordInput } from './dtos/forgot-password.input';
2727
import { ChangePasswordInput } from './dtos/change-password.input';
2828
import { ShortLivedTokenResponse } from './dtos/short-lived-token.response';
2929
// Interfaces
30-
import { GoogleUser } from './interfaces/google-user.interface';
30+
import { OAuthUser } from './interfaces/oauth-user.interface';
3131

3232
/**
3333
* The authentication service that encapsulates all authentication-related features and functionalities.
3434
*
3535
* @method register - Registers a new user with the specified details.
3636
* @method login - Logs in the user with the specified details.
37-
* @method loginWithGoogle - Logs in the user with the specified Google profile.
37+
* @method loginWithOAuth - Logs in the user with the specified OAuth profile.
3838
* @method logout - Logs out the user with the specified ID.
3939
* @method me - Returns the currently authenticated user.
4040
* @method verifyPassword - Verifies the password of the user with the specified ID.
@@ -225,33 +225,33 @@ export class AuthService {
225225
}
226226

227227
/**
228-
* Logs in the user with the specified Google profile.
228+
* Logs in the user with the specified OAuth profile.
229229
*
230-
* @param {GoogleUser} profile - The Google profile details for the user to login.
230+
* @param {OAuthUser} profile - The OAuth profile details for the user to login.
231231
* @returns {Promise<AuthResponse>} - The result of the login operation.
232-
* @throws {BadRequestException} - Thrown if the Google profile is invalid.
232+
* @throws {BadRequestException} - Thrown if the OAuth profile is invalid.
233233
* @throws {ForbiddenException} - Thrown if the user has already registered with a different provider.
234234
*/
235-
public async loginWithGoogle(profile: GoogleUser): Promise<AuthResponse> {
235+
public async loginWithOAuth(profile: OAuthUser): Promise<AuthResponse> {
236236
if (!profile)
237237
throw new BadRequestException(
238-
'You must provide a valid Google profile to proceed with the login process. Please try again.',
238+
'You must provide a valid OAuth profile to proceed with the login process. Please try again.',
239239
{ cause: new Error(), description: '1' },
240240
);
241241

242-
// Extract the provider, email, name, and avatar from the Google profile
242+
// Extract the provider, email, name, and avatar from the OAuth profile
243243
const { provider, email, name, avatar } = profile;
244244

245-
// Check if the Google profile is valid
245+
// Check if the OAuth profile is valid
246246
if (!provider || !email || !name || !avatar)
247247
throw new BadRequestException(
248-
'The Google profile provided is invalid. Please try again.',
248+
'The OAuth profile provided is invalid. Please try again.',
249249
{ cause: new Error(), description: '1' },
250250
);
251251

252252
/**
253-
* Find the user by the email provided in the Google profile.
254-
* If the user is not found, create a new user with the Google profile details and authenticate the user.
253+
* Find the user by the email provided in the OAuth profile.
254+
* If the user is not found, create a new user with the OAuth profile details and authenticate the user.
255255
* If the user is found, continue with the login process.
256256
*/
257257
const user: User = await this.userService
@@ -262,7 +262,7 @@ export class AuthService {
262262

263263
if (!user) {
264264
/**
265-
* Generate a random username by appending random numbers to the Google given name.
265+
* Generate a random username by appending random numbers to the OAuth given name.
266266
*/
267267
let username: string = name.toLowerCase() + '_';
268268

@@ -291,7 +291,7 @@ export class AuthService {
291291
*/
292292
if (user.connection.provider !== provider)
293293
throw new ForbiddenException(
294-
'You have already registered with a different provider. Please login using your email and password.',
294+
'You have already registered with a different provider. Please try logging in using your email and password.',
295295
{ cause: new Error(), description: '2' },
296296
);
297297

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Dependencies
2+
import { Injectable } from '@nestjs/common';
3+
import { AuthGuard } from '@nestjs/passport';
4+
5+
/**
6+
* Guard class representing a Twitter OAuth authentication guard.
7+
*
8+
* This guard utilizes the 'twitter' strategy to validate Twitter accounts.
9+
*
10+
* @export
11+
* @class XTwitterOAuthGuard
12+
* @extends {AuthGuard('xtwitter')}
13+
* @module AuthModule
14+
*/
15+
@Injectable()
16+
export class XTwitterOAuthGuard extends AuthGuard('twitter') {}

backend-nestjs/src/auth/interfaces/google-user.interface.ts

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* The OAuthUser interface represents the structure of the OAuth account.
3+
*
4+
* @export
5+
* @interface OAuthUser
6+
* @property {('google' | 'xtwitter')} provider - The provider of the OAuth account.
7+
* @property {string} email - The email of the OAuth account.
8+
* @property {string} name - The name of the OAuth account.
9+
* @property {string} avatar - The avatar of the OAuth account.
10+
* @module AuthModule
11+
*/
12+
export interface OAuthUser {
13+
/**
14+
* The provider of the OAuth account.
15+
* @type {('google' | 'xtwitter')}
16+
*/
17+
provider: 'google' | 'xtwitter';
18+
19+
/**
20+
* The email of the OAuth account.
21+
* @type {string}
22+
*/
23+
email: string;
24+
25+
/**
26+
* The name of the OAuth account.
27+
* @type {string}
28+
*/
29+
name: string;
30+
31+
/**
32+
* The avatar of the OAuth account.
33+
* @type {string}
34+
*/
35+
avatar: string;
36+
}

backend-nestjs/src/auth/strategies/google.strategy.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { PassportStrategy } from '@nestjs/passport';
44
import { Strategy, VerifyCallback } from 'passport-google-oauth2';
55

66
// Interfaces
7-
import { GoogleUser } from '../interfaces/google-user.interface';
7+
import { OAuthUser } from '../interfaces/oauth-user.interface';
88

99
/**
1010
* The strategy that handles the validation of Google accounts.
@@ -40,7 +40,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
4040
* @param {string} _refreshToken - The refresh token of the Google account.
4141
* @param {*} profile - The profile of the Google account.
4242
* @param {VerifyCallback} done - The callback function.
43-
* @returns {Promise<any>} - The validated user.
43+
* @returns {Promise<void>} - The validated user.
4444
*/
4545
public async validate(
4646
_accessToken: string,
@@ -51,13 +51,15 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
5151
// Extract the required information from the Google profile
5252
const { name, emails, photos } = profile;
5353

54-
const user: GoogleUser = {
54+
// Prepare the OAuth user object
55+
const user: OAuthUser = {
5556
provider: 'google',
5657
email: emails[0].value,
5758
name: name.givenName,
5859
avatar: photos[0].value,
5960
};
6061

62+
// Return the validated user
6163
done(null, user);
6264
}
6365
}

0 commit comments

Comments
 (0)