Dealing with request-specific or session-specific data in LoopBack 4

I am currently facing a challenge within our LoopBack4 application. We have implemented controllers and are using JWT for Authorization. In the token's payload, we include a list of rights granted to the requesting user. Additionally, we have added an AuthorizationInterceptor to verify permissions.

My mistake was storing the token data in a static variable and accessing it from various services and locations within the application. This led to issues where concurrent requests would overwrite each other's tokens. As a result, Request A ended up utilizing the rights of Request B.

The Issue:

  • A client sends Request A to the LB4 application with a token
  • The application stores this token in a static variable
  • Simultaneously, an incoming Request B provides a different token
  • The application replaces the token of Request A with that of Request B
  • Request A ends up operating with the rights of Request B

Application Structure:

In every Controller:

export class MiscController
{
    constructor(@inject(AServiceBindings.VALUE) public aService: AService) {}

    @get('/hasright', {})
    @authenticate('jwt', {"required":[1,2,3]}) // authorization checked by AuthorizationInterceptor
    async getVersion(): Promise<object>
    {
        return {hasRight: JWTService.checkRight(4)};
    }
}

JWT Service:

export class JWTService implements TokenService
{
    static AuthToken: Authtoken|null;
    static rights: number[];

    // constructor ...

    /** Method to check rights */
    static hasRight(rightId: number): boolean
    {
        return inArray(rightId, JWTService.rights);
    }

    async verifyToken(token: string): Promise<UserProfile>
    {
        // verify the token ...

        // store the Token data in static variables
        JWTService.AuthToken = authtoken;
        JWTService.rights = rightIds;

        return userProfile;
    }
}

export const JWTServiceBindings = {
    VALUE: BindingKey.create<JWTService>("services.JWTService")
};

AuthorizeInterceptor.ts

@globalInterceptor('', {tags: {name: 'authorize'}})
export class AuthorizationInterceptor implements Provider<Interceptor>
{
    constructor(
        @inject(AuthenticationBindings.METADATA) public metadata: AuthenticationMetadata,
        @inject(TokenServiceBindings.USER_PERMISSIONS) protected checkPermissions: UserPermissionsFn,
        @inject.getter(AuthenticationBindings.CURRENT_USER) public getCurrentUser: Getter<MyUserProfile>
    ) {}

    /**
     * Interceptor function for the binding.
     *
     * @returns An interceptor function
     */
    value()
    {
        return this.intercept.bind(this);
    }

    /**
     * Intercepting method
     * @param invocationCtx - Invocation context
     * @param next - Function to invoke next interceptor or target method
     */
    async intercept(invocationCtx: InvocationContext, next: () => ValueOrPromise<InvocationResult>)
    {
        if(!this.metadata)
        {
            return next();
        }

        const requiredPermissions = this.metadata.options as RequiredPermissions;
        const user                = await this.getCurrentUser();

        if(!this.checkPermissions(user.permissions, requiredPermissions))
        {
            throw new HttpErrors.Forbidden('Permission denied! You do not have the needed right to request this function.');
        }

        return next();
    }
}

JWTAuthenticationStrategy

export class JWTAuthenticationStrategy implements AuthenticationStrategy
{
    name = 'jwt';

    constructor(@inject(JWTServiceBindings.VALUE) public tokenService: JWTService) {}

    async authenticate(request: Request): Promise<UserProfile | undefined>
    {
        const token: string = this.extractCredentials(request);

        return this.tokenService.verifyToken(token);
    }

    // extract credentials etc ...
}

application.ts

export class MyApplication extends BootMixin(ServiceMixin(RepositoryMixin(RestApplication)))
{
    constructor(options: ApplicationConfig = {})
    {
        super(options);

        // Bind authentication related elements
        this.component(AuthenticationComponent);

        registerAuthenticationStrategy(this, JWTAuthenticationStrategy);
        this.bind(JWTServiceBindings.VALUE).toClass(JWTService);
        this.bind(TokenServiceBindings.USER_PERMISSIONS).toProvider(UserPermissionsProvider);
        this.bind(TokenServiceBindings.TOKEN_SECRET).to(TokenServiceConstants.TOKEN_SECRET_VALUE);

        // Set up custom sequence
        this.sequence(MySequence);

        // additional bindings and setup tasks...
    }
}

