3

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

kepair
  • 67
  • 6

2 Answers2

6

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.

Christoph Schubert
  • 1,089
  • 1
  • 8
  • 16
  • Hi Christoph, how would you change this solution to enforce authentication but not require a scope? – Niklas May 14 '20 at 07:55
  • Without having it tested I would probably adjust the `ScopesGuard` to not check for scopes but "only" calling the `verifyJwt` method. If you then also adjust the class names you could create an annotation like `@Authenticated()` which only verifies that the request has a valid JWT, but doesn't check any scopes. – Christoph Schubert May 14 '20 at 10:52
  • Hi Christoph, thanks for the reply. This is working to the point where verifyJwt is called - here I get an error "Failed to fetch verification keys from XSUAA service instance https://authentication.eu10.hana.ondemand.com" which is not the instance bound to the service. Since this seems to be determined automatically: how can I change it? – Niklas May 18 '20 at 07:11
  • There recently has been a change in how tokens have to be verified on SAP Cloud Platform. If you haven't done so already, can you upgrade to the latest version (1.20.1) and check if the problem persists? – Dennis H May 18 '20 at 15:30
3

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

Gopal Anand
  • 99
  • 4
  • 14