Skip to content
← Back to blog
NestJSAngularAuthenticationJWTOAuthTutorial

NestJS + Angular Authentication: JWT, OAuth, and Refresh Tokens

Firas Sayah·April 28, 2026·6 min read

Auth Is the First Feature, and the Hardest to Get Right

Every SaaS needs authentication. It's also where most security vulnerabilities live. This guide covers a production-ready auth system using NestJS and Angular — the same approach used in SaaS Starter.

Architecture Overview

The auth system has four layers:

  1. JWT access tokens — Short-lived (15 min), sent in Authorization headers
  2. Refresh tokens — Long-lived (7 days), stored in HTTP-only cookies
  3. Google OAuth — Social login via Passport.js
  4. Angular guards — Route protection on the frontend

Cloudrix SaaS Starter ships with this pre-configured and tested.

Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.

Try the live demo →

Step 1: JWT Strategy (NestJS)

Install Passport and JWT packages:

npm install @nestjs/passport @nestjs/jwt passport passport-jwt

Create the JWT strategy:

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private config: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: config.get('JWT_SECRET'),
      ignoreExpiration: false,
    });
  }

  async validate(payload: JwtPayload) {
    return { id: payload.sub, email: payload.email, role: payload.role };
  }
}

The key decisions here: extract from the Authorization header (not cookies — that opens CSRF issues), validate the payload structure, and attach the user object to the request.

Step 2: Refresh Token Rotation

Access tokens expire in 15 minutes. Refresh tokens let users stay logged in without re-entering credentials. The critical security pattern is rotation — every time a refresh token is used, issue a new one and invalidate the old one:

@Injectable()
export class AuthService {
  async refreshTokens(userId: string, refreshToken: string) {
    const user = await this.userService.findById(userId);
    if (!user || !user.hashedRefreshToken) {
      throw new ForbiddenException('Access denied');
    }

    const tokenMatches = await bcrypt.compare(
      refreshToken,
      user.hashedRefreshToken,
    );
    if (!tokenMatches) throw new ForbiddenException('Access denied');

    const tokens = await this.generateTokens(user.id, user.email);
    await this.updateRefreshToken(user.id, tokens.refreshToken);
    return tokens;
  }

  private async generateTokens(userId: string, email: string) {
    const [accessToken, refreshToken] = await Promise.all([
      this.jwtService.signAsync(
        { sub: userId, email },
        { secret: this.config.get('JWT_SECRET'), expiresIn: '15m' },
      ),
      this.jwtService.signAsync(
        { sub: userId, email },
        { secret: this.config.get('JWT_REFRESH_SECRET'), expiresIn: '7d' },
      ),
    ]);
    return { accessToken, refreshToken };
  }
}

Why rotation matters: If a refresh token is stolen, the attacker can use it once. When the legitimate user's next refresh fails (because the token was already rotated), you know the token was compromised and can invalidate all sessions.

Step 3: Google OAuth

Add Google OAuth with Passport:

npm install passport-google-oauth20
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(private config: ConfigService) {
    super({
      clientID: config.get('GOOGLE_CLIENT_ID'),
      clientSecret: config.get('GOOGLE_CLIENT_SECRET'),
      callbackURL: config.get('GOOGLE_CALLBACK_URL'),
      scope: ['email', 'profile'],
    });
  }

  async validate(accessToken: string, refreshToken: string, profile: any) {
    return {
      email: profile.emails[0].value,
      name: profile.displayName,
      googleId: profile.id,
    };
  }
}

The OAuth controller handles the redirect flow:

@Controller('auth')
export class AuthController {
  @Get('google')
  @UseGuards(AuthGuard('google'))
  googleLogin() {}

  @Get('google/callback')
  @UseGuards(AuthGuard('google'))
  async googleCallback(@Req() req, @Res() res: Response) {
    const tokens = await this.authService.googleLogin(req.user);
    res.cookie('refreshToken', tokens.refreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
    });
    res.redirect(`${this.config.get('APP_URL')}/auth/callback?token=${tokens.accessToken}`);
  }
}

Step 4: Angular Auth Guard

On the frontend, protect routes with a functional guard:

export const authGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isAuthenticated()) {
    return true;
  }

  router.navigate(['/login']);
  return false;
};

Step 5: Angular HTTP Interceptor

Attach the access token to every API request and handle token refresh automatically:

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getAccessToken();

  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
  }

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        return authService.refreshToken().pipe(
          switchMap((newToken) => {
            req = req.clone({
              setHeaders: { Authorization: `Bearer ${newToken}` },
            });
            return next(req);
          }),
        );
      }
      return throwError(() => error);
    }),
  );
};

This interceptor transparently refreshes expired tokens. Users never see a login screen mid-session.

Security Checklist

See how SaaS Starter implements all of this in the authentication feature page.

  • Store refresh tokens in HTTP-only, secure, SameSite cookies
  • Hash refresh tokens in the database (never store plaintext)
  • Rotate refresh tokens on every use
  • Set short access token expiry (15 minutes)
  • Validate email ownership before linking OAuth accounts
  • Rate-limit login and refresh endpoints

Skip the Implementation

The auth system above takes 1-2 weeks to build, test, and harden. SaaS Starter Pro ships it fully implemented — JWT, Google OAuth, refresh token rotation, Angular guards, interceptors, and the security hardening you'd otherwise forget.

Get Started Today

All of these patterns are fully implemented in SaaS Starter. Skip 2-6 weeks of setup — auth, payments, multi-tenancy, deployment all done.

F

Firas Sayah

Senior Software Engineer

Full-stack developer with 5+ years building production SaaS applications with NestJS and Angular.