0.3.10 • Published 6 months ago

@ce-spgi/common-api v0.3.10

Weekly downloads
-
License
UNLICENSED
Repository
-
Last release
6 months ago

Common Api

Based on the knowledge of the team, we decide to use typescript (a strongly typed programming language that builds on JavaScript) to develop all the microservices in which we have the option to choose the architecture.

After some iterations, some developers find NestJS, a typescript framework that help us to develop fast and without duplicated code. If you need more information about the framework I encourage you to read the official documentation that you can find on this link. We recommend having a look at the documentation if you have never work with this framework before.

The main points that are going be covered are:

Api Init

We discovered that the code that is used to start the API service is the same across all the projects that we did. In order to start the microservice, you just need to add the belows code inside your microservice entrypoint. The meaning of each variable would be described below.

During the init process we add the required code to avoid the app to crash at runtime and to validate the input parameters of each request. Also, we add a /healthCheck endpoint that could be used to reduce the downtime when swapping containers or detecting if the microservice is not performing as expected.

import { ApiDefinition } from '@ce-spgi/common-api';

ApiDefinition(AppModule, {
  name: '<NameOfTheMicroservice>',
  title: '<FullNameOfTheMicroservice>',
  description: '<DescriptionOfTheMicroservice>',
  version: '<VersionOfTheMicroservice>',
  authentication: true,
  prefix: '<MicroServicePrefix>',
  port: 3000,
  callback: null
});

AppModule

The nestJS application are divided in modules that have a root one that is call AppModule. This root module class should be placed as first parameter of the ApiDefinition method. If you want to learn more about this you should read the official documentation that you can find on this link.

Swagger Documentation

Some keys of config displayed above belongs to the swagger documentation that is autogenerated based on the code you need to write in order to the application to work. If you want to learn more about this you should read the official documentation that you can find on this link:

  • name: This option is used to index documentation into its own path (/docs/${name})
  • title: This option would be used as title on the documentation site
  • description: This option would be used as description on the documentation site
  • version: This option would be used as api version on the documentation site
  • authentication: This option would append all the required documentation for using a standard auth system.

Microservice

Some keys of config display above belongs to the NestJS app init process. This config would tune the application to avoid duplicated code in the project and to add more complex functionality the common api package.

  • prefix: This key allows the user to add a prefix to all the endpoints inside the microservice. If you put api on prefix, and you define a users endpoint, the final uri is going to be /api/users.
  • port: This is the port in which the server would listen on. The default port is 3000.
  • callback: It's a function that receive the nestJS App instance as parameter. This function is going to be call after setting up all the framework config and before the documentation config.

AppId

IBM Cloud App ID allows you to easily add authentication to web and mobile apps. As before we notice that the code was the same between MVPs So we create all the decorators and api endpoints to be able to integrate the appId in you NestJS backends. To be more precise, we create a wrapper of the official package ibmcloud-appid to work with the nestJS flows and code patterns. Refer to the package documentation to check some terms used in this documentation.

Endpoint config

In order to use the AppId, you need to add the following code inside the root AppModule. It's recommended to check the app-id types in order to know which values are required and which values not.

All the values in the AppId object came from the AppId config that can be found in the IBM Cloud console. It's recommended to place this AppId object inside a env.ts in order to be reused across the project. All the functions in the callbacks object are optional and can be omitted. These functions are called with the value that AppId returns when performing the action. Also, you need to define <baseEndpoint>. This is going to be used to expose all the app-id required endpoints.

The AppId endpoints includes all the swagger documentation that is going to be appended to the project one.

import { Module } from '@nestjs/common';
import { GenerateAppIdModule } from '@ce-spgi/common-api';

// Check AppIdConfig type for more info
const AppId = {
  tenantId: '<TenantIdFromIBMCloud>',
  clientId: '<ClientIdFromIBMCloud>',
  secret: '<SecretFromIBMCloud>',
  oauthServerUrl: `${'<ServiceURLFromIBMCloud>'}/oauth/v4/${'<TenantIdFromIBMCloud>'}`,
  redirectUri: '<YourFrontedRedirectURL>',
  version: 4,
  appidServiceEndpoint: '<ServiceURLFromIBMCloud>'
};

// Check AppIdCallbacks for an updated list of callbacks
const callbacks = {
  loginUrl: (params: Record<any, any>) => console.log(params),
  create: (params: Record<any, any>) => console.log(params),
  refresh: (params: Record<any, any>) => console.log(params),
};

