Gate master! Consul Caesar.js has a new challenge for you. Our legion is attracting the attention of various allies and trading partners who want to easily log into our forts using their own systems. It's time to learn Passport.js - a universal access control system!
Imagine you are a diplomatic guard at the largest Roman fort. Every day, envoys arrive from different countries:
Passport.js is a framework that allows:
1// auth/strategies/local.strategy.ts
2import { Injectable, UnauthorizedException } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { Strategy } from 'passport-local';
5import { AuthService } from '../services/auth.service';
6
7@Injectable()
8export class LocalStrategy extends PassportStrategy(Strategy) {
9 constructor(private authService: AuthService) {
10 super({
11 usernameField: 'username', // or 'email'
12 passwordField: 'password',
13 passReqToCallback: false, // whether to pass request to validate
14 });
15 }
16
17 async validate(username: string, password: string): Promise<any> {
18 const user = await this.authService.validateUser(username, password);
19
20 if (!user) {
21 throw new UnauthorizedException('Invalid legionary credentials!');
22 }
23
24 return user;
25 }
26}1// auth/guards/local-auth.guard.ts
2import { Injectable } from '@nestjs/common';
3import { AuthGuard } from '@nestjs/passport';
4
5@Injectable()
6export class LocalAuthGuard extends AuthGuard('local') {
7 constructor() {
8 super();
9 }
10
11 handleRequest(err, user, info, context) {
12 if (err || !user) {
13 throw new UnauthorizedException('Invalid login credentials!');
14 }
15 return user;
16 }
17}1# Installation
2npm install passport-google-oauth20
3npm install --save-dev @types/passport-google-oauth201// auth/strategies/google.strategy.ts
2import { Injectable } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { Strategy, VerifyCallback } from 'passport-google-oauth20';
5import { ConfigService } from '@nestjs/config';
6import { AuthService } from '../services/auth.service';
7
8@Injectable()
9export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
10 constructor(
11 private configService: ConfigService,
12 private authService: AuthService,
13 ) {
14 super({
15 clientID: configService.get<string>('GOOGLE_CLIENT_ID'),
16 clientSecret: configService.get<string>('GOOGLE_CLIENT_SECRET'),
17 callbackURL: configService.get<string>('GOOGLE_CALLBACK_URL'),
18 scope: ['email', 'profile'],
19 });
20 }
21
22 async validate(
23 accessToken: string,
24 refreshToken: string,
25 profile: any,
26 done: VerifyCallback,
27 ): Promise<any> {
28 const { name, emails, photos, id } = profile;
29
30 const user = {
31 googleId: id,
32 email: emails[0].value,
33 firstName: name.givenName,
34 lastName: name.familyName,
35 picture: photos[0].value,
36 accessToken,
37 refreshToken,
38 };
39
40 // Find or create the user
41 const existingUser = await this.authService.findOrCreateGoogleUser(user);
42
43 done(null, existingUser);
44 }
45}1// auth/guards/google-auth.guard.ts
2import { Injectable } from '@nestjs/common';
3import { AuthGuard } from '@nestjs/passport';
4
5@Injectable()
6export class GoogleAuthGuard extends AuthGuard('google') {
7 constructor() {
8 super({
9 accessType: 'offline',
10 prompt: 'consent',
11 });
12 }
13}1# Installation
2npm install passport-facebook
3npm install --save-dev @types/passport-facebook1// auth/strategies/facebook.strategy.ts
2import { Injectable } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { Strategy, Profile } from 'passport-facebook';
5import { ConfigService } from '@nestjs/config';
6import { AuthService } from '../services/auth.service';
7
8@Injectable()
9export class FacebookStrategy extends PassportStrategy(Strategy, 'facebook') {
10 constructor(
11 private configService: ConfigService,
12 private authService: AuthService,
13 ) {
14 super({
15 clientID: configService.get<string>('FACEBOOK_APP_ID'),
16 clientSecret: configService.get<string>('FACEBOOK_APP_SECRET'),
17 callbackURL: configService.get<string>('FACEBOOK_CALLBACK_URL'),
18 profileFields: ['id', 'emails', 'name', 'photos'],
19 });
20 }
21
22 async validate(
23 accessToken: string,
24 refreshToken: string,
25 profile: Profile,
26 done: Function,
27 ): Promise<any> {
28 const { name, emails, photos, id } = profile;
29
30 const user = {
31 facebookId: id,
32 email: emails?.[0]?.value,
33 firstName: name?.givenName,
34 lastName: name?.familyName,
35 picture: photos?.[0]?.value,
36 accessToken,
37 refreshToken,
38 };
39
40 const existingUser = await this.authService.findOrCreateFacebookUser(user);
41 done(null, existingUser);
42 }
43}1npm install passport-github2
2npm install --save-dev @types/passport-github21// auth/strategies/github.strategy.ts
2import { Injectable } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { Strategy, Profile } from 'passport-github2';
5import { ConfigService } from '@nestjs/config';
6import { AuthService } from '../services/auth.service';
7
8@Injectable()
9export class GitHubStrategy extends PassportStrategy(Strategy, 'github') {
10 constructor(
11 private configService: ConfigService,
12 private authService: AuthService,
13 ) {
14 super({
15 clientID: configService.get<string>('GITHUB_CLIENT_ID'),
16 clientSecret: configService.get<string>('GITHUB_CLIENT_SECRET'),
17 callbackURL: configService.get<string>('GITHUB_CALLBACK_URL'),
18 scope: ['user:email'],
19 });
20 }
21
22 async validate(
23 accessToken: string,
24 refreshToken: string,
25 profile: Profile,
26 done: Function,
27 ): Promise<any> {
28 const { username, displayName, emails, photos, id } = profile;
29
30 const user = {
31 githubId: id,
32 username: username,
33 displayName: displayName,
34 email: emails?.[0]?.value,
35 picture: photos?.[0]?.value,
36 accessToken,
37 refreshToken,
38 };
39
40 const existingUser = await this.authService.findOrCreateGitHubUser(user);
41 done(null, existingUser);
42 }
43}1// entities/user.entity.ts (update)
2import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
3import { Exclude } from 'class-transformer';
4
5@Entity('users')
6export class User {
7 @PrimaryGeneratedColumn()
8 id: number;
9
10 @Column({ unique: true, nullable: true })
11 username: string;
12
13 @Column({ unique: true })
14 email: string;
15
16 @Column({ nullable: true })
17 @Exclude()
18 password: string; // Nullable for OAuth users
19
20 @Column({ default: 'LEGION_MEMBER' })
21 role: string;
22
23 @Column({ nullable: true })
24 firstName: string;
25
26 @Column({ nullable: true })
27 lastName: string;
28
29 @Column({ nullable: true })
30 picture: string;
31
32 @Column({ nullable: true })
33 cohortName: string;
34
35 // OAuth provider IDs
36 @Column({ nullable: true, unique: true })
37 googleId: string;
38
39 @Column({ nullable: true, unique: true })
40 facebookId: string;
41
42 @Column({ nullable: true, unique: true })
43 githubId: string;
44
45 // OAuth tokens (encrypted)
46 @Column({ type: 'text', nullable: true })
47 @Exclude()
48 googleAccessToken: string;
49
50 @Column({ type: 'text', nullable: true })
51 @Exclude()
52 facebookAccessToken: string;
53
54 @Column({ type: 'text', nullable: true })
55 @Exclude()
56 githubAccessToken: string;
57
58 @Column({ default: true })
59 isActive: boolean;
60
61 @Column({ default: false })
62 emailVerified: boolean;
63
64 @CreateDateColumn()
65 joinedAt: Date;
66
67 @UpdateDateColumn()
68 lastLoginAt: Date;
69
70 @Column({ nullable: true })
71 lastLoginIp: string;
72
73 @Column({ default: 0 })
74 failedLoginAttempts: number;
75
76 @Column({ nullable: true })
77 lockedUntil: Date;
78
79 @Column({ nullable: true })
80 lastPasswordChange: Date;
81
82 // Provider information
83 @Column({ type: 'simple-array', default: [] })
84 authProviders: string[]; // ['local', 'google', 'facebook', 'github']
85}1// auth/services/auth.service.ts (extension)
2export class AuthService {
3 // ... previous methods
4
5 async findOrCreateGoogleUser(googleUser: any): Promise<User> {
6 // Check if the user already exists by Google ID
7 let user = await this.userService.findByGoogleId(googleUser.googleId);
8
9 if (user) {
10 // Update the token
11 await this.userService.updateGoogleToken(user.id, googleUser.accessToken);
12 return user;
13 }
14
15 // Check if a user with this email exists
16 user = await this.userService.findByEmail(googleUser.email);
17
18 if (user) {
19 // Link accounts
20 await this.userService.linkGoogleAccount(user.id, googleUser);
21 return user;
22 }
23
24 // Create a new user
25 return await this.userService.createGoogleUser(googleUser);
26 }
27
28 async findOrCreateFacebookUser(facebookUser: any): Promise<User> {
29 let user = await this.userService.findByFacebookId(facebookUser.facebookId);
30
31 if (user) {
32 await this.userService.updateFacebookToken(user.id, facebookUser.accessToken);
33 return user;
34 }
35
36 user = await this.userService.findByEmail(facebookUser.email);
37
38 if (user) {
39 await this.userService.linkFacebookAccount(user.id, facebookUser);
40 return user;
41 }
42
43 return await this.userService.createFacebookUser(facebookUser);
44 }
45
46 async findOrCreateGitHubUser(githubUser: any): Promise<User> {
47 let user = await this.userService.findByGitHubId(githubUser.githubId);
48
49 if (user) {
50 await this.userService.updateGitHubToken(user.id, githubUser.accessToken);
51 return user;
52 }
53
54 user = await this.userService.findByEmail(githubUser.email);
55
56 if (user) {
57 await this.userService.linkGitHubAccount(user.id, githubUser);
58 return user;
59 }
60
61 return await this.userService.createGitHubUser(githubUser);
62 }
63
64 async unlinkProvider(userId: number, provider: string): Promise<void> {
65 const user = await this.userService.findById(userId);
66
67 if (!user) {
68 throw new NotFoundException('User not found');
69 }
70
71 // Check if the user has an alternative login method
72 const remainingProviders = user.authProviders.filter(p => p !== provider);
73
74 if (remainingProviders.length === 0 && !user.password) {
75 throw new BadRequestException('You cannot unlink the last login method!');
76 }
77
78 await this.userService.unlinkProvider(userId, provider);
79 }
80}1// auth/controllers/oauth.controller.ts
2import {
3 Controller,
4 Get,
5 Post,
6 UseGuards,
7 Request,
8 Response,
9 Query,
10 BadRequestException
11} from '@nestjs/common';
12import { AuthService } from '../services/auth.service';
13import { GoogleAuthGuard } from '../guards/google-auth.guard';
14import { FacebookAuthGuard } from '../guards/facebook-auth.guard';
15import { GitHubAuthGuard } from '../guards/github-auth.guard';
16import { JwtAuthGuard } from '../guards/jwt-auth.guard';
17import { CurrentUser } from '../decorators/current-user.decorator';
18import { Public } from '../decorators/public.decorator';
19
20@Controller('auth')
21export class OAuthController {
22 constructor(private authService: AuthService) {}
23
24 // Google OAuth
25 @Public()
26 @Get('google')
27 @UseGuards(GoogleAuthGuard)
28 async googleAuth(@Request() req) {
29 // Will start the OAuth process
30 }
31
32 @Public()
33 @Get('google/callback')
34 @UseGuards(GoogleAuthGuard)
35 async googleAuthRedirect(@Request() req, @Response() res) {
36 const user = req.user;
37 const tokens = await this.authService.generateTokens(user);
38
39 // Redirect with the token (in a real application use secure cookies)
40 const redirectUrl = `${process.env.FRONTEND_URL}/auth/success?token=${tokens.access_token}`1 res.redirect(redirectUrl);
2 }
3
4 // Facebook OAuth
5 @Public()
6 @Get('facebook')
7 @UseGuards(FacebookAuthGuard)
8 async facebookAuth(@Request() req) {
9 // Will start the OAuth process
10 }
11
12 @Public()
13 @Get('facebook/callback')
14 @UseGuards(FacebookAuthGuard)
15 async facebookAuthRedirect(@Request() req, @Response() res) {
16 const user = req.user;
17 const tokens = await this.authService.generateTokens(user);
18
19 const redirectUrl = `${process.env.FRONTEND_URL}/auth/success?token=${tokens.access_token}`;
20 res.redirect(redirectUrl);
21 }
22
23 // GitHub OAuth
24 @Public()
25 @Get('github')
26 @UseGuards(GitHubAuthGuard)
27 async githubAuth(@Request() req) {
28 // Will start the OAuth process
29 }
30
31 @Public()
32 @Get('github/callback')
33 @UseGuards(GitHubAuthGuard)
34 async githubAuthRedirect(@Request() req, @Response() res) {
35 const user = req.user;
36 const tokens = await this.authService.generateTokens(user);
37
38 const redirectUrl = `${process.env.FRONTEND_URL}/auth/success?token=${tokens.access_token}`;
39 res.redirect(redirectUrl);
40 }
41
42 // Link/Unlink accounts
43 @Post('link/google')
44 @UseGuards(JwtAuthGuard)
45 async linkGoogle(@CurrentUser() user: any, @Query('code') code: string) {
46 if (!code) {
47 throw new BadRequestException('Missing authorization code');
48 }
49
50 // Implement account linking
51 return { message: 'Google account has been linked!' };
52 }
53
54 @Post('unlink/:provider')
55 @UseGuards(JwtAuthGuard)
56 async unlinkProvider(
57 @CurrentUser() user: any,
58 @Param('provider') provider: string,
59 ) {
60 await this.authService.unlinkProvider(user.id, provider);
61 return { message: `${provider} account has been unlinked!` };
62 }
63
64 @Get('providers')
65 @UseGuards(JwtAuthGuard)
66 async getLinkedProviders(@CurrentUser() user: any) {
67 const userDetails = await this.userService.findById(user.id);
68 return {
69 providers: userDetails.authProviders,
70 canUnlink: userDetails.authProviders.length > 1 || !!userDetails.password,
71 };
72 }
73}1// auth/strategies/api-key.strategy.ts
2import { Injectable, UnauthorizedException } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { Strategy } from 'passport-custom';
5import { Request } from 'express';
6import { AuthService } from '../services/auth.service';
7
8@Injectable()
9export class ApiKeyStrategy extends PassportStrategy(Strategy, 'api-key') {
10 constructor(private authService: AuthService) {
11 super();
12 }
13
14 async validate(req: Request): Promise<any> {
15 const apiKey = req.headers['x-api-key'] as string;
16
17 if (!apiKey) {
18 throw new UnauthorizedException('Missing API key!');
19 }
20
21 const user = await this.authService.validateApiKey(apiKey);
22
23 if (!user) {
24 throw new UnauthorizedException('Invalid API key!');
25 }
26
27 return user;
28 }
29}1# .env
2# Google OAuth
3GOOGLE_CLIENT_ID=your-google-client-id
4GOOGLE_CLIENT_SECRET=your-google-client-secret
5GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback
6
7# Facebook OAuth
8FACEBOOK_APP_ID=your-facebook-app-id
9FACEBOOK_APP_SECRET=your-facebook-app-secret
10FACEBOOK_CALLBACK_URL=http://localhost:3000/auth/facebook/callback
11
12# GitHub OAuth
13GITHUB_CLIENT_ID=your-github-client-id
14GITHUB_CLIENT_SECRET=your-github-client-secret
15GITHUB_CALLBACK_URL=http://localhost:3000/auth/github/callback
16
17# Frontend URL
18FRONTEND_URL=http://localhost:30011// auth/strategies/session.strategy.ts
2import { Injectable } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { Strategy } from 'passport-local';
5import { AuthService } from '../services/auth.service';
6
7@Injectable()
8export class SessionStrategy extends PassportStrategy(Strategy, 'session') {
9 constructor(private authService: AuthService) {
10 super({
11 usernameField: 'username',
12 passwordField: 'password',
13 });
14 }
15
16 async validate(username: string, password: string): Promise<any> {
17 const user = await this.authService.validateUser(username, password);
18 return user;
19 }
20}
21
22// Session serialization
23export class SessionSerializer {
24 serializeUser(user: any, done: Function) {
25 done(null, user.id);
26 }
27
28 async deserializeUser(userId: number, done: Function) {
29 const user = await this.userService.findById(userId);
30 done(null, user);
31 }
32}1// auth.module.ts (full version)
2import { Module, MiddlewareConsumer } from '@nestjs/common';
3import { JwtModule } from '@nestjs/jwt';
4import { PassportModule } from '@nestjs/passport';
5import { TypeOrmModule } from '@nestjs/typeorm';
6import { ConfigModule } from '@nestjs/config';
7
8import { User } from './entities/user.entity';
9import { AuthService } from './services/auth.service';
10import { UserService } from './services/user.service';
11import { CryptoService } from './services/crypto.service';
12
13import { AuthController } from './controllers/auth.controller';
14import { OAuthController } from './controllers/oauth.controller';
15
16import { LocalStrategy } from './strategies/local.strategy';
17import { JwtStrategy } from './strategies/jwt.strategy';
18import { GoogleStrategy } from './strategies/google.strategy';
19import { FacebookStrategy } from './strategies/facebook.strategy';
20import { GitHubStrategy } from './strategies/github.strategy';
21import { ApiKeyStrategy } from './strategies/api-key.strategy';
22
23import { LocalAuthGuard } from './guards/local-auth.guard';
24import { JwtAuthGuard } from './guards/jwt-auth.guard';
25import { GoogleAuthGuard } from './guards/google-auth.guard';
26import { FacebookAuthGuard } from './guards/facebook-auth.guard';
27import { GitHubAuthGuard } from './guards/github-auth.guard';
28
29import { jwtConfig } from './jwt.config';
30
31@Module({
32 imports: [
33 TypeOrmModule.forFeature([User]),
34 PassportModule.register({ defaultStrategy: 'jwt' }),
35 JwtModule.registerAsync(jwtConfig),
36 ConfigModule,
37 ],
38 controllers: [AuthController, OAuthController],
39 providers: [
40 // Services
41 AuthService,
42 UserService,
43 CryptoService,
44
45 // Strategies
46 LocalStrategy,
47 JwtStrategy,
48 GoogleStrategy,
49 FacebookStrategy,
50 GitHubStrategy,
51 ApiKeyStrategy,
52
53 // Guards
54 LocalAuthGuard,
55 JwtAuthGuard,
56 GoogleAuthGuard,
57 FacebookAuthGuard,
58 GitHubAuthGuard,
59 ],
60 exports: [
61 AuthService,
62 UserService,
63 JwtAuthGuard,
64 LocalAuthGuard,
65 ],
66})
67export class AuthModule {}Implement additional strategies:
1// Your code here
2// 1. Discord OAuth Strategy
3// 2. Twitter OAuth Strategy
4// 3. LinkedIn OAuth Strategy
5// 4. Microsoft OAuth Strategy
6// 5. Custom LDAP Strategy for corporate accountsCongratulations! You have been appointed chief diplomatic guard! Now you can:
✓ Configure Passport.js with different strategies ✓ Implement OAuth with Google, Facebook, GitHub ✓ Link accounts from different providers ✓ Create custom strategies for special cases ✓ Manage multiple authentication methods ✓ Handle redirects and OAuth callbacks
Now your legion can welcome guests from all over the world!