We use cookies to enhance your experience on the site
CodeWorlds

Passport.js - access control system

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!

What is Passport.js?

Imagine you are a diplomatic guard at the largest Roman fort. Every day, envoys arrive from different countries:

  • Spaniards with their own royal documents
  • English with Crown seals
  • Dutch with East India Company certificates
  • French with royal letters of safe conduct
  • Local legionaries with traditional passwords

Passport.js is a framework that allows:

  • Unified interface for different authentication methods
  • Strategies for each type of login
  • Middleware easily integrated with Express/NestJS
  • Over 500+ ready-made strategies for different providers

Basic strategies in NestJS

Local Strategy - traditional passwords

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}

Local Auth Guard

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}

Google OAuth Strategy

1# Installation
2npm install passport-google-oauth20
3npm install --save-dev @types/passport-google-oauth20
1// 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}

Google Auth Guard

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}

Facebook Strategy

1# Installation
2npm install passport-facebook
3npm install --save-dev @types/passport-facebook
1// 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}

GitHub Strategy

1npm install passport-github2
2npm install --save-dev @types/passport-github2
1// 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}

Extended User Entity

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}

Extended Auth Service

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}

OAuth Controller

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}

Custom Strategy - API Key

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}

Environment Variables

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:3001

Session Strategy (alternative to JWT)

1// 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}

Complete module configuration

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 {}

Practical exercise

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 accounts

Summary

Congratulations! 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!

Go to CodeWorlds