In nest.js, is it possible to get service instance inside a param decorator?
Asked Answered
I

4

21

I want to achieve something like this using nest.js: (something very similar with Spring framework)

@Controller('/test')
class TestController {
  @Get()
  get(@Principal() principal: Principal) {

  }
}

After hours of reading documentation, I found that nest.js supports creating custom decorator. So I decided to implement my own @Principal decorator. The decorator is responsible for retrieving access token from http header and get principal of user from my own auth service using the token.

import { createParamDecorator } from '@nestjs/common';

export const Principal = createParamDecorator((data: string, req) => {
  const bearerToken = req.header.Authorization;
  // parse.. and call my authService..
  // how to call my authService here?
  return null;
});

But the problem is that I have no idea how to get my service instance inside a decorator handler. Is it possible? And how? Thank you in advance

Ieyasu answered 7/4, 2019 at 15:50 Comment(0)
A
34

It is not possible to inject a service into your custom decorator.

Instead, you can create an AuthGuard that has access to your service. The guard can then add a property to the request object, which you can then access with your custom decorator:

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const bearerToken = request.header.Authorization;
    const user = await this.authService.authenticate(bearerToken);
    request.principal = user;
    // If you want to allow the request even if auth fails, always return true
    return !!user;
  }
}
import { createParamDecorator } from '@nestjs/common';

export const Principal = createParamDecorator((data: string, req) => {
  return req.principal;
});

and then in your controller:

@Get()
@UseGuards(AuthGuard)
get(@Principal() principal: Principal) {
  // ...
}

Note that nest offers some standard modules for authentication, see the docs.

Argumentum answered 7/4, 2019 at 16:8 Comment(2)
This is not an appropriate use of guards, imo. There's a solution below that makes use of Pipes which is probably a better option in a NestJS context.Eardrop
@TaylorBuckner This is exactly what the default AuthGuard does in the @nestjs/passport library, see github.com/nestjs/passport/blob/…Argumentum
R
20

for NestJS v7

Create custom pipe

// parse-token.pipe.ts
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class ParseTokenPipe implements PipeTransform {
    // inject any dependency
    constructor(private authService: AuthService) {}
    
    async transform(value: any, metadata: ArgumentMetadata) {
        console.log('additional options', metadata.data);
        return this.authService.parse(value);
    }
}

Use this pipe with property decorator

// decorators.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { ParseTokenPipe} from './parse-token.pipe';

export const GetToken = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  return ctx.switchToHttp().getRequest().header.Authorization;
});

export const Principal = (additionalOptions?: any) => GetToken(additionalOptions, ParseTokenPipe);

Use this decorator with or without additional options

@Controller('/test')
class TestController {
  @Get()
  get(@Principal({hello: "world"}) principal) {}
}

Romeu answered 9/5, 2021 at 6:49 Comment(0)
I
0

You can use middlewar for all controller.

auth.middleware.ts


interface AccountData {
  accId: string;
  iat: number;
  exp: number;
}

interface RequestWithAccountId extends Request {
  accId: string;
}

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private readonly authenticationService: AuthenticationService) {}
  async use(req: RequestWithAccountId, res: Response, next: NextFunction) {
    const token =
      req.body.token || req.query.token || req.headers['authorization'];
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const {
        accId,
      }: AccountData = await this.authenticationService.verifyToken(token);
      req.accId = accId;
      next();
    } catch (err) {
      throw new UnauthorizedException();
    }
  }
}

Then create AccountId decorator

account-id.decorator.ts

import {
  createParamDecorator,
  ExecutionContext,
  UnauthorizedException,
} from '@nestjs/common';

export const AccountId = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const req = ctx.switchToHttp().getRequest();
    const token = req.accId;
    if (!token) {
      throw new UnauthorizedException();
    }
    return token;
  },
);

Then apply AccountId decorator in your controller

your.controller.ts

  @Get()
  async someEndpoint(
    @AccountId() accountId,
  ) {
    console.log('accountId',accontId)
  }
Inhospitality answered 13/2, 2021 at 20:8 Comment(1)
using async someEndPoint (@Request() req) { return req.accId } is is much simpler, no need to use decorator...Schoolbag
N
0

The answers here are good, but I didn't like the idea of polluting the request object directly with a string indexed variable. So I used an attempt like this:

basic-auth.guard.ts:

import { UserSession } from '@interfaces/types/auth/user-session';
import { CanActivate, ExecutionContext, Inject, Injectable, createParamDecorator } from '@nestjs/common';
import { FastifyRequest } from 'fastify';
import { AuthService } from '../services/auth/auth.service';

const SYMBOL = Symbol('BasicAuthGuard');

export const Session = createParamDecorator<never, ExecutionContext, UserSession>((_, ctx) => {
  const request = ctx.switchToHttp().getRequest();
  return request[SYMBOL];
})

@Injectable()
export class BasicAuthGuard implements CanActivate {

  constructor(
    @Inject(AuthService)
    private readonly auth_service: AuthService
  ) { }

  async canActivate(context: ExecutionContext): Promise<boolean> {

    const request = context.switchToHttp().getRequest<FastifyRequest>();

    const [type, credential] = request.headers.authorization?.split(' ') || [];

    if (type !== 'Bearer')
      return false;

    const session = await this.auth_service.authenticate({ credential });
    (request as any)[SYMBOL] = session;

    return true;
  }
}

my-controller.ts:


import { UserSession } from '@interfaces/types/auth/user-session';
import { ResidentListSearchParams } from '@interfaces/types/resident';
import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
import { BasicAuthGuard, Session } from '../../guards/basic-auth.guard';
import { ResidentService } from '../../services/resident/resident.service';

@Controller('residents')
export class ResidentController {

  constructor(
    @Inject(ResidentService)
    private resident_service: ResidentService
  ) { }

  @Get()
  @UseGuards(BasicAuthGuard)
  async list(
    @Query() search_params: ResidentListSearchParams,
    @Session() user: UserSession
  ) {
    console.log(user); // { email: '[email protected]', id: 1 }
    return await this.resident_service.list({ search_params });
  }
}
Neusatz answered 6/3 at 17:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.