@Module({
  imports: [GenerateAppIdModule('<baseEndpoint>', AppId, callbacks)]
})
export class AppModule {
}

Strategy

Inside each module that you would like to secure using AppID should include a call to the function GenerateAppIdStrategy(AppId) with the appId configuration as parameter inside the providers list and the PassportModule as import. Your module Should look like the one below.

import { Module } from '@nestjs/common';
import { SourcesController } from './sources.controller';
import { SourcesService } from './sources.service';
import { GenerateAppIdStrategy } from '@ce-spgi/common-api';
import { PassportModule } from '@nestjs/passport';

// Check AppIdConfig type for more info
const AppId = {
  tenantId: '<TenantIdFromIBMCloud>',
  clientId: '<ClientIdFromIBMCloud>',
  secret: '<SecretFromIBMCloud>',
  oauthServerUrl: `${'<ServiceURLFromIBMCloud>'}/oauth/v4/${'<TenantIdFromIBMCloud>'}`,
  redirectUri: '<YourRedirectURL>',
  version: 4,
  appidServiceEndpoint: '<ServiceURLFromIBMCloud>'
};

@Module({
  imports: [PassportModule],
  controllers: [SourcesController],
  providers: [SourcesService, GenerateAppIdStrategy(AppId)]
})
export class SourcesModule {
}

Guards

In order to have more granularity, you also need to define which controllers or which specific endpoints need the auth process to be applied.

Your controller should look like below if you want it to be secured by appId.

import { Controller, ForbiddenException, Get, Header, Param, ParseIntPipe, UseGuards } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AppIdGuard, AppIdSecurity } from '@ce-spgi/common-api';

@Controller('sources')
@UseGuards(AppIdGuard())
@AppIdSecurity()
export class SourcesController {
  @Get()
  async list(): Promise<any[]> {
    return [{}];
  }
}

If you just want some endpoints, your controller should look like below.

import { Controller, ForbiddenException, Get, Header, Param, ParseIntPipe, UseGuards } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AppIdGuard, AppIdSecurity } from '@ce-spgi/common-api';

@Controller('sources')
@AppIdSecurity()
export class SourcesController {
  @Get()
  @UseGuards(AppIdGuard())
  async list(): Promise<any[]> {
    return [{}];
  }
}

Decorators

NestJS use decorators to access the request information. Keeping in mind this aspect, we create a group of decorators to access to all information that is in the appId token or can be inferred from it.

  • @AppIdPayload(): Return all the information that is inside the Auth token.
  • @AppIdToken(): Return the auth token as string
  • @AppIdProfileManager(AppIdConfig): Return an instance of appId profile manger (check ibmcloud-appid for more information).
  • @AppIdUser(AppIdConfig): Return a promise with all the user information that is inside AppId
  • @AppIdProperties(AppIdConfig): Return a promise with all the properties that user has is inside AppId
  • @AppIdSecurity(): It's a decorator that adds all the required documentation to mark an endpoint or a group of endpoint as an auth required

Auth Flow

The flow described below assumes that all the request are done from a fronted. For more information about the request content, check you microservice documentation.

  1. GET <apiPrefix>/<baseEndpoint> return the redirect URL that the frontend must use to redirect the user to AppId login.
  2. On AppId callback you should call POST <apiPrefix>/<baseEndpoint> placing the queryParams as body.
  3. From now on, set the token that came in the post request in all the request headers: Authorization: Bearer <token>
  4. If you want to refresh the token POST <apiPrefix>/<baseEndpoint>/refresh with the refresh_token

Example

  • Env
export const AppId = {
  tenantId: process.env.APPID_TENANT_ID,
  clientId: process.env.APPID_CLIENT_ID,
  secret: process.env.APPID_SECRET,
  oauthServerUrl: `${process.env.APPID_SERVICE_URL}/oauth/v4/${process.env.APPID_TENANT_ID}`,
  redirectUri: process.env.APPID_REDIRECT_URL,
  version: 4,
  appidServiceEndpoint: process.env.APPID_SERVICE_URL
};
  • users/Controller
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { ApiBadRequestResponse, ApiBody, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AppIdGuard, AppIdUser, AppIdSecurity } from '@ce-spgi/common-api';
import { AppId } from '../env';

@Controller('users')
@UseGuards(AppIdGuard())
@AppIdSecurity()
export class UserController {
  @Get('me')
  async show(@AppIdUser(AppId) user: any): Promise<any> {
    return await user;
  }
}
  • users/Module