sequence.ts

export class MySequence implements SequenceHandler
{
    // constructor ...

    async handle(context: RequestContext)
    {
        try
        {
            const {request, response} = context;

            const route = this.findRoute(request);

            // authenticate request
            await this.authenticateRequest(request);
            userId = getMyUserId(); // using helper method

            // Proceed with invoking controller after successful authentication
            const args   = await this.parseParams(request, route);
            const result = await this.invoke(route, args);
            this.send(response, result);
        }
        catch(err)
        {
            this.reject(context, err);
        }
        finally
        {
            // perform actions using userId ...
        }
    }
}

helper.ts // simple file with utility functions

export function getMyUserId(): number
{
    return ((JWTService.AuthToken && JWTService.AuthToken.UserId) || 0);
}

Additionally, I need a solution to access user data within services and other parts of the application, such as the authorized user's token. Where should I place and how should I manage the token?

Resources consulted: How to use stateful requests in Loopback 4? -> https://github.com/strongloop/loopback-next/issues/1863

I came across recommendations suggesting the use of

const session = this.restoreSession(context);
in a custom sequence.ts file. Although I followed this advice, the function restoreSession is not recognized.

Another suggestion involved implementing the express-session package. However, this option was deemed unsuitable due to client limitations regarding cookie storage.

Answer №1

I successfully resolved an issue by following the instructions outlined in this helpful documentation: https://github.com/strongloop/loopback-next/blob/607dc0a3550880437568a36f3049e1de66ec73ae/docs/site/Context.md#request-level-context-request

My Approach:

  1. I implemented binding context-based values within the sequence.ts file.
export class MySequence implements SequenceHandler
{
    // constructor ...

    async handle(context: RequestContext)
    {
        try
        {
            const {request, response} = context;

            const route = this.findRoute(request);

            // execute authentication action
            const userProfile = await this.authenticateRequest(request);
            context.bind('MY_USER_ID').to(userProfile.id); 

            // Proceed with invoking the controller after successful authentication
            const args   = await this.parseParams(request, route);
            const result = await this.invoke(route, args);
            this.send(response, result);
        }
        catch(err)
        {
            this.reject(context, err);
        }
        finally
        {
            // perform actions using userId...
        }
    }
}

  1. Revamping the JWT-Service to eliminate static approaches:
export class JWTService implements TokenService
{
    // static AuthToken: Authtoken|null; // no longer required
    // static rights: number[]; // no longer required

    // constructor ...

    /** A method to check rights */
    /* Update on how the checking of rights is handled now
    */

    async verifyToken(token: string): Promise<UserProfile>
    {
        // verify the token ...

        return userProfile;
    }
}
  1. Controllers requiring context-bound data should inject the bound value accordingly.
export class aController
{
    constructor(
        @inject('MY_USER_ID') public authorizedUserId: number 
        // Injecting the bound value from the context here
    ) {}

    @get('/myuserid', {})
    @authenticate('jwt', {})
    async getVersion(): Promise<object>
    {
        return this.authorizedUserId; 
    }
}

Each controller and service loaded post-sequence (excluding jwt-service) can now inject the bound value and utilize it effectively.

While the process was a bit intricate as the linked documentation did not cover this exact method, I am pleased that it now works for me. If anyone has alternate solutions, please feel free to share!

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Issue with debugging Azure Functions TypeScript using f5 functionality is unresolved

I am encountering issues running my Azure TypeScript function locally in VS code. I am receiving the errors shown in the following image. Can someone please assist me with this? https://i.stack.imgur.com/s3xxG.png ...

Running a NodeJS Redux example of a Todo List

While browsing through the Redux documentation, I stumbled upon an example app located here. I have a question about how to run this example app on a Node.js server and preview the results in a browser. Would it be better to use Express.js as the framewo ...

Prettier seems to be producing varied outcomes across various machines

My teammate and I are collaborating on the same project, but we're facing an issue where our Prettier configurations conflict. Each time we push our code to Github, his Prettier format overrides mine. Here's an example of his formatting: const in ...

Dispatching an express instance to be handled by http.createServer

