2

I have a loopback 4 project and I am using @loopback/authentication: ^6.0.0 to add JWT authentication. I followed the official documentaion and I wired it up to my MongoDB. All of this went well and I can secure endpoints. However, progress has come to screeching halt. When a user logs in the system generates a JWT token and I need to add a userId to the payload. I just can't figure out how to add anything to the payload.

The code that creates the JWT at login is:

  @post('/users/login', {
    responses: {
      '200': {
        description: 'Token',
        content: {
          'application/json': {
            schema: {
              type: 'object',
              properties: {
                token: {
                  type: 'string',
                },
              },
            },
          },
        },
      },
    },
  })
  async login(
    @requestBody(CredentialsRequestBody) credentials: Credentials,
  ): Promise<{token: string}> {
    // ensure the user exists, and the password is correct
    const user = await this.userService.verifyCredentials(credentials);
    // convert a User object into a UserProfile object (reduced set of properties)
    const userProfile = this.userService.convertToUserProfile(user);
    // create a JSON Web Token based on the user profile
    const token = await this.jwtService.generateToken(userProfile);
    return {token};
  }

I first tried to manually add userId from the user object to the userProfile object and that didn't work. When I check the generateToken method I find it is typed to UserProfile which is defined as:

export interface UserProfile extends Principal {
    email?: string;
    name?: string;
}

Just to test I tried to add my userID: number; parameter but that doesn't work. The payload generated is always:

{
  "id": "84f0106e-3d47-4af5-8ea3-f8d41194be87",
  "name": "chrisloughnane",
  "email": "test@gmail.com",
  "iat": 1597973078,
  "exp": 1597994678
}

The name parameter is also confusing because I do not that a name in my User object definition. It does have a username, how does it bind this, I can't find the code.

How can I add extra parameters to the generate JWT payload?

Curious When a request is made to a secured endpoint this payload is then processed so the userId is used to fetch the correct data. I can write a function to manually decode the payload, is there a built in function to do this?

UPDATE: The solution to access the payload I came up with was to inject SecurityBindings.USER to the constructor of my controller and then assign it to a variable that can be used in any endpoint.

constructor(
    @repository(IconsRepository)
    public iconsRepository: IconsRepository,
    @inject(SecurityBindings.USER, {optional: true})
    public user: UserProfile,
  ) {
    this.userId = this.user[securityId];
  }
chris loughnane
  • 2,648
  • 4
  • 33
  • 54

1 Answers1

0

You need to extends UserProfile interface.

Example:

export interface UserJWT extends UserProfile {
   someProperty: string;
}

Full example is the following:

STEP 1: Refactor JWTService

@bind({scope: BindingScope.TRANSIENT})
export class JWTService implements TokenService {
    constructor() {}

    async verifyToken(token: string): Promise<UserJWT> {
        try {
            return await jwt.verify(token, 'your secret') as UserJWT;
        } catch (err) {
            throw new HttpErrors.Unauthorized('Invalid token');
        }
    }

    async generateToken(user: UserJWT): Promise<string> {
        return await jwt.sign(user, 'secret', {
            expiresIn: 'exp timer'
        });
    }
}

STEP 2: Bind service to keys.ts

export namespace TokenServiceConstants {
    export const TOKEN_SECRET = 'somevalue';
    export const TOKEN_EXP = '600000';
}

export namespace TokenServiceBindings {
    export const TOKEN_SECRET = BindingKey.create<string> ('authentication.jwt.secret');
    export const TOKEN_EXP = BindingKey.create<string> ('authentication.jwt.expires.in');

    export const TOKEN_SERVICE = BindingKey.create<TokenService> ('services.authentication.jwt.tokenservice'); // TokenService imported from @loopback/authentication !!
}

STEP 3: You could bind on new JWTAuthenticationComponet as following

export class JWTAuthenticationComponent implements Component {
    bindings: Binding[] = [
        Binding.bind(TokenServiceBindings.TOKEN_SECRET).to(TokenServiceConstants.TOKEN_SECRET),
        Binding.bind(TokenServiceBindings.TOKEN_EXP).to(TokenServiceConstants.TOKEN_EXP),
        Binding.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(JWTService),
        Binding.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService)
    ]

    constructor(
        @inject(CoreBindings.APPLICATION_INSTANCE) app: Application
    ) {
        registerAuthenticationStrategy(app, JWTAuthenticationStrategy);
    }
}

STEP 4: Add component to application.ts

this.component(AuthenticationComponent);
this.component(JWTAuthenticationComponent); // imported from previous step

STEP 5: Add AuthenticationFn to sequence.ts

constructor(
    @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
    @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
    @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
    @inject(SequenceActions.SEND) public send: Send,
    @inject(SequenceActions.REJECT) public reject: Reject,
    @inject(AuthenticationBindings.AUTH_ACTION) protected authRequest: AuthenticateFn // imported from @loopback/authentication
  ) {}

  async handle(context: RequestContext) {
    try {
      const {request, response} = context;
      const finished = await this.invokeMiddleware(context);
      if (finished) return;
      const route = this.findRoute(request);

      await this.authRequest(request);

      const args = await this.parseParams(request, route);
      const result = await this.invoke(route, args);
      this.send(response, result);
    } catch (err) {
      if (err.code === AUTHENTICATION_STRATEGY_NOT_FOUND || err.code === USER_PROFILE_NOT_FOUND) Object.assign(err, {statusCode: 401});
      
      this.reject(context, err);
    }
  }

STEP 6: Use @authenticate('jwt)

@post('/route', {})
@authenticate('jwt')
async someFunc(): Promise<AnyObject> {}

Extra: retrive id

@post('/route', {})
@authenticate('jwt')
async someFunc(
    @inject(SecurityBindings.USER) currentUser: UserJWT
): Promise<AnyObject> {
    return { userId: currentUser.id } // NEVER user securityId !!
}

Extra 2: Use generateToken

constructor(
   @inject(TokenServiceBindings.TOKEN_SERVICE) public tokenService: TokenService
) {}

@post('/route', {})
@authenticate('jwt')
async someFunc(): Promise<AnyObject> {
   return { token: this.tokenService.generateToken(userProfile) }
}

Hope i can help you.

lorenzoli
  • 45
  • 5