A citizen from the province of Gaul comes to the camp gate. You can make him create a new account: invent a password, confirm an address, go through the whole recruitment procedure. Or you can strike an alliance with Gaul: her officials will confirm the newcomer's identity, and you will accept that confirmation as your own.
The second way is called OAuth 2.0 - and it is what sits behind the "Sign in with Google" button you know from hundreds of sites. It is taught at this point for one reason: the citizen's password never enters your camp. You do not have to store it, hash it or protect it from leaking, because you never see it.
Before we look at code, let's follow what happens at that button - because this sequence is the heart of OAuth, and the code merely serves it.
Step four is the one most often forgotten. Google confirms identity once, at the entrance. All the application's later work rests on your own pass - exactly the kind you met in the lesson on JWT.
The code for this exchange is taken on by a Passport strategy. You know the skeleton; only the body of
validate is new:1@Injectable()
2export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
3 constructor(
4 private configService: ConfigService,
5 private usersService: UsersService,
6 ) {
7 super({
8 clientID: configService.get('GOOGLE_CLIENT_ID'),
9 clientSecret: configService.get('GOOGLE_CLIENT_SECRET'),
10 callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
11 scope: ['email', 'profile'],
12 });
13 }
14
15 async validate(accessToken: string, refreshToken: string, profile: any) {
16 const { name, emails } = profile;
17
18 return this.usersService.findOrCreateSocialUser({
19 email: emails[0].value,
20 firstName: name.givenName,
21 provider: 'google',
22 providerId: profile.id,
23 });
24 }
25}In
super() we describe the alliance itself. clientID and clientSecret are your application's papers issued by Google - read from configuration, never written into the code. callbackURL is the address from step three, where Google will send the citizen back; you must register that same address in the Google console, or the redirect will be rejected. scope states what you are asking for - here only an email address and a basic profile.The rule about
scope is short: ask for the minimum. Every extra permission is one more consent screen for the user and one more thing you have to protect.The
validate method runs only after the return from Google, with the citizen's profile ready. The returned object lands - as in every strategy - in request.user.Note the
provider and providerId pair in the data we pass on. That pair replaces the password.providerId is the citizen's identifier in Google's system - unchanging, unlike an email address, which can be edited. But providerId alone is not enough, because a citizen with the same number may exist at another provider. So we remember the pair: who confirmed and whom they confirmed.1async findOrCreateSocialUser(data: SocialUserDto): Promise<User> {
2 const existing = await this.usersRepository.findOne({
3 where: { provider: data.provider, providerId: data.providerId },
4 });
5
6 if (existing) {
7 return existing;
8 }
9
10 return this.usersRepository.save({
11 email: data.email,
12 firstName: data.firstName,
13 provider: data.provider,
14 providerId: data.providerId,
15 });
16}The name
findOrCreate describes the whole logic: on the first login we create the entry, on every later one we find it. Note what this entity lacks - a password column. A citizen admitted through the alliance never supplied a password, so there is nothing to store. That is in fact an easy trap to fall into: if your password column is required (nullable: false), saving such a user will fail.On the controller's side the alliance comes down to two addresses:
1@Controller('auth')
2export class AuthController {
3 @Get('google')
4 @UseGuards(AuthGuard('google'))
5 googleLogin() {
6 // the redirect is performed by the guard
7 }
8
9 @Get('google/callback')
10 @UseGuards(AuthGuard('google'))
11 googleCallback(@Request() req) {
12 return this.authService.generateToken(req.user);
13 }
14}The first endpoint has an empty body, and that is not a mistake - its only job is to trigger the guard, which redirects the user to Google. Code in this method would never run.
The second is the address from
callbackURL. When the citizen returns, the guard exchanges the code for a profile, the strategy saves or finds the user, and at the end we issue our own JWT. And here step four closes: from this moment the application no longer needs Google.The alliance is struck, the gate is open to citizens of neighbouring provinces:
clientID and clientSecret are read from configuration, callbackURL must match the address registered with the provider,scope, ask for the minimum data you need,provider + providerId pair - it replaces the password, because emails get changed,findOrCreate creates the entry on the first login and finds it on every later one,password column cannot be required,In the next lesson we will turn to sessions and cookies, the other road to the fort - the stateful alternative to passes. For now remember: OAuth is an alliance in which a neighbour vouches for the newcomer; you accept that vouching once and immediately issue a document of your own.