After reading the Node.js documentation, it is clear that the http.createServer function requires a requestListener argument, which is a function automatically added to the request event. I am curious about how one can pass an instance of express to http. ...

Issues detected in the performance of custom Express Server on NextJs

Switching to NextJs has been a challenge for me. I have developed an ExpressJs API and now I want to integrate it as a Custom NextJs Server, following the documentation. Unfortunately, I can't seem to get it working properly. Below is my server.ts fi ...

Implementing a hook operation for the hasAndBelongsToMany relationship

Is it possible to set up an operation hook, like after saving, when connecting or disconnecting an instance of the associated model? Take for example loopback's Assembly and Part models: I am interested in running specific code when adding (or remov ...

What is the best way to manage a promise in Jest?

I am encountering an issue at this particular section of my code. The error message reads: Received promise resolved instead of rejected. I'm uncertain about how to address this problem, could someone provide assistance? it("should not create a m ...

There seems to be an issue with creating cookies in the browser using Express.js

When using res.cookie(), I encountered an issue where the cookie was created but not showing in the browser. Although the cookie was not stored, it was visible when using Postman. I attempted to set the cookie using res.cookie("access_token", token, {sec ...

Utilize URL parameters in Node.js and Express for effective communication

I have an endpoint ..com When I perform a curl call, the endpoint looks like this: ..com?param1=true Now, I am attempting to make a similar call from Node.js. However, I'm uncertain about whether param1 should be included in the headers, concatenate ...

Exploring the functionalities of the read and write stream in csv-parse

Hey everyone, I'm new here and feeling a bit confused about how to properly use readstream and writestream. Currently, I'm attempting this (using the library https://www.npmjs.com/package/csv-parse) fs.createReadStream(path.join(__dirname," ...

Express Stripe webhook signature verification failed

I am currently testing a local webhook for a Stripe event, but I keep receiving the following error message: Webhook signature verification failed. Below is my webhook endpoint code: exports.stripeListenWebhook = (req, res) => { let data let e ...

Implementing Autocomplete feature in Reactjs with Ant Design Library

In my React application, I created an autocomplete feature with a list of options for the user to select from. Example in Action: Click here to see the demo List of available options: const options = [ { label: "hello", value: "1" ...

Utilize the built-in compression feature of Express 4.x

After running my local web server through Google's PageSpeed Chrome extension, I discovered that compression is not enabled on my server. My backend is powered by Node.js with Express 4.x. In my search for a solution to compress data, I came across ht ...

What is the best way to create this server backend route?

I'm currently working on a fullstack project that requires a specific sequence of events to take place: When a user submits an event: A request is sent to the backend server The server then initiates a function for processing This function should ru ...

Tips for incorporating JSON data from an API into your frontend interface

After following a tutorial on setting up an API with Express and PostgreSQL, I was able to successfully retrieve all my data in JSON format. However, I am now facing the challenge of using this data on the frontend of a website. The code snippets below ar ...

Vuetify's v-data-table is experiencing issues with displaying :headers and :items

I'm facing an issue while working on a Vue project with typescript. The v-data-table component in my Schedule.vue file is not rendering as expected. Instead, all I can see is the image below: https://i.sstatic.net/AvjwA.png Despite searching extensi ...

Testing a function that utilizes Nitro's useStorage functionality involves creating mock data to simulate the storage behavior

I have developed a custom function for caching management, specifically for storing responses from API calls. export const cache = async (key: string, callback: Function) => { const cacheKey = `cache:${key}`; const data = await useStorage().get ...

Transferring a zipped file between a Node.js server and a Node.js client

I am facing an issue with sending a zip file from a node.js server to a node.js client. The problem is that when I try to save the zip file, it becomes corrupted and cannot be opened. To create and send the zip file to the client, I am using the adm-zip l ...

Unable to verify Angular 5 identity

After authentication, the application should redirect to another page. However, despite successful authentication according to the logs, the redirection does not occur. What could be causing this issue? auth.guard.ts: import { Injectable } from &apos ...

What is the best way to specify data types for all attributes within an interface?

In building an interface for delivering strings in an i18n application: interface ILocaleStringsProvider { 'foo': string 'bar': string 'baz': string 'blablabla': string // numerous other string properties ...