import { Module } from '@nestjs/common';
import { UserController } from './sources.controller';
import { GenerateAppIdStrategy } from '@ce-spgi/common-api';
import { PassportModule } from '@nestjs/passport';
import { AppId } from '../env';

@Module({
  imports: [PassportModule],
  controllers: [SourcesController],
  providers: [GenerateAppIdStrategy(AppId)]
})
export class UserModule {
}

Verify

IBM Security Verify allows you to add authentication to web and mobile apps as well as to define access policies to add more control and features on the logins. As before we notice that the code was the same between MVPs So we create all the decorators and api endpoints to be able to integrate the appId in you NestJS backends. To be more precise, we integrated the REST API of IBM Security Verify to work with the nestJS flows and code patterns. Refer to the API documentation to check some terms used in this documentation.

Endpoint config

In order to use the Verify, you need to add the following code inside the root AppModule.

All the values in the Verify object came from the Verify config that can be found in the IBM Security Verify webpage when adding a new application under the Sign-on tab. It's recommended to place this Verify object inside a env.ts in order to be reused across the project. A Also, you need to define <baseEndpoint>. This is going to be used to expose all the app-id required endpoints.

The Verify endpoints includes all the swagger documentation that is going to be appended to the project one.

import { Module } from '@nestjs/common'
import uuid4 from 'uuid4'
import { GenerateVerifyModule } from '@ce-spgi/common-api/dist/verify'

const Verify = {
  clientId: '<ClientIDFromVerifyWeb>',
  secret: '<SecretFromVerifyWeb>',
  oauthServerUrl: '<OauthServerUrlFromVerifyWeb>,
  redirectUri: '<YourFrontEndRedirectURL>',
  options: {
    scope: 'openid profile email groups',
    authorizationGrant: 'id_token token refresh_token',
    codeChallenge: () => uuid4()
  }
}


@Module({
  imports: [GenerateVerifyModule('<baseEndpoint>', Verify, {})]
})
export class AppModule {}

Strategy

Inside each module that you would like to secure using Verify should include a call to the function GenerateVerifyStrategy(Verify) with the Verify configuration as parameter inside the providers list. Your module Should look like the one below.

import { Module } from '@nestjs/common';
import { SourcesController } from './sources.controller';
import { SourcesService } from './sources.service';
import { GenerateVerifyStrategy } from '@ce-spgi/common-api';
import uuid4 from 'uuid4'

const Verify = {
  clientId: '<ClientIDFromVerifyWeb>',
  secret: '<SecretFromVerifyWeb>',
  oauthServerUrl: '<OauthServerUrlFromVerifyWeb>,
  redirectUri: '<YourFrontEndRedirectURL>',
  options: {
    scope: 'openid profile email groups',
    authorizationGrant: 'id_token token refresh_token',
    codeChallenge: () => uuid4()
  }
}

@Module({
  imports: [],
  controllers: [SourcesController],
  providers: [SourcesService, GenerateVerifyStrategy(Verify)]
})
export class SourcesModule {
}

Guards

In order to have more granularity, you also need to define which controllers or which specific endpoints need the auth process to be applied.

Your controller should look like below if you want it to be secured by Verify.

import { Controller, ForbiddenException, Get, Header, Param, ParseIntPipe, UseGuards } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { VerifyGuard, VerifySecurity } from '@ce-spgi/common-api/dist/verify'

@Controller('sources')
@UseGuards(VerifyGuard())
@VerifySecurity()
export class SourcesController {
  @Get()
  async list(): Promise<any[]> {
    return [{}];
  }
}

If you just want some endpoints, your controller should look like below.

import { Controller, ForbiddenException, Get, Header, Param, ParseIntPipe, UseGuards } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AppIdGuard, AppIdProperties } from '@ce-spgi/common-api';

@Controller('sources')
@VerifySecurity()
export class SourcesController {
  @Get()
  @UseGuards(Guard())AppId
  async list(): Promise<any[]> {
    return [{}];
  }
}

Auth Flow

