3
votes

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

2

2 Answers

6
votes

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!

  1. 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.

  2. 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.

  3. 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.

  4. 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.

2
votes

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.

  1. 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