OAuth — Sign In With Google

Let a Third Party Vouch For Your Users

OAuth — Sign In With Google

OAuth lets users sign in with an existing account at Google, GitHub, or anywhere else. The Passport strategy makes the wiring straightforward.

4 min read Level 3/5 #nestjs#oauth#auth
What you'll learn
  • Register an OAuth app with the provider
  • Implement a GoogleStrategy
  • Wire callback route and issue a session or JWT

OAuth shifts the password problem to someone else’s servers. Your app never sees the user’s credentials — it just trusts the access token that comes back. The pattern is the same for Google, GitHub, Microsoft, and the rest.

Register the OAuth App

Before any code, register your app with the provider (Google Cloud Console, GitHub Developer Settings, etc.). You’ll get:

  • A client ID (public)
  • A client secret (keep this in env vars)
  • A callback URL — the route on your server the provider will redirect to after the user authorizes.

Use https://your-domain.com/auth/google/callback in production and http://localhost:3000/auth/google/callback in dev.

The Google Strategy

npm i passport-google-oauth20
npm i -D @types/passport-google-oauth20
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, Profile } from 'passport-google-oauth20';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(cfg: ConfigService) {
    super({
      clientID: cfg.get<string>('GOOGLE_CLIENT_ID'),
      clientSecret: cfg.get<string>('GOOGLE_CLIENT_SECRET'),
      callbackURL: cfg.get<string>('GOOGLE_CALLBACK_URL'),
      scope: ['profile', 'email'],
    });
  }

  async validate(_accessToken: string, _refreshToken: string, profile: Profile) {
    return {
      email: profile.emails?.[0]?.value,
      name: profile.displayName,
      provider: 'google',
      providerId: profile.id,
    };
  }
}

The second argument to PassportStrategy(Strategy, 'google') names the strategy. That’s the name guards will use.

Two Routes: Start and Callback

The first route kicks off the redirect to Google. The second handles the return trip.

@Controller('auth')
export class AuthController {
  constructor(private readonly auth: AuthService) {}

  @Get('google')
  @UseGuards(AuthGuard('google'))
  start() { /* AuthGuard redirects, this body never runs */ }

  @Get('google/callback')
  @UseGuards(AuthGuard('google'))
  async callback(@Request() req, @Res({ passthrough: true }) res) {
    const user = await this.auth.upsertOAuthUser(req.user);
    const token = await this.auth.signJwt(user);
    res.cookie('access_token', token, { httpOnly: true, secure: true });
    return res.redirect('/');
  }
}

In the callback, req.user is whatever your validate() returned. The typical next step is: find or create a local user record, then issue your JWT — OAuth is for sign-in, not for ongoing API auth.

A Note on Refresh Tokens

If you’ll call Google APIs as the user later (Drive, Calendar), pass accessType: 'offline' and store the refresh token. If you only need sign-in, you can throw both tokens away after the callback.

OpenAPI & Swagger →