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
Related
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!
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'm trying to create a user token based on the secret of the user trying to log in. However instead of using a secret from the environment I want to use a secret assigned to a user object inside the database.
import { Injectable } from '#nestjs/common';
import { JwtService } from '#nestjs/jwt';
import { UserService } from '#src/modules/user/services';
#Injectable()
export class AuthService {
public constructor(private readonly jwtService: JwtService,
private readonly userService: UserService) {}
public async createToken(email: string): Promise<JwtReply> {
const expiresIn = 60 * 60 * 24;
const user = await this.userService.user({ where: { email } });
const accessToken = await this.jwtService.signAsync({ email: user.email },
/* user.secret ,*/
{ expiresIn });
return {
accessToken,
expiresIn,
};
}
}
I'm new to Nestjs and maybe I'm missing something.
node-jsonwebtoken does provide the necessary parameter in the sign(...) function. nestjs/jwt is missing this parameter (see code). How would you solve it without using node-jsonwebtoken or maybe a more abstract question: does my way of handling user secret make sense here? Thanks.
This is not yet possible solely with nest's JwtModule but you can easily implement the missing parts yourself.
Live Demo
You can create tokens by calling the following routes:
user1 (secret: '123'): https://yw7wz99zv1.sse.codesandbox.io/login/1
user2 (secret: '456'): https://yw7wz99zv1.sse.codesandbox.io/login/2
Then call the protected route '/' with your token and receive your user:
curl -X GET https://yw7wz99zv1.sse.codesandbox.io/ \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxIiwiaWF0IjoxNTUzNjQwMjc5fQ.E5o3djesqWVHNGe-Hi3KODp0aTiQU9X_H3Murht1R5U'
How does it work?
In the AuthService I'm just using the standard jsonwebtoken library to create the token. You can then call createToken from your login route:
import * as jwt from 'jsonwebtoken';
export class AuthService {
constructor(private readonly userService: UserService) {}
createToken(userId: string) {
const user = this.userService.getUser(userId);
return jwt.sign({ userId: user.userId }, user.secret, { expiresIn: 3600 });
}
// ...
}
In the JwtStrategy you use secretOrKeyProvider instead of secretOrKey which can asynchronously access the UserService to get the user secret dynamically:
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKeyProvider: (request, jwtToken, done) => {
const decodedToken: any = jwt.decode(jwtToken);
const user = this.userService.getUser(decodedToken.userId);
done(null, user.secret);
},
});
}
// ...
}
Note that the options you pass to the JwtModule like expiresIn will not be used, instead directly pass your options in the AuthService. Import the JwtModule without any options:
JwtModule.register({})
General
Does my way of handling user secret make sense here?
This is hard to answer without knowing your exact requirements. I guess there are use cases for jwt with dynamic secrets but with it you are losing a great property of jwt: they are stateless. This means that your AuthService can issue a jwt token and some ProductService that requires authentication can just trust the jwt (it knows the secret) without making any calls to other services (i.e. UserService which has to query the database).
If user-related keys are not a hard requirement consider rotating the keys frequently instead by making use of jwt's kid property.
The option to add secret into JwtSignOptions has been added in nestjs/jwt version 7.1.0.
With that, the example would be:
public async createToken(email: string): Promise<JwtReply> {
const expiresIn = 60 * 60 * 24;
const user = await this.userService.user({ where: { email } });
const accessToken = await this.jwtService.signAsync(
{ email: user.email },
{ expiresIn,
secret: user.secret,
});
return {
accessToken,
expiresIn,
};
}
I had also case to sign access and refresh tokens with different secret keys.
If u follow nestjs docs, u see JwtModule is registered with single config and token is signed without options (with default config). To use jwtService sign function with options import JwtModule.register with empty object
import { JwtModule } from '#nestjs/jwt';
#Module({
imports: [JwtModule.register({})],
providers: [],
controllers: []
})
export class AuthModule {}
And making config file with different sign options
#Injectable()
export class ApiConfigService {
constructor(private configService: ConfigService) {
}
get accessTokenConfig(): any {
return {
secret: this.configService.get('JWT_ACCESS_TOKEN_KEY'),
expiresIn: eval(this.configService.get('JWT_ACCESS_TOKEN_LIFETIME'))
}
}
get refreshTokenConfig(): any {
return {
secret: this.configService.get('JWT_REFRESH_TOKEN_KEY'),
expiresIn: eval(this.configService.get('JWT_REFRESH_TOKEN_LIFETIME'))
}
}
}
u may sign token with desired config
#Injectable()
export class AuthService {
constructor(private jwtService: JwtService, private apiConfigService: ApiConfigService ) {}
login(user: any) {
let payload = {username: user.username, id: user.id};
let jwt = this.jwtService.sign(payload, this.apiConfigService.accessTokenConfig);
//
return { token: jwt };
}
}
I'm trying to figure out an appropriate way of doing authentication, which I know is a touchy subject on the GitHub issue page.
My authentication is simple. I store a JWT token in the session. I send it to a different server for approval. If I get back true, we keep going, if I get back false, it clears the session and puts sends them to the main page.
In my server.js file I have the following (note- I am using the example from nextjs learn and just adding isAuthenticated):
function isAuthenticated(req, res, next) {
//checks go here
//if (req.user.authenticated)
// return next();
// IF A USER ISN'T LOGGED IN, THEN REDIRECT THEM SOMEWHERE
res.redirect('/');
}
server.get('/p/:id', isAuthenticated, (req, res) => {
const actualPage = '/post'
const queryParams = { id: req.params.id }
app.render(req, res, actualPage, queryParams)
})
This works as designed. If I refresh the page /p/123, it will redirect to the /. However, if I go there via a next/link href, it doesn't. Which I believe is because it's not using express at this point but next's custom routing.
Is there a way I can bake in a check for every single next/link that doesn't go through express so that I can make sure the user is logged in?
Tim from the next chat helped me solve this. Solution can be found here but I will quote him so you all can see:
You can do the check in _app.js getInitialProps and redirect like this
Example of how to use it
_app.js documentation
I've also created an example skeleton template you can take a look at.
--
EDIT July 2021 - WARNING: This is an outdated solution and has not been confirmed to work with the latest versions of next.js. Use skeleton template at your own risk.
Edit: Updated answer for Next 12.2+
Note: The below contents is copied from the official blog post since SO generally discourages links that can become stale/dead over time
https://nextjs.org/blog/next-12-2#middleware-stable
Middleware is now stable in 12.2 and has an improved API based on feedback from users.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
// If the incoming request has the "beta" cookie
// then we'll rewrite the request to /beta
export function middleware(req: NextRequest) {
const isInBeta = JSON.parse(req.cookies.get('beta') || 'false');
req.nextUrl.pathname = isInBeta ? '/beta' : '/';
return NextResponse.rewrite(req.nextUrl);
}
// Supports both a single value or an array of matches
export const config = {
matcher: '/',
};
Migration guide
https://nextjs.org/docs/messages/middleware-upgrade-guide
Breaking changes
No Nested Middleware
No Response Body
Cookies API Revamped
New User-Agent Helper
No More Page Match Data
Executing Middleware on Internal Next.js Requests
How to upgrade
You should declare one single Middleware file in your application, which should be located next to the pages directory and named without an _ prefix. Your Middleware file can still have either a .ts or .js extension.
Middleware will be invoked for every route in the app, and a custom matcher can be used to define matching filters. The following is an example for a Middleware that triggers for /about/* and /dashboard/:path*, the custom matcher is defined in an exported config object:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
// Supports both a single string value or an array of matchers
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
Edit: Outdated answer for next > 12 and < 12.2
With the release of Next.js 12, there's now beta support for middleware using Vercel Edge Functions.
https://nextjs.org/blog/next-12#introducing-middleware
Middleware uses a strict runtime that supports standard Web APIs like fetch. > This works out of the box using next start, as well as on Edge platforms like Vercel, which use Edge Functions.
To use Middleware in Next.js, you can create a file pages/_middleware.js. In this example, we use the standard Web API Response (MDN):
// pages/_middleware.js
export function middleware(req, ev) {
return new Response('Hello, world!')
}
JWT Authentication example
https://github.com/vercel/examples/tree/main/edge-functions/jwt-authentication
in next.config.js:
const withTM = require('#vercel/edge-functions-ui/transpile')()
module.exports = withTM()
in pages/_middleware.js:
import { NextRequest, NextResponse } from 'next/server'
import { setUserCookie } from '#lib/auth'
export function middleware(req: NextRequest) {
// Add the user token to the response
return setUserCookie(req, NextResponse.next())
}
in pages/api/_middleware.js:
import type { NextRequest } from 'next/server'
import { nanoid } from 'nanoid'
import { verifyAuth } from '#lib/auth'
import { jsonResponse } from '#lib/utils'
export async function middleware(req: NextRequest) {
const url = req.nextUrl
if (url.searchParams.has('edge')) {
const resOrPayload = await verifyAuth(req)
return resOrPayload instanceof Response
? resOrPayload
: jsonResponse(200, { nanoid: nanoid(), jwtID: resOrPayload.jti })
}
}
in pages/api/index.js:
import type { NextApiRequest, NextApiResponse } from 'next'
import { verify, JwtPayload } from 'jsonwebtoken'
import { nanoid } from 'nanoid'
import { USER_TOKEN, JWT_SECRET_KEY } from '#lib/constants'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({
error: { message: 'Method not allowed' },
})
}
try {
const token = req.cookies[USER_TOKEN]
const payload = verify(token, JWT_SECRET_KEY) as JwtPayload
res.status(200).json({ nanoid: nanoid(), jwtID: payload.jti })
} catch (err) {
res.status(401).json({ error: { message: 'Your token has expired.' } })
}
}
There is no middleware for no API routes in NextJS, but there are HOCs, which you can use to connect to db - select the user, etc:
https://hoangvvo.com/blog/nextjs-middleware
I am struggling with Typescript and modifying the definition of existing module.
We are used to put anything we want to output to "res.out" and at the end there is something like this "res.json(res.out)". This allows us to have general control over the app at the moment of sending the response.
So I have function like this
export async function register(req: Request, res: Response, next: Next) {
try {
const user = await userService.registerOrdinaryUser(req.body)
res.status(201);
res.out = user;
return helper.resSend(req, res, next);
} catch (ex) {
return helper.resError(ex, req, res, next);
}
};
We are using restify. And I get compilation error, because "out" is not part of restify.Response.
Now we have workaround that we have our "own" objects, that extends the Restify ones.
import {
Server as iServer,
Request as iRequest,
Response as iResponse,
} from 'restify'
export interface Server extends iServer {
}
export interface Request extends iRequest {
}
export interface Response extends iResponse {
out?: any;
}
export {Next} from 'restify';
We just did this to make project compilable, but looking for better solution. I have tried things like this:
/// <reference types="restify" />
namespace Response {
export interface customResponse;
}
interface customResponse {
out?: any;
}
But it does not work, right now it says "Duplicate identifier 'Response'".
So anyone how to add definition to restify.Response object with some simple code?
You can use interface merging.
import { Response } from "restify";
declare module "restify" {
interface Response {
out?: any
}
}