NestJS Authentication with OAuth2.0: Adding External Providers
Tags: typescript, nestjs, node, tutorial
Published: 2023-04-14 22:11:24

## Series Intro

This series will cover the full implementation of OAuth2.0 Authentication in [NestJS](https://nestjs.com/) for the following types of APIs:

- [Express](https://expressjs.com/) REST API;
- [Fastify](https://www.fastify.io/) REST API;
- [Apollo GraphQL](https://www.apollographql.com/docs/apollo-server/) API.

And it is divided in 5 parts:

- Configuration and operations;
- Express Local OAuth REST API;
- Fastify Local OAuth REST API;
- Apollo Local OAuth GraphQL API;
- Adding External OAuth Providers to our API;

Lets start the fifth and last part of this series.

## Tutorial Intro

On this tutorial we will add to the Fastify API from [the third part](https://dev.to/tugascript/nestjs-authentication-with-oauth20-fastify-local-oauth-5gn9) the capability of signing in with external OAuth 2.0 providers such as Microsoft, Google, Facebook and GitHub.

This tutorial will be slightly more complex than the previous ones, as on the one hand, we need an implementation that works for both adapters (Fastify and Express), and on the other hand it should apply to the maximum number of external providers.


<small><b>TLDR:</b> if you do not have 45 minutes to read the article, the source code can be found on this [repo](https://github.com/tugascript/nestjs-oauth/tree/external-providers). While you at it if you liked what you read consider [buying me a coffee](https://www.buymeacoffee.com/barracha).</small>


### The Flow

The flow is a bit complex and requires access to the front-end, as we will need to be able to redirect back to it:

![OAuth Flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4mm7k4kumzbnmvfhhr0.png)

**Note**: Apple's flow is a bit different and will not be covered in this article.

### Options

There are several ways to implement this flow in NestJS, for example this are my recommended 3 ways:

1. Use adapter specific packages:
    - [Passport](https://www.passportjs.org/) for Express;
    - [Fastify-OAuth2](https://github.com/fastify/fastify-oauth2) for Fastify;
2. Implement an NestJS External OAuth Provider, using [dynamic modules](https://docs.nestjs.com/fundamentals/dynamic-modules);
3. Implementing External OAuth Routes under a feature flags.

My preferred approach is the 3rd one, and that is the one we will implement on this tutorial.

### Approach

We will:

- Allow for multiple types of authentication for each user;
- Only require confirmation when the user was registed through Local OAuth;
- Users can create a password for Local OAuth even if they registered with an external provider.

Since the setup is quite different, we will separate the Local OAuth and External OAuth into distinct modules.

This tutorial only covers the code and API logic, it will not show you how to register your app on Microsoft, Google, Facebook or GitHub. I will assume that you already know how to do that, if you do not, you can find how in each providers' official documentation.

## Set up

Start by upgrading the library to the latest npm package versions:

```bash
$ yarn upgrade-interactive
```

<small>**NOTE**: if you update Mikro-ORM to the latest version you will need to update the imports on the `common.service.ts` to the `EntityRepository` of the `@mikro-orm/postgresql` package.</small>

To facilitate our implementation we will use the [simple-oauth2](https://github.com/lelylan/simple-oauth2) library, so start by installing it:

```bash
$ yarn add simple-oauth2
$ yarn add -D @types/simple-oauth2
```

And for API calls we will use [Axios](https://axios-http.com/), nest has its own [Axios integration](https://docs.nestjs.com/techniques/http-module), so install them as well:

```bash
$ yarn add @nestjs/axios axios
```

## Changes

There are several changes that we need to make on our previous API implementation.

## User changes

### Entities

#### OAuth Provider Entity

First we need an `enum` with the possible providers, for this example we will allow 5 providers. Create the `oauth-providers.enum.ts` on the `enums` directory:

```ts
export enum OAuthProvidersEnum {
  LOCAL = 'local',
  MICROSOFT = 'microsoft',
  GOOGLE = 'google',
  FACEBOOK = 'facebook',
  GITHUB = 'github',
}
```

Since we will allow multiple providers we need to create a `OAuthProviderEntity` to keep track of the user's active providers. For simplicity we will not allow the user to manually to disconnect from the providers. 

Create the `oauth-provider.entity.ts`:

```ts
import {
  Entity,
  Enum,
  ManyToOne,
  PrimaryKeyType,
  Property,
  Unique,
} from '@mikro-orm/core';
import { IsEnum } from 'class-validator';
import { OAuthProvidersEnum } from '../enums/oauth-providers.enum';
import { IOAuthProvider } from '../interfaces/oauth-provider.interface';
import { UserEntity } from './user.entity';

@Entity({ tableName: 'oauth_providers' })
@Unique({ properties: ['provider', 'user'] })
export class OAuthProviderEntity implements IOAuthProvider {
  @Enum({
    items: () => OAuthProvidersEnum,
    primary: true,
    columnType: 'varchar',
    length: 9,
  })
  @IsEnum(OAuthProvidersEnum)
  public provider: OAuthProvidersEnum;

  @ManyToOne({
    entity: () => UserEntity,
    primary: true,
    onDelete: 'cascade',
  })
  public user: UserEntity;

  @Property({ onCreate: () => new Date() })
  public createdAt: Date = new Date();

  @Property({ onUpdate: () => new Date() })
  public updatedAt: Date = new Date();

  [PrimaryKeyType]?: [OAuthProvidersEnum, number];
}
```

And import it on the `UsersModule`:

```ts
// ...
import { OAuthProviderEntity } from './entities/oauth-provider.entity';
// ...

@Module({
  imports: [MikroOrmModule.forFeature([UserEntity, OAuthProviderEntity])],
  // ...
})
export class UsersModule {}
```

Finally inject it into the `UserService`:

```ts
//...  
import { OAuthProviderEntity } from './entities/oauth-provider.entity';
// ...

@Injectable()
export class UsersService {
  constructor(
    // ...
    @InjectRepository(OAuthProviderEntity)
    private readonly oauthProvidersRepository: EntityRepository<OAuthProviderEntity>,
    // ...
  ) {}

  // ...
}
```

#### User Entity

For the user entity we need to make some changes. Now the Password is not mandatory, so change the `BCRYPT_HASH` regex into `BCRYPT_HASH_OR_USET`:

```ts
export const BCRYPT_HASH_OR_UNSET =
  /(UNSET|(\$2[abxy]?\$\d{1,2}\$[A-Za-z\d\./]{53}))/;
```

And add the one-to-many relation for the `OAuthProviderEntity`:

```ts
import {
  Collection,
  // ...
  OneToMany,
  // ...
} from '@mikro-orm/core';
// ...
import { OAuthProviderEntity } from './oauth-provider.entity';

@Entity({ tableName: 'users' })
export class UserEntity implements IUser {
  // ...

  @OneToMany(() => OAuthProviderEntity, (oauth) => oauth.user)
  public oauthProviders = new Collection<OAuthProviderEntity, UserEntity>(this);
}
```

Because of the external providers the users now can be confirmed automatically, so update the `CredentialsEmbeddable`:

```ts
import { Embeddable, Property } from '@mikro-orm/core';
import dayjs from 'dayjs';
import { ICredentials } from '../interfaces/credentials.interface';

@Embeddable()
export class CredentialsEmbeddable implements ICredentials {
  @Property({ default: 0 })
  public version: number = 0;

  @Property({ default: '' })
  public lastPassword: string = '';

  @Property({ default: dayjs().unix() })
  public passwordUpdatedAt: number = dayjs().unix();

  @Property({ default: dayjs().unix() })
  public updatedAt: number = dayjs().unix();

  constructor(isConfirmed = false) {
    this.version = isConfirmed ? 1 : 0;
  }

  public updatePassword(password: string): void {
    this.version++;
    this.lastPassword = password;
    const now = dayjs().unix();
    this.passwordUpdatedAt = now;
    this.updatedAt = now;
  }

  public updateVersion(): void {
    this.version++;
    this.updatedAt = dayjs().unix();
  }
}
```

### Service

When we create an user, we need to create an `OAuthProvider` as well, therefore create the `createOAuthProvider` private method:

```ts
// ...
import { OAuthProvidersEnum } from './enums/oauth-providers.enum';

@Injectable()
export class UsersService {
  // ...

  private async createOAuthProvider(
    provider: OAuthProvidersEnum,
    userId: number,
  ): Promise<OAuthProviderEntity> {
    const oauthProvider = this.oauthProvidersRepository.create({
      provider,
      user: userId,
    });
    await this.commonService.saveEntity(
      this.oauthProvidersRepository,
      oauthProvider,
      true,
    );
    return oauthProvider;
  }
}
```

And update the `create` public method, adding a new `provider` argument, making the `password` an optional argument, and confirming the user if the provider is not local:

```ts
// ...

@Injectable()
export class UsersService {
  // ...

  public async create(
    provider: OAuthProvidersEnum,
    email: string,
    name: string,
    password?: string,
  ): Promise<UserEntity> {
    const isConfirmed = provider !== OAuthProvidersEnum.LOCAL;
    const formattedEmail = email.toLowerCase();
    await this.checkEmailUniqueness(formattedEmail);
    const formattedName = this.commonService.formatName(name);
    const user = this.usersRepository.create({
      email: formattedEmail,
      name: formattedName,
      username: await this.generateUsername(formattedName),
      password: isUndefined(password) ? 'UNSET' : await hash(password, 10),
      confirmed: isConfirmed,
      credentials: new CredentialsEmbeddable(isConfirmed),
    });
    await this.commonService.saveEntity(this.usersRepository, user, true);
    await this.createOAuthProvider(provider, user.id);
    return user;
  }
}
```

To be able to accept external OAuth we need to be able to get or create a new user, so create the `findOrCreate` public method:

```ts
// ...

@Injectable()
export class UsersService {
  // ...

  public async findOrCreate(
    provider: OAuthProvidersEnum,
    email: string,
    name: string,
  ): Promise<UserEntity> {
    const formattedEmail = email.toLowerCase();
    const user = await this.usersRepository.findOne(
      {
        email: formattedEmail,
      },
      {
        populate: ['oauthProviders'],
      },
    );

    if (isUndefined(user) || isNull(user)) {
      return this.create(provider, email, name);
    }
    if (
      isUndefined(
        user.oauthProviders.getItems().find((p) => p.provider === provider),
      )
    ) {
      await this.createOAuthProvider(provider, user.id);
    }

    return user;
  }

  // ...
}
```

Both Update and Reset Password have to take into account external providers, hence we need to refactor them a bit, start by extracting the password change logic into its own method:

```ts
// ...

@Injectable()
export class UsersService {
  // ...

  private async changePassword(
    user: UserEntity,
    password: string,
  ): Promise<UserEntity> {
    user.credentials.updatePassword(user.password);
    user.password = await hash(password, 10);
    await this.commonService.saveEntity(this.usersRepository, user);
    return user;
  }

  // ...
}
```

Next, update the `updatePassword` and `resetPassword` public methods:

```ts
// ...

@Injectable()
export class UsersService {
  // ...

  public async updatePassword(
    userId: number,
    newPassword: string,
    password?: string,
  ): Promise<UserEntity> {
    const user = await this.findOneById(userId);

    if (user.password === 'UNSET') {
      await this.createOAuthProvider(OAuthProvidersEnum.LOCAL, user.id);
    } else {
      if (isUndefined(password) || isNull(password)) {
        throw new BadRequestException('Password is required');
      }
      if (!(await compare(password, user.password))) {
        throw new BadRequestException('Wrong password');
      }
      if (await compare(newPassword, user.password)) {
        throw new BadRequestException('New password must be different');
      }
    }

    return await this.changePassword(user, newPassword);
  }

  public async resetPassword(
    userId: number,
    version: number,
    password: string,
  ): Promise<UserEntity> {
    const user = await this.findOneByCredentials(userId, version);
    return await this.changePassword(user, password);
  }

  // ...
}
```

As a last update, just add a `findOAuthProviders` so users can know how many providers they have active:

```ts
// ...

@Injectable()
export class UsersService {
  // ...

  public async findOAuthProviders(
    userId: number,
  ): Promise<OAuthProviderEntity[]> {
    return await this.oauthProvidersRepository.find(
      {
        user: userId,
      },
      { orderBy: { provider: QueryOrder.ASC } },
    );
  }
  // ...
}
```

## Jwt Changes

There is just one minor change, as we need a private method from the `AuthService` to be shared between it and the future `Oauth2Service` for external providers.

### Service

Move the `AuthService`'s `generateAuthTokens` private method to the `JwtService` so we can share its logic with the `Oauth2Service`:

```ts
// ...

@Injectable()
export class JwtService {
  // ...

  public async generateAuthTokens(
    user: IUser,
    domain?: string,
    tokenId?: string,
  ): Promise<[string, string]> {
    return Promise.all([
      this.generateToken(user, TokenTypeEnum.ACCESS, domain, tokenId),
      this.generateToken(user, TokenTypeEnum.REFRESH, domain, tokenId),
    ]);
  }
}
```

## Auth Changes

### Module

Move the `ThrottlerModule` import into the `AppModule`:

```ts
// ...
import { ThrottlerModule } from '@nestjs/throttler';
// ...

@Module({
  imports: [
    // ...
    ThrottlerModule.forRootAsync({
      imports: [ConfigModule],
      useClass: ThrottlerConfig,
    }),
    // ...
  ],
  // ...
})
export class AppModule {}
```

### Service

Now that we have a public `generateAuthTokens` remove the private version, and use the public version from the `JwtService`:

```ts
// ...

@Injectable()
export class AuthService {
  // ...

  public async confirmEmail(
    dto: ConfirmEmailDto,
    domain?: string,
  ): Promise<IAuthResult> {
    // ...

    const [accessToken, refreshToken] =
      await this.jwtService.generateAuthTokens(user, domain);
    return { user, accessToken, refreshToken };
  }

  public async signIn(dto: SignInDto, domain?: string): Promise<IAuthResult> {
    // ...

    const [accessToken, refreshToken] =
      await this.jwtService.generateAuthTokens(user, domain);
    return { user, accessToken, refreshToken };
  }

  public async refreshTokenAccess(
    refreshToken: string,
    domain?: string,
  ): Promise<IAuthResult> {
    // ...

    const [accessToken, newRefreshToken] =
      await this.jwtService.generateAuthTokens(user, domain, tokenId);
    return { user, accessToken, refreshToken: newRefreshToken };
  }

  // ...

  public async updatePassword(
    userId: number,
    dto: ChangePasswordDto,
    domain?: string,
  ): Promise<IAuthResult> {
    // ...

    const [accessToken, refreshToken] =
      await this.jwtService.generateAuthTokens(user, domain);
    return { user, accessToken, refreshToken };
  }
  
  // ...
}
```

And the order of the inputs of the `UsersService`'s `updatePassword` method change:

```ts
// ...

@Injectable()
export class AuthService {
  // ...

  public async updatePassword(
    userId: number,
    dto: ChangePasswordDto,
    domain?: string,
  ): Promise<IAuthResult> {
    // ...
    const user = await this.usersService.updatePassword(
      userId,
      password1,
      password,
    );
    // ...
  }

  // ...
}
```

On a last note, update the `signUp` method to pass the `OAuthProviderEnum.LOCAL`:

```ts
// ...
import { OAuthProvidersEnum } from '../users/enums/oauth-providers.enum';
// ...

@Injectable()
export class AuthService {
  // ...

  public async signUp(dto: SignUpDto, domain?: string): Promise<IMessage> {
    // ...
    const user = await this.usersService.create(
      OAuthProvidersEnum.LOCAL,
      email,
      name,
      password1,
    );
    // ...
  }

  // ...
}
```

## OAuth2 Resource

To set up our external providers we will use a new REST resource named `oauth2`:

```bash
$ nest g res oauth2
```

### OAuth Class

Before starting working on the service and controller we need to create an external provider wrapper. It is just a class that encapsulates the OAuth provider logic, so create a `classes` directory with an `oauth.class.ts` file inside:

```ts
export class OAuthClass {}
```

Before implementing it we need to understand what we need from it. The `OAuthClass` will consist on three parts:

1. **Provider**: the provider URLs and Client options, we abstract this with the `AuthorizationCode` from the simple-oauth2 package;
2. **Authorization URL**: the URL with the parameters that we temporarily redirect our user to sign-in/up;
3. **Data URL**: the URL we use to get the user data when we get the external provider's access token.

#### Types

Create a new `interfaces` directory with the following interfaces:

- `IAuthParams`: the parameters necessary for generating the Authorization URL, normally the `redirect_uri` (also known as the callback URL), `scope` and `state`.
    ```ts
    export interface IAuthParams {
      readonly redirect_uri: string;
      readonly scope: string | string[];
      readonly state: string;
    }
    ```
- `ICallbackQuery`: the query parameters that you receive on the (redirect_uri) callback URL.
    ```ts
    export interface ICallbackQuery {
      readonly code: string;
      readonly state: string;
    }
    ```
- `IProvider`: the provider URLs (hosts) and relative paths.
    ```ts
    export interface IProvider {
      readonly tokenHost: string;
      readonly tokenPath: string;
      readonly authorizeHost: string;
      readonly authorizePath: string;
      readonly refreshPath?: string;
      readonly revokePath?: string;
    }
    ```
- _User Responses_: this one is not a single interface but many interfaces, one for each provider.
    ```ts
    export interface IMicrosoftUser {
      readonly businessPhones: string[];
      readonly displayName: string;
      readonly givenName: string;
      readonly jobTitle: string;
      readonly mail: string;
      readonly mobilePhone: string;
      readonly officeLocation: string;
      readonly preferredLanguage: string;
      readonly surname: string;
      readonly userPrincipalName: string;
      readonly id: string;
    }

    export interface IGoogleUser {
      readonly sub: string;
      readonly name: string;
      readonly given_name: string;
      readonly family_name: string;
      readonly picture: string;
      readonly email: string;
      readonly email_verified: boolean;
      readonly locale: string;
      readonly hd: string;
    }

    export interface IFacebookUser {
      readonly id: string;
      readonly name: string;
      readonly email: string;
    }

    interface IGitHubPlan {
      readonly name: string;
      readonly space: number;
      readonly private_repos: number;
      readonly collaborators: number;
    }

    export interface IGitHubUser {
      readonly login: string;
      readonly id: number;
      readonly node_id: string;
      readonly avatar_url: string;
      readonly gravatar_id: string;
      readonly url: string;
      readonly html_url: string;
      readonly followers_url: string;
      readonly following_url: string;
      readonly gists_url: string;
      readonly starred_url: string;
      readonly subscriptions_url: string;
      readonly organizations_url: string;
      readonly repos_url: string;
      readonly events_url: string;
      readonly received_events_url: string;
      readonly type: string;
      readonly site_admin: boolean;
      readonly name: string;
      readonly company: string;
      readonly blog: string;
      readonly location: string;
      readonly email: string;
      readonly hireable: boolean;
      readonly bio: string;
      readonly twitter_username: string;
      readonly public_repos: number;
      readonly public_gists: number;
      readonly followers: number;
      readonly following: number;
      readonly created_at: string;
      readonly updated_at: string;
      readonly private_gists: number;
      readonly total_private_repos: number;
      readonly owned_private_repos: number;
      readonly disk_usage: number;
      readonly collaborators: number;
      readonly two_factor_authentication: boolean;
      readonly plan: IGitHubPlan;
    }
    ```

#### Private Static Params

Back on the class, start adding the providers as static parameters, you can find most of them in the [fastify-oauth2 repository](https://github.com/fastify/fastify-oauth2).

First the providers:

```ts
import { IAuthParams } from '../interfaces/auth-params.interface';
import { IProvider } from '../interfaces/provider.interface';

export class OAuthClass {
  private static readonly [OAuthProvidersEnum.MICROSOFT]: IProvider = {
    authorizeHost: 'https://login.microsoftonline.com',
    authorizePath: '/common/oauth2/v2.0/authorize',
    tokenHost: 'https://login.microsoftonline.com',
    tokenPath: '/common/oauth2/v2.0/token',
  };
  private static readonly [OAuthProvidersEnum.GOOGLE]: IProvider = {
    authorizeHost: 'https://accounts.google.com',
    authorizePath: '/o/oauth2/v2/auth',
    tokenHost: 'https://www.googleapis.com',
    tokenPath: '/oauth2/v4/token',
  };
  private static readonly [OAuthProvidersEnum.FACEBOOK]: IProvider = {
    authorizeHost: 'https://facebook.com',
    authorizePath: '/v9.0/dialog/oauth',
    tokenHost: 'https://graph.facebook.com',
    tokenPath: '/v9.0/oauth/access_token',
  };
  private static readonly [OAuthProvidersEnum.GITHUB]: IProvider = {
    authorizeHost: 'https://github.com',
    authorizePath: '/login/oauth/authorize',
    tokenHost: 'https://github.com',
    tokenPath: '/login/oauth/access_token',
  };
}
```

And then the user data urls:

```ts
// ...

export class OAuthClass {
  // ...
  private static userDataUrls: Record<OAuthProvidersEnum, string> = {
    [OAuthProvidersEnum.GOOGLE]:
      'https://www.googleapis.com/oauth2/v3/userinfo',
    [OAuthProvidersEnum.MICROSOFT]: 'https://graph.microsoft.com/v1.0/me',
    [OAuthProvidersEnum.FACEBOOK]:
      'https://graph.facebook.com/v16.0/me?fields=email,name',
    [OAuthProvidersEnum.GITHUB]: 'https://api.github.com/user',
    [OAuthProvidersEnum.LOCAL]: '',
  };
}
```

We need a static method to generate the Authorization Parameters. It will take the provider and the API url as arguments, and return the params:

```ts
import { randomBytes } from 'crypto';
import { OAuthProvidersEnum } from '../../users/enums/oauth-providers.enum';
// ...

export class OAuthClass {
  // ...

  private static genAuthorization(
    provider: OAuthProvidersEnum,
    url: string,
  ): IAuthParams {
    // generates the callback url given the provider
    const redirect_uri = `${url}/api/auth/ext/${provider}/callback`;
    // generates the state for the parameters
    const state = randomBytes(16).toString('hex');

    switch (provider) {
      case OAuthProvidersEnum.GOOGLE:
        return {
          state,
          redirect_uri,
          scope: [
            'https://www.googleapis.com/auth/userinfo.email',
            'https://www.googleapis.com/auth/userinfo.profile',
          ],
        };
      case OAuthProvidersEnum.MICROSOFT:
        return {
          state,
          redirect_uri,
          scope: ['openid', 'profile', 'email'],
        };
      case OAuthProvidersEnum.FACEBOOK:
        return {
          state,
          redirect_uri,
          scope: ['email', 'public_profile'],
        };
      case OAuthProvidersEnum.GITHUB:
        return {
          state,
          redirect_uri,
          scope: ['user:email', 'read:user'],
        };
    }
  }
}
```

<small>**Note**: I made the scopes static for consistent, but they could have been passed as a parameter, this depends on your preference.</small>

#### Private Non-static Params

This class will have three main non-static parameters:

- _Code_: a class to get the access token provided by the simple-oauth2 package;
- _Authorization_: the authorization params for the redirect URI;
- _User Data URL_: the URL to fetch the user data after getting the external provider's access token.

```ts
// ...
import { AuthorizationCode } from 'simple-oauth2';
import { IAuthParams } from '../interfaces/auth-params.interface';
import { IClient } from '../interfaces/client.interface';
// ...

export class OAuthClass {
   // ...

  private readonly code: AuthorizationCode;
  private readonly authorization: IAuthParams;
  private readonly userDataUrl: string;

  constructor(
    private readonly provider: OAuthProvidersEnum,
    private readonly client: IClient,
    private readonly url: string,
  ) {
    if (provider === OAuthProvidersEnum.LOCAL) {
      throw new Error('Invalid provider');
    }

    this.code = new AuthorizationCode({
      client,
      auth: OAuthClass[provider],
    });
    this.authorization = OAuthClass.genAuthorization(provider, url);
    this.userDataUrl = OAuthClass.userDataUrls[provider];
  }
}
```

#### Get Methods

Get methods are like parameters, but provided by methods with no arguments, this class will have 3:

1. Get State:
    ```ts
    // ...

    export class OAuthClass {
       // ...

      public get state(): string {
        return this.authorization.state;
      }
    }
    ```
2. Get Data URL:
    ```ts
    // ...

    export class OAuthClass {
       // ...

      public get dataUrl(): string {
        return this.userDataUrl;
      }
    }
    ```
3. Get Authorization URL (this is provided by the `AuthorizationCode` from simple-oauth2):
    ```ts
    // ...

    export class OAuthClass {
       // ...

      public get authorizationUrl(): string {
        return this.code.authorizeURL(this.authorization);
      }
    }
    ```

#### Normal Methods

We need a method to get the access token from the external provided. Most of the underlying logic is already provided by the `AuthorizationCode` class from the simple-oauth2 package.

```ts
// ...

export class OAuthClass {
   // ...

  public async getToken(code: string): Promise<string> {
    const result = await this.code.getToken({
      code,
      redirect_uri: this.authorization.redirect_uri,
      scope: this.authorization.scope,
    });
    return result.token.access_token as string;
  }
}
```

### Config

Create an interface with all our external providers, we will call it `oauth2.interface.ts`:

```ts
import { IClient } from '../../oauth2/interfaces/client.interface';

export interface IOAuth2 {
  readonly microsoft: IClient | null;
  readonly google: IClient | null;
  readonly facebook: IClient | null;
  readonly github: IClient | null;
}
```

Add it and the `url` to the `config.interface.ts`:

```ts
import { IOAuth2 } from './oauth2.interface';

export interface IConfig {
  // ...
  readonly url: string;
  // ...
  readonly oauth2: IOAuth2;
}
```

Update the config schema with the new ENV variables:

```ts
import Joi from 'joi';

export const validationSchema = Joi.object({
  // ...
  URL: Joi.string().uri().required(),
  // ...
  MICROSOFT_CLIENT_ID: Joi.string().optional(),
  MICROSOFT_CLIENT_SECRET: Joi.string().optional(),
  GOOGLE_CLIENT_ID: Joi.string().optional(),
  GOOGLE_CLIENT_SECRET: Joi.string().optional(),
  FACEBOOK_CLIENT_ID: Joi.string().optional(),
  FACEBOOK_CLIENT_SECRET: Joi.string().optional(),
  GITHUB_CLIENT_ID: Joi.string().optional(),
  GITHUB_CLIENT_SECRET: Joi.string().optional(),
});
```

And finally add them to the `config` function:

```ts
// ...

export function config(): IConfig {
  // ...

  return {
    // ...
    url: process.env.URL,
    // ...
    oauth2: {
      microsoft:
        isUndefined(process.env.MICROSOFT_CLIENT_ID) ||
        isUndefined(process.env.MICROSOFT_CLIENT_SECRET)
          ? null
          : {
              id: process.env.MICROSOFT_CLIENT_ID,
              secret: process.env.MICROSOFT_CLIENT_SECRET,
            },
      google:
        isUndefined(process.env.GOOGLE_CLIENT_ID) ||
        isUndefined(process.env.GOOGLE_CLIENT_SECRET)
          ? null
          : {
              id: process.env.GOOGLE_CLIENT_ID,
              secret: process.env.GOOGLE_CLIENT_SECRET,
            },
      facebook:
        isUndefined(process.env.FACEBOOK_CLIENT_ID) ||
        isUndefined(process.env.FACEBOOK_CLIENT_SECRET)
          ? null
          : {
              id: process.env.FACEBOOK_CLIENT_ID,
              secret: process.env.FACEBOOK_CLIENT_SECRET,
            },
      github:
        isUndefined(process.env.GITHUB_CLIENT_ID) ||
        isUndefined(process.env.GITHUB_CLIENT_SECRET)
          ? null
          : {
              id: process.env.GITHUB_CLIENT_ID,
              secret: process.env.GITHUB_CLIENT_SECRET,
            },
    },
  };
}
```

### Module

Start by importing the necessary dependencies, namely the `UsersModule`, `JwtModule` and `HttpModule`, and export the `Oauth2Service`:

```ts
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { JwtModule } from '../jwt/jwt.module';
import { UsersModule } from '../users/users.module';
import { Oauth2Controller } from './oauth2.controller';
import { Oauth2Service } from './oauth2.service';

@Module({
  imports: [
    UsersModule,
    JwtModule,
    HttpModule.register({
      timeout: 5000,
      maxRedirects: 5,
    }),
  ],
  controllers: [Oauth2Controller],
  providers: [Oauth2Service],
})
export class Oauth2Module {}
```

### Service

Inject all the necessary services:

```ts
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CommonService } from '../common/common.service';
import { JwtService } from '../jwt/jwt.service';
import { UsersService } from '../users/users.service';

@Injectable()
export class Oauth2Service {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    private readonly httpService: HttpService,
    private readonly commonService: CommonService,
  ) {}
}
```

And the private parameters for each external provider:

```ts
// ...
import { OAuthProvidersEnum } from '../users/enums/oauth-providers.enum';
import { OAuthClass } from './classes/oauth.class';

@Injectable()
export class Oauth2Service {
  private readonly [OAuthProvidersEnum.MICROSOFT]: OAuthClass | null;
  private readonly [OAuthProvidersEnum.GOOGLE]: OAuthClass | null;
  private readonly [OAuthProvidersEnum.FACEBOOK]: OAuthClass | null;
  private readonly [OAuthProvidersEnum.GITHUB]: OAuthClass | null;

  // ...
}
```

We will use a private static method to create these parameters:

```ts
// ...
import { isNull } from '../common/utils/validation.util';

@Injectable()
export class Oauth2Service {
  // ...

  constructor(
    // ...
  ) {
    const url = configService.get<string>('url');
    this[OAuthProvidersEnum.MICROSOFT] = Oauth2Service.setOAuthClass(
      OAuthProvidersEnum.MICROSOFT,
      configService,
      url,
    );
    this[OAuthProvidersEnum.GOOGLE] = Oauth2Service.setOAuthClass(
      OAuthProvidersEnum.GOOGLE,
      configService,
      url,
    );
    this[OAuthProvidersEnum.FACEBOOK] = Oauth2Service.setOAuthClass(
      OAuthProvidersEnum.FACEBOOK,
      configService,
      url,
    );
    this[OAuthProvidersEnum.GITHUB] = Oauth2Service.setOAuthClass(
      OAuthProvidersEnum.GITHUB,
      configService,
      url,
    );
  }

  private static setOAuthClass(
    provider: OAuthProvidersEnum,
    configService: ConfigService,
    url: string,
  ): OAuthClass | null {
    const client = configService.get<IClient | null>(
      `oauth2.${provider.toLowerCase()}`,
    );

    if (isNull(client)) {
      return null;
    }

    return new OAuthClass(provider, client, url);
  }
}
```

And, since the provider can be null, we need to check everytime we call it. Thus, create a wrapper method to get the provider:

```ts
// ...
import {
  // ...
  NotFoundException,
} from '@nestjs/common';
// ...

@Injectable()
export class Oauth2Service {
  // ...

    private getOAuth(provider: OAuthProvidersEnum): OAuthClass {
    const oauth = this[provider];

    if (isNull(oauth)) {
      throw new NotFoundException('Page not found');
    }

    return oauth;
  }
}
```

#### Core Logic

The core logic is comprised of 4 methods:

1. **Get Authorization URL**: to redirect the user to the provider.
    ```ts
    // ...

    @Injectable()
    export class Oauth2Service {
      // ...
  
      public getAuthorizationUrl(provider: OAuthProvidersEnum): string {
        return this.getOAuth(provider).authorizationUrl;
      }

      // ...
    }
    ```
2. **Get Access Token**: get the provider's access token given the callback code and state.
    ```ts
    // ...

    @Injectable()
    export class Oauth2Service {
      // ...

      private async getAccessToken(
        provider: OAuthProvidersEnum,
        code: string,
        state: string,
      ): Promise<string> {
        const oauth = this.getOAuth(provider);

        if (state !== oauth.state) {
          throw new UnauthorizedException('Corrupted state');
        }

        return await this.commonService.throwInternalError(oauth.getToken(code));
      }
    }
    ```
3. **Get User Data**: this is dependent on the previous method, and uses its access token to get the user data from the provider.
    ```ts
    // ...
    import { AxiosError } from 'axios';
    import { catchError, firstValueFrom } from 'rxjs';

    @Injectable()
    export class Oauth2Service {
      // ...

      public async getUserData<T extends Record<string, any>>(
        provider: OAuthProvidersEnum,
        cbQuery: ICallbackQuery,
      ): Promise<T> {
        const { code, state } = cbQuery;
        const accessToken = await this.getAccessToken(provider, code, state);
        const userData = await firstValueFrom(
          this.httpService
            .get<T>(this.getOAuth(provider).dataUrl, {
              headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${accessToken}`,
              },
            })
            .pipe(
              catchError((error: AxiosError) => {
                throw new UnauthorizedException(error.response.data);
              }),
            ),
        );
        return userData.data;
      }

      // ...
    }
    ```
4. **Login**: finds or creates a new user and generates the Auth tokens.
    ```ts
    // ...

    @Injectable()
    export class Oauth2Service {
      // ...

      public async login(
        provider: OAuthProvidersEnum,
        email: string,
        name: string,
      ): Promise<[string, string]> {
        const user = await this.usersService.findOrCreate(provider, email, name);
        return this.jwtService.generateAuthTokens(user);
      }

      // ...
    }
    ```

### Controller

Before start writing the controllers logic we need to understand the concept of a feature flag, if you ask ChatGPT, it will answer as follows:

> A Feature Flag is a software development technique that allows developers to turn specific functionality or features of their application on or off, without deploying new code. It is also known as a feature toggle or feature switch.

A great article on how to implement them in NestJS can be found on the [wanago's blog](https://wanago.io/2022/08/22/api-nestjs-feature-flags-feature-toggles/).

#### OAuth Flag Guard

To use feature flags we will use a `mixin` guard that checks if provider is null or not:

```ts
import {
  CanActivate,
  ExecutionContext,
  mixin,
  NotFoundException,
  Type,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { FastifyRequest } from 'fastify';
import { isNull } from '../../common/utils/validation.util';
import { OAuthProvidersEnum } from '../../users/enums/oauth-providers.enum';
import { IClient } from '../interfaces/client.interface';

export const OAuthFlagGuard = (
  provider: OAuthProvidersEnum,
): Type<CanActivate> => {
  class OAuthFlagGuardClass implements CanActivate {
    constructor(private readonly configService: ConfigService) {}

    public canActivate(context: ExecutionContext): boolean {
      const client = this.configService.get<IClient | null>(
        `oauth2.${provider}`,
      );

      if (isNull(client)) {
        const request = context.switchToHttp().getRequest<FastifyRequest>();
        throw new NotFoundException(`Cannot ${request.method} ${request.url}`);
      }

      return true;
    }
  }

  return mixin(OAuthFlagGuardClass);
};
```

#### DTOs

There is only necessity for one DTO, the `CallbackQueryDto`:

```ts
import { IsString } from 'class-validator';
import { ICallbackQuery } from '../interfaces/callback-query.interface';

export abstract class CallbackQueryDto implements ICallbackQuery {
  @IsString()
  public code: string;

  @IsString()
  public state: string;
}
```

#### Controller Set-up

Add the same private parameters as the ones in the `AuthController`, plus an `url` parameter for the front-end url. Futhermore, inject the `Oauth2Service` and the `ConfigService`.

```ts
import {
  Controller,
  // ...
  UseGuards,
} from '@nestjs/common';
import { FastifyThrottlerGuard } from '../auth/guards/fastify-throttler.guard';
import { ConfigService } from '@nestjs/config';
import { Oauth2Service } from './oauth2.service';

@ApiTags('Oauth2')
@Controller('api/auth/ext')
@UseGuards(FastifyThrottlerGuard)
export class Oauth2Controller {
  private readonly url: string;
  private readonly cookiePath = '/api/auth';
  private readonly cookieName: string;
  private readonly refreshTime: number;
  private readonly testing: boolean;

  constructor(
    private readonly oauth2Service: Oauth2Service,
    private readonly configService: ConfigService,
  ) {
    this.url = `https://${this.configService.get<string>('domain')}`;
    this.cookieName = this.configService.get<string>('REFRESH_COOKIE');
    this.refreshTime = this.configService.get<number>('jwt.refresh.time');
    this.testing = this.configService.get<boolean>('testing');
  }
}
```

#### Controller main logic

The logic for all the routes will be the same, we will:

- Temporary redirect the user to the external provider URL:
    ```ts
    import {
      // ...
      HttpStatus,
    } from '@nestjs/common';
    // ...
    import { FastifyReply } from 'fastify';

    @ApiTags('Oauth2')
    @Controller('api/auth/ext')
    @UseGuards(FastifyThrottlerGuard)
    export class Oauth2Controller {
      // ...

      private startRedirect(
        res: FastifyReply,
        provider: OAuthProvidersEnum,
      ): FastifyReply {
        return res
          .status(HttpStatus.TEMPORARY_REDIRECT)
          .redirect(this.oauth2Service.getAuthorizationUrl(provider));
      }
    }
    ```
- Recieve the response on our callback URL, and permanent redirect to the front-end with an access and refresh token pair:
    ```ts
    // ...

    @ApiTags('Oauth2')
    @Controller('api/auth/ext')
    @UseGuards(FastifyThrottlerGuard)
    export class Oauth2Controller {
      // ...

      private async loginAndRedirect(
        res: FastifyReply,
        provider: OAuthProvidersEnum,
        email: string,
        name: string,
      ): Promise<FastifyReply> {
        const [accessToken, refreshToken] = await this.oauth2Service.login(
          provider,
          email,
          name,
        );
        return res
          .cookie(this.cookieName, refreshToken, {
            secure: !this.testing,
            httpOnly: true,
            signed: true,
            path: this.cookiePath,
            expires: new Date(Date.now() + this.refreshTime * 1000),
      })
          .status(HttpStatus.PERMANENT_REDIRECT)
          .redirect(`${this.url}/?access_token=${accessToken}`);
      }
    }
    ```

#### Controller routes

Create a redirect route for each provider:

```ts
import {
  // ...
  Query,
  Res,
  // ...
} from '@nestjs/common';
// ...
import { ApiNotFoundResponse, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Public } from '../auth/decorators/public.decorator';
import { CallbackQueryDto } from './dtos/callback-query.dto';
import { OAuthFlagGuard } from './guards/oauth-flag.guard';
// ...

@ApiTags('Oauth2')
@Controller('api/auth/ext')
@UseGuards(FastifyThrottlerGuard)
export class Oauth2Controller {
  // ...

  @Public()
  @UseGuards(OAuthFlagGuard(OAuthProvidersEnum.MICROSOFT))
  @Get('microsoft')
  @ApiResponse({
    description: 'Redirects to Microsoft OAuth2 login page',
    status: HttpStatus.TEMPORARY_REDIRECT,
  })
  @ApiNotFoundResponse({
    description: 'OAuth2 is not enabled for Microsoft',
  })
  public microsoft(@Res() res: FastifyReply): FastifyReply {
    return this.startRedirect(res, OAuthProvidersEnum.MICROSOFT);
  }

  @Public()
  @UseGuards(OAuthFlagGuard(OAuthProvidersEnum.GOOGLE))
  @Get('google')
  @ApiResponse({
    description: 'Redirects to Google OAuth2 login page',
    status: HttpStatus.TEMPORARY_REDIRECT,
  })
  @ApiNotFoundResponse({
    description: 'OAuth2 is not enabled for Google',
  })
  public google(@Res() res: FastifyReply): FastifyReply {
    return this.startRedirect(res, OAuthProvidersEnum.GOOGLE);
  }

  @Public()
  @UseGuards(OAuthFlagGuard(OAuthProvidersEnum.FACEBOOK))
  @Get('facebook')
  @ApiResponse({
    description: 'Redirects to Facebook OAuth2 login page',
    status: HttpStatus.TEMPORARY_REDIRECT,
  })
  @ApiNotFoundResponse({
    description: 'OAuth2 is not enabled for Facebook',
  })
  public facebook(@Res() res: FastifyReply): FastifyReply {
    return this.startRedirect(res, OAuthProvidersEnum.FACEBOOK);
  }

  @Public()
  @UseGuards(OAuthFlagGuard(OAuthProvidersEnum.GITHUB))
  @Get('github')
  @ApiResponse({
    description: 'Redirects to GitHub OAuth2 login page',
    status: HttpStatus.TEMPORARY_REDIRECT,
  })
  @ApiNotFoundResponse({
    description: 'OAuth2 is not enabled for GitHub',
  })
  public github(@Res() res: FastifyReply): FastifyReply {
    return this.startRedirect(res, OAuthProvidersEnum.GITHUB);
  }

  // ...
}
```

And a callback route for each provider:

```ts
// ...
import {
  IFacebookUser,
  IGitHubUser,
  IGoogleUser,
  IMicrosoftUser,
} from './interfaces/user-response.interface';

@ApiTags('Oauth2')
@Controller('api/auth/ext')
@UseGuards(FastifyThrottlerGuard)
export class Oauth2Controller {
  // ...

  @Public()
  @UseGuards(OAuthFlagGuard(OAuthProvidersEnum.MICROSOFT))
  @Get('microsoft/callback')
  @ApiResponse({
    description: 'Redirects to the frontend with the JWT token',
    status: HttpStatus.PERMANENT_REDIRECT,
  })
  @ApiNotFoundResponse({
    description: 'OAuth2 is not enabled for Microsoft',
  })
  public async microsoftCallback(
    @Query() cbQuery: CallbackQueryDto,
    @Res() res: FastifyReply,
  ): Promise<FastifyReply> {
    const provider = OAuthProvidersEnum.MICROSOFT;
    const { displayName, mail } =
      await this.oauth2Service.getUserData<IMicrosoftUser>(provider, cbQuery);
    return this.loginAndRedirect(res, provider, mail, displayName);
  }

  @Public()
  @UseGuards(OAuthFlagGuard(OAuthProvidersEnum.GOOGLE))
  @Get('google/callback')
  @ApiResponse({
    description: 'Redirects to the frontend with the JWT token',
    status: HttpStatus.PERMANENT_REDIRECT,
  })
  @ApiNotFoundResponse({
    description: 'OAuth2 is not enabled for Google',
  })
  public async googleCallback(
    @Query() cbQuery: CallbackQueryDto,
    @Res() res: FastifyReply,
  ): Promise<FastifyReply> {
    const provider = OAuthProvidersEnum.GOOGLE;
    const { name, email } = await this.oauth2Service.getUserData<IGoogleUser>(
      provider,
      cbQuery,
    );
    return this.loginAndRedirect(res, provider, email, name);
  }

  @Public()
  @UseGuards(OAuthFlagGuard(OAuthProvidersEnum.FACEBOOK))
  @Get('facebook/callback')
  @ApiResponse({
    description: 'Redirects to the frontend with the JWT token',
    status: HttpStatus.PERMANENT_REDIRECT,
  })
  @ApiNotFoundResponse({
    description: 'OAuth2 is not enabled for Facebook',
  })
  public async facebookCallback(
    @Query() cbQuery: CallbackQueryDto,
    @Res() res: FastifyReply,
  ): Promise<FastifyReply> {
    const provider = OAuthProvidersEnum.FACEBOOK;
    const { name, email } = await this.oauth2Service.getUserData<IFacebookUser>(
      provider,
      cbQuery,
    );
    return this.loginAndRedirect(res, provider, email, name);
  }

  @Public()
  @UseGuards(OAuthFlagGuard(OAuthProvidersEnum.GITHUB))
  @Get('github/callback')
  @ApiResponse({
    description: 'Redirects to the frontend with the JWT token',
    status: HttpStatus.PERMANENT_REDIRECT,
  })
  @ApiNotFoundResponse({
    description: 'OAuth2 is not enabled for GitHub',
  })
  public async githubCallback(
    @Query() cbQuery: CallbackQueryDto,
    @Res() res: FastifyReply,
  ): Promise<FastifyReply> {
    const provider = OAuthProvidersEnum.GITHUB;
    const { name, email } = await this.oauth2Service.getUserData<IGitHubUser>(
      provider,
      cbQuery,
    );
    return this.loginAndRedirect(res, provider, email, name);
  }

  // ...
}
```

## Conclusion

This is the end of my OAuth 2.0 series with NestJS, with this series you have learnt how to create a production grade OAuth 2.0 microservice using NodeJS, with both local and external OAuth.

The full source code can be found on this [repo](https://github.com/tugascript/nestjs-oauth/tree/external-providers).

## About the Author

Hey there! I am Afonso Barracha, your go-to econometrician who found his way into the world of back-end development with a soft spot for GraphQL. If you enjoyed reading this article, why not show some love by [buying me a coffee](https://www.buymeacoffee.com/barracha)?

Lately, I have been diving deep into more advanced subjects. As a result, I have switched from sharing my thoughts every week to posting once or twice a month. This way, I can make sure to bring you the highest quality content possible.

Do not miss out on any of my latest articles – follow me here on [dev](https://dev.to/tugascript), on [twitter](https://twitter.com/tugascript) and [LinkedIn](https://www.linkedin.com/in/afonso-barracha/) to stay updated. I would be thrilled to welcome you to our ever-growing community! See you around!