lukeautry / tsoa

Build OpenAPI-compliant REST APIs using TypeScript and Node
MIT License
3.57k stars 504 forks source link

[Question] Accessing a IOC singleton from within an authentication module function #1002

Closed kwhitaker closed 3 years ago

kwhitaker commented 3 years ago

Our API has to talk to another REST API. In order to keep things streamlined, I've written an API Client singleton that each of the TSOA classes have access to. I'm using Tsyringe for IOC to accomplish this, by decorating the API Client with @singleton and decorating the controller classes with @injectable.

I have a authentication module in place for making sure each request coming into the TSOA API has a JWT, and in order to provide that JWT to each of my controller methods I'm using the @Request() type from TSOA to grab the header, and then forward them into the API Client. This is obviously not very DRY. What I'd like to do is have the authentication module send the JWT to the API Client when its validated. I was wondering if this was possible.

I recognize that this might be a better question for the TSyringe folks, but all of their documentation revolves around class decorators, and I was wondering if you all had a way I could just wrap or otherwise provide the API client to the authentication module function. Thanks.

Sorting

Context (Environment)

Version of the library: 3.6.1 Version of NodeJS: 14.15.4

Authentication Module

import { Request } from 'express';
import createError from 'http-errors';

export function expressAuthentication(
  request: Request,
  securityName: string,
  scopes?: string[]
): Promise<string> {
  console.log(scopes);
  console.log(request.headers);

  if (securityName === 'jwt') {
    const token = request.headers['authorization'];

    return new Promise((resolve, reject) => {
      if (!token) {
        reject(createError(401, 'Unauthorized'));
      } else {
        // this is where I'd like to set the JWT on the API Client
        resolve(token);
      }
    });
  }

  return Promise.reject();
}

API Client

import createError from 'http-errors';
import jwtDecode from 'jwt-decode';
import { identity } from 'lodash/fp';
import { stringify } from 'query-string';
import { singleton } from 'tsyringe';
import { Auth0JWT } from './types';

@singleton()
export class ApiClient {
  public client: AxiosInstance;
  private token?: string;

  constructor() {
    this.client = axios.create({
      baseURL: process.env.API_URL,
      headers: {
        common: {
          'Access-Control-Allow-Origin': '*',
          'Content-Type': 'application/json;charset=UTF-8'
        }
      },
      paramsSerializer: (params) => stringify(params, { arrayFormat: 'comma' })
    });

    this.client.interceptors.request.use(this.onRequest, this.onRequestError);
    this.client.interceptors.response.use(identity, this.onResponseError);
  }

  private onRequest = async (config: AxiosRequestConfig) => {
    const token = this.accessToken;

    if (!token) {
      throw createError(401, 'Unauthorized');
    }

    config.headers = {
      Authorization: `Bearer ${token}`
    };

    return config;
  };

  private onRequestError = (error: AxiosError) => {
    return Promise.reject(error);
  };

  private onResponseError = (error: AxiosError) => {
    return Promise.reject(error);
  };

  get accessToken(): string | undefined {
    if (!this.token) {
      throw createError(401, 'Unauthorized');
    }

    try {
      const { exp } = jwtDecode<Auth0JWT>(this.token);
      const expDate = new Date(exp * 1000);

      if (expDate < new Date()) {
        throw createError(401, 'Unauthoried');
      }

      return this.token;
    } catch (err) {
      console.error(err);
      return undefined;
    }
  }

  set accessToken(next: string | undefined) {
    if (!next) {
      this.token = next;
      return;
    }

    const actualNext = next.split('Bearer ')[1];

    if (actualNext === this.token) {
      return;
    }

    this.token = actualNext;
  }
}
tsimbalar commented 3 years ago

Maybe some of the answers / discussions in https://github.com/lukeautry/tsoa/issues/855 could help ?

lazharichir commented 3 years ago

Rather new to TSOA myself so I may have missed an obvious point. We have our domain and persistence already written, one being checkAuthTokenUsecase(token: string, secret: string, userStore: UserStore) (JWT) and its counterpart generateAuthToken(user: User, secret: string, userStore: UserStore).

I would need, in my authentication module function (export function expressAuthentication( request: express.Request, securityName: string, scopes?: string[] ): Promise<any>) to inject the UserStore instance – so I can pass it on to our use case function.

I do not see where to pass additional parameters to the authentication module function.

tsimbalar commented 3 years ago

@lazharichir do the comments in here help : https://github.com/lukeautry/tsoa/issues/855#issuecomment-735431085 ?

github-actions[bot] commented 3 years ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days