We use cookies to enhance your experience on the site
CodeWorlds

OAuth and Social Login - alliances of the Empire

Diplomat of the Empire! Architect Vitruvius introduces you to the art of forming alliances with other Roman provinces. In the digital world, this means OAuth and Social Login - allowing citizens from other provinces (Google, Facebook, GitHub) to securely enter your legion's system!

What is OAuth?

OAuth is like a diplomatic agreement between provinces of the Empire - it allows citizens from one province (application) to securely use resources of another province without revealing their secrets (passwords).

Imagine your legion forms an alliance with the Google province. When a new citizen wants to join your legion, they can prove their identity through their home province instead of creating a new account in your camp.

Implementing Google OAuth

Installation and configuration

1// Package installation
2// npm install @nestjs/passport passport passport-google-oauth20
3// npm install -D @types/passport-google-oauth20
4
5// google.strategy.ts
6import { Injectable } from '@nestjs/common';
7import { PassportStrategy } from '@nestjs/passport';
8import { Strategy, VerifyCallback } from 'passport-google-oauth20';
9import { ConfigService } from '@nestjs/config';
10
11@Injectable()
12export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
13 constructor(
14 private configService: ConfigService,
15 private usersService: UsersService,
16 ) {
17 super({
18 clientID: configService.get('GOOGLE_CLIENT_ID'),
19 clientSecret: configService.get('GOOGLE_CLIENT_SECRET'),
20 callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
21 scope: ['email', 'profile'],
22 });
23 }
24
25 async validate(
26 accessToken: string,
27 refreshToken: string,
28 profile: any,
29 done: VerifyCallback,
30 ): Promise<any> {
31 const { name, emails, photos } = profile;
32 
33 const citizenData = {
34 email: emails[0].value,
35 firstName: name.givenName,
36 lastName: name.familyName,
37 picture: photos[0].value,
38 accessToken,
39 refreshToken,
40 provider: 'google',
41 providerId: profile.id,
42 };
43
44 // Find or create the citizen in the legion database
45 const citizen = await this.usersService.findOrCreateSocialUser(citizenData);
46
47 console.log('Citizen from Google joined the legion:', citizen.email);
48
49 done(null, citizen);
50 }
51}

Social user management service

1// users.service.ts
2@Injectable()
3export class UsersService {
4 constructor(
5 @InjectRepository(User) private usersRepository: Repository<User>,
6 private jwtService: JwtService,
7 ) {}
8
9 async findOrCreateSocialUser(socialData: any): Promise<User> {
10 // Search for user by email or provider ID
11 let user = await this.usersRepository.findOne({
12 where: [
13 { email: socialData.email },
14 { 
15 provider: socialData.provider, 
16 providerId: socialData.providerId 
17 },
18 ],
19 });
20
21 if (user) {
22 // Update data from the provider
23 user.firstName = socialData.firstName;
24 user.lastName = socialData.lastName;
25 user.picture = socialData.picture;
26 user.lastLoginAt = new Date();
27 
28 await this.usersRepository.save(user);
29 
30 console.log('Return of a known citizen:', user.email);
31 } else {
32 // Create a new citizen of the Empire
33 user = this.usersRepository.create({
34 email: socialData.email,
35 firstName: socialData.firstName,
36 lastName: socialData.lastName,
37 picture: socialData.picture,
38 provider: socialData.provider,
39 providerId: socialData.providerId,
40 role: 'miles', // Default rank - soldier
41 emailVerified: true, // OAuth providers verify email
42 createdAt: new Date(),
43 lastLoginAt: new Date(),
44 });
45
46 await this.usersRepository.save(user);
47
48 console.log('New citizen from', socialData.provider, ':', user.email);
49 }
50
51 return user;
52 }
53
54 async generateSocialLoginTokens(user: User) {
55 const payload = {
56 sub: user.id,
57 email: user.email,
58 role: user.role,
59 provider: user.provider,
60 };
61
62 const accessToken = this.jwtService.sign(payload, { expiresIn: '1h' });
63 const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
64
65 // Save refresh token in the database
66 await this.usersRepository.update(user.id, {
67 refreshToken: await bcrypt.hash(refreshToken, 10),
68 lastLoginAt: new Date(),
69 });
70
71 return { accessToken, refreshToken };
72 }
73}

OAuth Controller

