We use cookies to enhance your experience on the site
CodeWorlds

OAuth and Social Login - the alliances of the Empire

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.

The four steps of the alliance

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.

  1. The citizen clicks "Sign in with Google" on your page.
  2. Your application redirects him to Google. From that moment he is talking to Google, not to you - and there, on Google's page, he enters his password.
  3. Google sends him back to an agreed address, attaching a one-time code. Your server exchanges that code for the citizen's data.
  4. Your application issues its own JWT - from now on the citizen moves around the camp on your pass, and Google's role ends.

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 strategy - handling the return from Gaul

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
.

Two identifiers instead of a password

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.

Two endpoints closing the loop

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.

Summary

The alliance is struck, the gate is open to citizens of neighbouring provinces:

  • OAuth 2.0 allows logging in through an external service, and the password never enters your application,
  • the flow has four steps: the click, the redirect to the provider, the return with a code, and issuing your own JWT,
  • the provider's role ends after login - from there your pass takes over,
  • clientID
    and
    clientSecret
    are read from configuration,
    callbackURL
    must match the address registered with the provider,
  • in
    scope
    , ask for the minimum data you need,
  • you remember identity as the
    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,
  • an OAuth user has no password, so the
    password
    column cannot be required,
  • the starting endpoint has an empty body - all the work is done by the guard.

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.

Go to CodeWorlds