I have the below two guards in NestJS(one for api key based authentication and another for token based authentication).The ApiKeyGuard is the top priority.I want to implement a system where if anyone has a key it will not check the other guard.Is there any way I can make the AuthGuard optional based on whether the first Guard passed in cases where there is a ApiKeyGuard?
// Can be accessed with token within app as well as third party users
#UseGuards(ApiKeyGuard, AuthGuard)
#Get('/get-products')
async getProducts(): Promise<any> {
try {
return this.moduleRef
.get(`appService`, { strict: false })
.getProducts();
} catch (error) {
throw new InternalServerErrorException(error.message, error.status);
}
}
// Only to be accessed with token within app
#UseGuards(AuthGuard)
#Get('/get-users')
async getUsers(): Promise<any> {
try {
return this.moduleRef
.get(`appService`, { strict: false })
.getUsers();
} catch (error) {
throw new InternalServerErrorException(error.message, error.status);
}
}
The below guard is used to check for api key based authentication
api-key.guard.ts
#Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private readonly apiKeyService: ApiKeyService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const key = req.headers['X-API-KEY'] ?? req.query.api_key;
return this.apiKeyService.isKeyValid(key);
}
The below guard is used to check for token based authentication
authentication.guard.ts
#Injectable()
export class AuthGuard implements CanActivate, OnModuleInit {
constructor(private readonly moduleRef: ModuleRef) {}
onModuleInit() {}
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
// Get request data and validate token
const request = context.switchToHttp().getRequest();
if (request.headers.authorization) {
const token = request.headers.authorization.split(' ')[1];
const response = await this.checkToken(token);
if (response) {
return response;
} else {
throw new UnauthorizedException();
}
} else {
throw new UnauthorizedException();
}
} catch (error) {
throw new UnauthorizedException();
}
}
}
What I did was using #nestjs/passport and using the AuthGuard and making custom PassportJS strategies. I had a similar issue, and looked for a way to accomplish this without using some "magic". The documentation can be found here.
In the AuthGuard, you can add multiple guards. It's a bit hidden away in the documentation, although it is very powerful. Take a look here, especially the last line of the section, it states:
In addition to extending the default error handling and authentication logic, we can allow authentication to go through a chain of strategies. The first strategy to succeed, redirect, or error will halt the chain. Authentication failures will proceed through each strategy in series, ultimately failing if all strategies fail.
Which can be done like so:
export class JwtAuthGuard extends AuthGuard(['strategy_jwt_1', 'strategy_jwt_2', '...']) { ... }
Now, back to your example, you've to create 2 custom strategies, one for the API key and one for the authorization header, and both these guards should be activated.
So for the API strategy (as example):
import { Strategy } from 'passport-custom';
import { Injectable } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import { Strategy } from 'passport-custom';
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
#Injectable()
export class ApiStrategy extends PassportStrategy(Strategy, 'api-strategy') {
constructor(private readonly apiKeyService: ApiKeyService) {}
async validate(req: Request): Promise<User> {
const key = req.headers['X-API-KEY'] ?? req.query.api_key;
if ((await this.apiKeyService.isKeyValid(key)) === false) {
throw new UnauthorizedException();
}
return this.getUser();
}
}
Do something similar for your other way of authenticating, and then use the Passport guard as follows:
#UseGuard(AuthGuard(['api-strategy', 'other-strategy'])
This way, the guard will try all strategies (in order) and when all of them fail, your authentication has failed. If one of them succeeds, you're authenticated!
Related
I have created a guard
#Injectable()
export class EmailConfirmationGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request: RequestWithUser = context.switchToHttp().getRequest();
console.log(request.user);
if (!request.user?.hasEmailConfirmed) {
throw new UnauthorizedException("Confirm your email first before updating your profile");
}
return true;
}
}
And i am using it on of my endpoints
#UseGuards(JwtAuthGuard)
#UseGuards(EmailConfirmationGuard)
#Post("/update-profile")
#UseInterceptors(FileInterceptor("file"))
async updateProfile(#UploadedFile() file: Express.Multer.File, #Body("full-name") fullname: string,#Request() req) {
The point is it is faling because getRequest is not returning the authenticated user it is returning undefined
const request: RequestWithUser = context.switchToHttp().getRequest();
How can i return the authenticated user from the response ?
You should use your JwtAuthGuard at your controller level since nest doesn't have an order to run the decorators.
#UseGuards(JwtAuthGuard)
export class YourController{
#UseGuards(EmailConfirmationGuard)
#Post()
public async yourFunction() {}
}
I create an authentication middleware in NestJs like below:
#Injectable()
export class AuthenticationMiddleware implements NestMiddleware {
constructor() {}
async use(req: any, res: any, next: () => void) {
const authHeaders = req.headers.authorization;
if (authHeaders) {
//some logic etc.
//req.user = user;
next();
} else {
throw new UnathorizedException();
}
}
}
... where I get from headers - an auth token, decode it and check if this user is correct and exists in database, if he exists then i set user object into req.user. And now I have a question, how to get this req.user in my services and use in business logic? I need to get id from req.user but I do not know how.
I know that I can do this by using #Req() request in controller parameters and pass this request into my function, but I do not want it, cause is (for me) a ugly practice. So, how to get this req.user into my services?
thanks for any help!
Well, to get the user in the service you have two options:
use #Req() in the controller and pass it, as you have mentioned
Make your service REQUEST scoped and inject the request object into the service
Personally, I'd go with the former, as request scoping has its own pros and cons to start weighing and dealing with (like not being able to use the service in a passport strategy or a cron job). You can also just make the user optional, or bundle it into the body or whatever is passed to the service and then have access to it without it being an explicit parameter.
You can create a decorator to do it. Something like this
current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '#nestjs/common';
export const CurrentUser = createParamDecorator(
(property: string, ectx: ExecutionContext) => {
const ctx = ectx.getArgByIndex(1);
return property ? ctx.req.user && ctx.req.user[property] : ctx.req.user;
},
);
example.controller.ts
#ApiTags('example')
#Controller('example')
export class ExampleController {
constructor(private readonly exampleService: ExampleService) {}
#Get('/')
public async doSomething(#CurrentUser() user: YourUserClassOrInteface,): Promise<any> {
return this.exampleService.exampleFunction(user.id);
}
}
example.service.ts
export class ExampleService {
constructor() {}
public async exampleFunction(id: string): Promise<void> {
console.log('id:', id);
return;
}
}
IMPORTANT: Injecting the Request in the services is not a good solution because it will make a new one in each endpoint request. That is why the Decorators are used. It will make it easy to work with needed data and do not hand over only the parameters that are needed instead of transferring the extra big request object.
Alternative solution(if you won't use request scoped injection): you can use async hooks. There is many libraries which simplify async hooks usage, like this one. You simply set your context in middleware:
#Injectable()
export class AuthenticationMiddleware implements NestMiddleware {
constructor() {}
async use(req: any, res: any, next: () => void) {
const authHeaders = req.headers.authorization;
if (authHeaders) {
//some logic etc.
//req.user = user;
Context.run(next, { user: req.user });
} else {
throw new UnathorizedException();
}
}
}
And then you can get user instance in any place in your code by simply calling Context.get()
You can define your own Request interface like this
import { Request } from 'express';
...
export interface IRequestWithUser extends Request {
user: User;
}
then just give the type of req parameter to IRequestWithUser.
I can't get user from request in decorator nest, pleas help me.
Middleware good working it find user by token and save user in request
my middleware:
import { Injectable, NestMiddleware, HttpStatus } from '#nestjs/common';
import { HttpException } from '#nestjs/common/exceptions/http.exception';
import { Request, Response } from 'express';
import { AuthenticationService } from '../modules/authentication-v1/authentication.service';
#Injectable()
export class AuthenticationMiddleware implements NestMiddleware {
constructor(
private readonly authenticationService : AuthenticationService
) {
}
async use(req: Request, res: Response, next: Function) {
let token = req.headers;
if(!token) {
throw new HttpException('token is required', 401);
}
if (!token.match(/Bearer\s(\S+)/)) {
throw new HttpException('Unsupported token', 401);
}
const [ tokenType, tokenValue ] = token.split(' ');
try {
const result = await this.authenticationService.getAccessToken(tokenValue);
req.user = result;
next();
} catch (e) {
throw new HttpException(e.message, 401);
}
}
}
but here request don't have property user and i don't know why
user decorator:
export const User = createParamDecorator((data: any, req) => {
return req.user; // but here user undefined
});
app module:
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthenticationMiddleware)
.forRoutes({ path: 'auto-reports-v1', method: RequestMethod.GET });
}
}
route method:
#UseInterceptors(LoggingInterceptor)
#Controller('auto-reports-v1')
#ApiTags('auto-reports-v1')
export class AutoReportsController {
constructor(private readonly autoReportsService: AutoReportsService) {}
#Get()
async findAll(
#Query() filter: any,
#User() user: any): Promise<Paginated> {
return this.autoReportsService.findPaginatedByFilter(filter, user);
}
}
In NestJS with Fastify, middleware attaches values to req.raw. This is because middleware runs before the request gets wrapped by the FastifyRequest object, so all value attachments are made to the IncomingRequest object (same as Express Request object). Fastify will then wrap the IncomingRequest in its own FastifyRequest object and expose the IncomingRequest through req.raw meaning the user you are looking for is at req.raw.user not req.user. If you want to have the same functionality across Express and Fastify, I'd suggest using a Guard instead.
Accepted answer didn't help me, but the "custom decorators" from nest js documentation uses createParamDecorator with ExecutionContext as second parameter, and this worked for me:
import { createParamDecorator, ExecutionContext } from '#nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
I'm following the tutorials at developers.sap.com for the Javascript:
Get Started with SAP Cloud SDK for JavaScript.
I created my application with:
sap-cloud-sdk init my-sdk-project
Now I'd like to add security to it, specifically, I want to use an approuter to access the app and I want to block any unauthenticated request to the service directly.
Optionally, I want to include scopes for the different endpoints of my app.
I don't have any problem adding an approuter, but when it comes to secure the node app, I can't seem to find the right way.
I can only find examples of securing an app with basic express node apps like these ones:
Hello World Sample using NodeJS
node.js Hello World
But they have a different structure that the one provided by sap-cloud-sdk tool, which uses nestjs.
The Help Portal doesn't point to any examplet either if you are using Nestjs.
Is there any resource, tutorial, or example to help me implement security in an scaffolded app?
Kr,
kepair
There is no resource yet on how to setup Cloud Foundry security with the Cloud SDK for JS, but I tinkered around with it a bit in the past with the following result.
Disclaimer: This is by no means production ready code! Please take this only as a inspiration and verify all behavior on your side via tests as well as adding robust error handling!
Introduce a scopes.decorator.ts file with the following content:
import { SetMetadata } from '#nestjs/common';
export const ScopesMetadataKey = 'scopes';
export const Scopes = (...scopes: string[]) => SetMetadata(ScopesMetadataKey, scopes);
This will create an annotation that you can add to your controller method in a follow up step. The parameters given will be the scopes that an endpoint requires before being called.
Create a Guard scopes.guard.ts like the following:
import { CanActivate, ExecutionContext, Injectable } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
import { retrieveJwt, verifyJwt } from '#sap/cloud-sdk-core';
import { getServices } from '#sap/xsenv';
import { ScopesMetadataKey } from './scopes.decorator';
#Injectable()
export class ScopesGuard implements CanActivate {
private xsappname;
constructor(private readonly reflector: Reflector) {
this.xsappname = getServices({ uaa: { label: 'xsuaa' } }).uaa.xsappname;
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const scopes = this.reflector.get<string[]>(ScopesMetadataKey, context.getHandler());
if (!scopes) {
return true;
}
const request = context.switchToHttp().getRequest();
const encodedJwt = retrieveJwt(request);
if (!encodedJwt) {
return false;
}
const jwt = await verifyJwt(encodedJwt);
return this.matchScopes(scopes, jwt.scope);
}
private matchScopes(expectedScopes: string[], givenScopes: string[]): boolean {
const givenSet = new Set(givenScopes);
return expectedScopes.every(scope => givenSet.has(this.xsappname + '.' + scope));
}
}
This Guard should be called before all endpoints and verifies that all requires scopes are present in the incoming JWT.
Add the guard to your nest application setup:
import { Reflector } from '#nestjs/core';
import { ScopesGuard } from './auth/scopes.guard';
// ...
const app = ...
const reflector = app.get(Reflector)
app.useGlobalGuards(new ScopesGuard(reflector));
// ...
This ensures that all incoming requests are actually "guarded" by your guard above.
Use the annotation created in the first step on your protection worthy endpoints:
import { Controller, Get } from '#nestjs/common';
import { Scopes } from '../auth/scopes.decorator';
#Controller('/api/rest/foo')
export class FooController {
constructor(private readonly fooService: FooService) {}
#Get()
#Scopes('FooViewer')
getFoos(): Promise<Foo[]> {
return this.fooService.getFoos();
}
}
This endpoint is now only callable if a JWT with the required scope is provided.
You can use the standard nodejs authentication implementation in sap-cloud-sdk/nest.js project without creating any middleware.
Since the JWTStrategy which is part of #sap/xssec have the middleware implementation, things are very simplified.
For Authentication change main.ts
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { getServices } from '#sap/xsenv';
const xsuaa = getServices({ xsuaa: { tag: 'xsuaa' } }).xsuaa;
import * as passport from 'passport';
import { JWTStrategy } from '#sap/xssec';
passport.use(new JWTStrategy(xsuaa));
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));
await app.listen(process.env.PORT || 3000);
}
bootstrap();
This will initialize the middleware.
2. For scope check and authorization
import { Controller, Get, Req, HttpException, HttpStatus } from '#nestjs/common';
import { AppService } from './app.service';
#Controller()
export class AppController {
constructor(private readonly appService: AppService) { }
#Get()
getHello(#Req() req: any): any {
console.log(req.authInfo);
const isAuthorized = req.authInfo.checkLocalScope('YourScope');
if (isAuthorized) {
return req.user;
} else {
return new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
// return this.appService.getHello();
}
}
For more details please refer to this
Is it possible to make a redirect from a Nest controller without the usage of the #Response object?
For now I know that we can only do this via direct #Response object injection into the route handler.
You can write a RedirectInterceptor:
#Injectable()
export class RedirectInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, stream$: Observable<any>): Observable<any> {
const response = context.switchToHttp().getResponse();
response.redirect('redirect-target');
return stream$;
}
}
Then use it in your controller like this:
#Get('user')
#UseInterceptors(RedirectInterceptor)
getUser() {
// will be redirected.
}
It is important not to return anything from your controller, otherwise you will get the following error: Can't set headers after they are sent.
If needed the RedirectInterceptor can be dynamic as well:
#Injectable()
export class RedirectInterceptor implements NestInterceptor {
constructor(private readonly target: string) {}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
intercept(context: ExecutionContext, stream$: Observable<any>): Observable<any> {
const response = context.switchToHttp().getResponse();
response.redirect(this.target);
^^^^^^^^^^^
return stream$;
}
}
and then in the controller:
#UseInterceptors(new RedirectInterceptor('redirect-target'))
(a bit of a different implementation to another answer here...)
I created a RedirectError which can be thrown more dynamically than a decorator
import { ExceptionFilter, Catch, ArgumentsHost } from '#nestjs/common';
import { Response } from 'express';
export class RedirectError extends Error {
constructor(public readonly status: number, public readonly url: string) {
super();
}
}
#Catch(RedirectError)
export class RedirectFilter implements ExceptionFilter {
public catch(exception: RedirectError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
return response.redirect(exception.status, exception.url);
}
}
and then in main.ts set it:
app.useGlobalFilters(new RedirectFilter());
and finally to use it:
throw new RedirectError(302, '/some-target');
I've done it more complex, but I think it is good enough.
Create a class such as util/RedirectException like this:
The code like this:
import { HttpException, HttpStatus } from '#nestjs/common';
export class RedirectException extends HttpException {
constructor(message?: string | object) {
super(message, HttpStatus.CONTINUE);
}
}
Create a RedirectFilter by: nest g f RedirectFilter
Write the code like this:
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '#nestjs/common';
import { RedirectException } from './util/redirect-exception';
#Catch()
export class RedirectFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const res = host.switchToHttp().getResponse(),
req = host.switchToHttp().getRequest();
try {
if (exception instanceof RedirectException) {
Object.keys(exception.message).forEach(k => {
req.session[k] = exception.message[k];
});
req.session.save(() => {
res.redirect(exception.message.url);
});
return;
}
if (exception instanceof HttpException) {
return res.status(exception.status).json(exception.message)
}
res.status(500).json({status: 500, message: 'Internal Server error'})
} catch (e) {
return res.status(500)
.json({
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: e.message
});
}
}
}
This class help you handle all the response when an exception is throw. And yes, this include the Redirect exception. Now we can throw the exception with exactly params, and it work!
Use the filter in main.ts: app.useGlobalFilters(new RedirectFilter());
And in controller, if you want to redirect to an url, just do this any time you want
Code:
throw new RedirectException({
url: 'the url you want to redirect',
field1: 'The first field you want to pass to session'
field2: 'The second field you want to pass to session'
});
Don't forget setup express-session if you want to pass data by session when redirect: https://www.npmjs.com/package/express-session.
If you don't want to use this, just replace the code inside if (exception instanceof RedirectException) {} to: res.redirect(exception.message.url);. It don't check and setup the session anymore.