1// auth.controller.ts
2@Controller('auth')
3export class AuthController {
4 constructor(
5 private authService: AuthService,
6 private usersService: UsersService,
7 ) {}
8
9 // Start the Google login process
10 @Get('google')
11 @UseGuards(AuthGuard('google'))
12 async googleAuth(@Req() req) {
13 // Passport will automatically redirect to Google
14 }
15
16 // Callback after Google authorization
17 @Get('google/callback')
18 @UseGuards(AuthGuard('google'))
19 async googleAuthRedirect(@Req() req, @Res() res) {
20 try {
21 const user = req.user;
22 const tokens = await this.usersService.generateSocialLoginTokens(user);
23 
24 // Redirect to frontend with tokens
25 const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
26 const redirectUrl = `${frontendUrl}/auth/callback?` +
27 `token=${tokens.accessToken}&` +
28 `refresh=${tokens.refreshToken}&` +
29 `user=${encodeURIComponent(JSON.stringify({
30 id: user.id,
31 email: user.email,
32 name: `${user.firstName} ${user.lastName}`,
33 picture: user.picture,
34 provider: user.provider
35 }))}`
36
37 res.redirect(redirectUrl);
38 } catch (error) {
39 console.error('Error during OAuth callback:', error);
40 res.redirect('/auth/error?message=oauth_failed');
41 }
42 }
43
44 // Endpoint for logging in through different providers
45 @Post('social/login')
46 async socialLogin(@Body() socialLoginDto: SocialLoginDto) {
47 const user = await this.usersService.findOrCreateSocialUser(socialLoginDto);
48 const tokens = await this.usersService.generateSocialLoginTokens(user);
49 
50 return {
51 message: 'Successful login through the Empire alliance!',
52 user: {
53 id: user.id,
54 email: user.email,
55 name: `${user.firstName} ${user.lastName}`,
56 picture: user.picture,
57 provider: user.provider,
58 role: user.role,
59 },
60 tokens,
61 };
62 }
63}

Facebook OAuth Strategy

1// facebook.strategy.ts
2import { Injectable } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { Strategy, Profile } from 'passport-facebook';
5
6@Injectable()
7export class FacebookStrategy extends PassportStrategy(Strategy, 'facebook') {
8 constructor(
9 private configService: ConfigService,
10 private usersService: UsersService,
11 ) {
12 super({
13 clientID: configService.get('FACEBOOK_APP_ID'),
14 clientSecret: configService.get('FACEBOOK_APP_SECRET'),
15 callbackURL: configService.get('FACEBOOK_CALLBACK_URL'),
16 scope: 'email',
17 profileFields: ['emails', 'name', 'picture.type(large)'],
18 });
19 }
20
21 async validate(
22 accessToken: string,
23 refreshToken: string,
24 profile: Profile,
25 done: Function,
26 ): Promise<any> {
27 const { name, emails, photos } = profile;
28 
29 const citizenData = {
30 email: emails[0].value,
31 firstName: name.givenName,
32 lastName: name.familyName,
33 picture: photos[0].value,
34 accessToken,
35 refreshToken,
36 provider: 'facebook',
37 providerId: profile.id,
38 };
39
40 const citizen = await this.usersService.findOrCreateSocialUser(citizenData);
41
42 console.log('Citizen from Facebook joined the legion:', citizen.email);
43
44 done(null, citizen);
45 }
46}

GitHub OAuth Strategy

1// github.strategy.ts
2import { Injectable } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { Strategy, Profile } from 'passport-github2';
5
6@Injectable()
7export class GitHubStrategy extends PassportStrategy(Strategy, 'github') {
8 constructor(
9 private configService: ConfigService,
10 private usersService: UsersService,
11 ) {
12 super({
13 clientID: configService.get('GITHUB_CLIENT_ID'),
14 clientSecret: configService.get('GITHUB_CLIENT_SECRET'),
15 callbackURL: configService.get('GITHUB_CALLBACK_URL'),
16 scope: ['user:email'],
17 });
18 }
19
20 async validate(
21 accessToken: string,
22 refreshToken: string,
23 profile: Profile,
24 done: Function,
25 ): Promise<any> {
26 const { username, emails, photos, displayName } = profile;
27 
28 const names = displayName?.split(' ') || [username];
29 
30 const citizenData = {
31 email: emails[0].value,
32 firstName: names[0],
33 lastName: names.slice(1).join(' ') || '',
34 picture: photos[0].value,
35 username,
36 accessToken,
37 refreshToken,
38 provider: 'github',
39 providerId: profile.id,
40 };
41
42 const citizen = await this.usersService.findOrCreateSocialUser(citizenData);
43
44 console.log('Citizen from GitHub joined the legion:', citizen.email);
45
46 done(null, citizen);
47 }
48}

OAuth is a powerful tool for building cross-platform alliances! Thanks to it, citizens from different provinces of the Empire can easily join your legion using identities from trusted allies.

In the next exercise, we will learn advanced password hashing techniques - the art of creating unbreakable vault codes of the Empire!

Go to CodeWorlds