The flow described below assumes that all the request are done from a fronted. For more information about the request content, check you microservice documentation.

  1. GET <apiPrefix>/<baseEndpoint> return the redirect URL that the frontend must use to redirect the user to Verify login page and the codeChallenge that has to be sent later with the code.
  2. On Verify callback you should call POST <apiPrefix>/<baseEndpoint> with the codeChallenge and the code on the query params of the callback as body of the POST as follows:
     {
       codeVerifier: '<codeChallengeFromTheStep1>';
       code: '<codeFromTheCallbackQueryParams>'
     }
  3. From now on, set the token that came in the post request in all the request headers: Authorization: Bearer <token>
  4. If you want to refresh the token POST <apiPrefix>/<baseEndpoint>/refresh with the refresh_token

Example

  • Env
import uuid4 from 'uuid4'

const Verify = {
  clientId: process.env.VERIFY_LOGIN_CLIENT_ID,
  secret: process.env.VERIFY_LOGIN_SECRET,
  oauthServerUrl: process.env.VERIFY_LOGIN_SERVICE_URL,
  redirectUri: process.env.VERIFY_LOGIN_REDIRECT_URL,
  options: {
    scope: 'openid profile email groups',
    authorizationGrant: 'id_token token refresh_token',
    codeChallenge: () => uuid4()
  }
}
  • users/Controller
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { ApiBadRequestResponse, ApiBody, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { VerifyGuard, VerifySecurity } from '@ce-spgi/common-api'

@Controller('users')
@UseGuards(VerifyGuard())
@VerifySecurity()
export class UserController {
  @Get('me')
  async show(user: any): Promise<any> {
    return await user;
  }
}
  • users/Module
import { Module } from '@nestjs/common';
import { UserController } from './sources.controller';
import { GenerateVerifyStrategy } from '@ce-spgi/common-api'

import { Verify } from '../env'

@Module({
  imports: [],
  controllers: [SourcesController],
  providers: [SourcesService, GenerateVerifyStrategy(Verify)]
})
export class UserModule {
}

Validation

The main key to avoid errors during the request process is the input data validation. The way you define the validators change depending on where the data come from. If you want to learn more about this you should read the official documentation that you can find on this link.

Body

For the body validation we use the class-validator. You can find an example of body validation below. If you want to learn more about this you should read the official documentation that you can find on this link.

import { IsDefined, IsNotEmpty, IsObject, IsOptional } from 'class-validator';

export class DashboardBodyDto {
  @IsDefined()
  @IsNotEmpty()
  name: string;

  @IsOptional()
  description: string;

  @IsDefined()
  @IsNotEmpty()
  @IsObject()
  spec: Record<string, unknown>;
}

QueryParams + PathParams

For the body validation we use the build-in pipes + some custom pipes that are inside the library. You can find an example of validation below. If you want to learn more about this you should read the official documentation that you can find on this link.

import { Controller, ForbiddenException, Get, Header, Param, ParseIntPipe, UseGuards } from '@nestjs/common';
import { AppIdGuard } from '@ce-spgi/common-api';
import { AppIdProperties, AppIdToken, AppIdSecurity } from '@ce-spgi/common-api';
import { AppId } from '../env';

@Controller('sources')
@UseGuards(AppIdGuard())
@AppIdSecurity()
export class SourcesController {
  constructor(private readonly appService: SourcesService) {
  }

  @Get(':sectionId')
  async show(@Param('sectionId', new ParseIntPipe()) sectionId: number): Promise<string> {
    return this.appService.show(sectionId);
  }
}

Customs

For some request we need to extend the build-in pipes to have more accurate validation.

  • MaxValuePipe(maxVal: number): Validate if a number is lte the maxVal value.
  • MinValuePipe: Validate if a number is gte the maxVal value.
  • ParseBoolPipe: Validate and transform a string into a boolean
  • ParseDatePipe: Validate and transform a string into a date
  • ParseIntegerPipe: Validate and transform a string into an integer

Flow Control

During the api init we add the required code to use exceptions as flow control without affecting the rest of the requests. NestJS have Build-in Exceptions that also change the response statusCode and have a dynamic JSON message that could help at the time of returning custom error values.

0.3.10

6 months ago

0.3.9

12 months ago

0.3.8

12 months ago

0.3.7

1 year ago

0.3.6

1 year ago

0.3.5

2 years ago

0.3.0

2 years ago

0.2.7

2 years ago

0.2.6

2 years ago

0.3.2

2 years ago

0.3.1

2 years ago

0.3.4

2 years ago

0.2.5

2 years ago

0.3.3

2 years ago

0.2.4

2 years ago

0.2.3

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago

0.1.4

2 years ago

0.2.2

2 years ago

0.1.3

2 years ago

0.1.2

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago