Loopback-next: Loopback 4 authentication not passing role to loopback authorization. Thus getting 401 everytime

Created on 29 May 2020  路  30Comments  路  Source: strongloop/loopback-next

I have implemented jwt authentication using custom user service which implements user-service from loopback authentication/jwt. Successfully issuing a token to the user.
But while implementing authorization, AuthorizationContextdoesn't contain role.
I have checked that AuthorizationContext .principals[0] contains only id and name, not the role.
AuthorizationContext.roles is also empty. Which clearly means authentication is not passing user property role to authorization

Here is the code
GIT repository: https://github.com/pratikjaiswal15/loopbackauthrization

user.model.ts

 @property({
    type: 'string',
  })
  role: string;

authorize.ts

import {AuthorizationContext, AuthorizationDecision, AuthorizationMetadata} from '@loopback/authorization';
import {securityId, UserProfile} from '@loopback/security';
import _ from 'lodash';

// Instance level authorizer
// Can be also registered as an authorizer, depends on users' need.
export async function basicAuthorization(
  authorizationCtx: AuthorizationContext,
  metadata: AuthorizationMetadata,

): Promise<AuthorizationDecision> {

  console.log(authorizationCtx.principals[0])
  console.log(authorizationCtx.roles)
  // No access if authorization details are missing
  let currentUser: UserProfile;
  if (authorizationCtx.principals.length > 0) {
    const user = _.pick(authorizationCtx.principals[0], [
      'id',
      'name',
      'role', // propety role
    ]);
    currentUser = {[securityId]: user.id, name: user.name, roles: user.role};
  } else {
    return AuthorizationDecision.DENY;
  }

  if (!currentUser.roles) {
    return AuthorizationDecision.DENY;
  }

  // Authorize everything that does not have a allowedRoles property
  if (!metadata.allowedRoles) {
    return AuthorizationDecision.ALLOW;
  }

  let roleIsAllowed = false;
  for (const role of currentUser.roles) {
    if (metadata.allowedRoles!.includes(role)) {
      roleIsAllowed = true;
      break;
    }
  }

  if (!roleIsAllowed) {
    return AuthorizationDecision.DENY;
  }

  // Admin and support accounts bypass id verification
  if (
    currentUser.roles.includes('admin') ||
    currentUser.roles.includes('support')
  ) {
    return AuthorizationDecision.ALLOW;
  }

  /**
   * Allow access only to model owners, using route as source of truth
   *
   * eg. @post('/users/{userId}/orders', ...) returns `userId` as args[0]
   */
  if (currentUser[securityId] === authorizationCtx.invocationContext.args[0]) {
    return AuthorizationDecision.ALLOW;
  }

  return AuthorizationDecision.DENY;
}

user.service.ts (authentication)

import {UserService} from '@loopback/authentication';
import {repository} from '@loopback/repository';
import {HttpErrors} from '@loopback/rest';
import {securityId, UserProfile} from '@loopback/security';
import {compare} from 'bcryptjs';
// User --> MyUser
import {User} from '../models/user.model';
// UserRepository --> MyUserRepository
import {Credentials, UserRepository} from '../repositories/user.repository';


export class CustomUserService implements UserService<User, Credentials> {
  constructor(
    // UserRepository --> MyUserRepository
    @repository(UserRepository) public userRepository: UserRepository,
  ) {}

  // User --> MyUser
  async verifyCredentials(credentials: Credentials): Promise<User> {
    const invalidCredentialsError = 'Invalid email or password.';

    const foundUser = await this.userRepository.findOne({
      where: {email: credentials.email},
    });
    if (!foundUser) {
      throw new HttpErrors.Unauthorized(invalidCredentialsError);
    }

    const credentialsFound = await this.userRepository.findCredentials(
      foundUser.id,
    );
    if (!credentialsFound) {
      throw new HttpErrors.Unauthorized(invalidCredentialsError);
    }

    const passwordMatched = await compare(
      credentials.password,
      credentialsFound.password,
    );

    if (!passwordMatched) {
      throw new HttpErrors.Unauthorized(invalidCredentialsError);
    }

    return foundUser;
  }

  // User --> MyUser
  convertToUserProfile(user: User): UserProfile {

    let address = ''
    if (user.address) {
      address = user.address
    }

    const profile = {
      [securityId]: user.id!.toString(),
      name: user.name,
      id: user.id,
      email: user.email,
      role: user.role,
      address: user.address
    }

    console.log(profile)
    return profile


  }
}

question

Most helpful comment

The best person to answer questions on this is @jannyHou who has been working on this brand new component. Janny, how would you recommend to proceed?

All 30 comments

@HrithikMittal: I think you could help @pratikjaiswal15 on the issue.

Yes sure @madaky and thank you for considering me into this

and @pratikjaiswal15 you have to bind your custom user service in your application.ts
if you need more help of how to do it please check this repo
https://github.com/HrithikMittal/Loopback4-auth

and moreover, I run your repo also and you got the error
image

This is because you have not created a password field which is needed in interceptor and also the keys.ts needed some bugs to fix as it should be in order

BindingKey.create<UserService<Credentials, User>

It is advisable to create a project again and before start writing code decide your use case and database modelling is must because when I check there is DB error also.
You can refer and complete docs from this:

Authentication: https://github.com/HrithikMittal/Loopback4-auth
Authorization: https://github.com/HrithikMittal/Loopback4-authorization

I hope this will help you out.
and I think @madaky you close this issue because there is no error related to the loopback4 authentication or authorization this is all because of wrong/incorrect setup of the project.
And thanks again @madaky

Okay thanks I will check it. Thank again

If it will solve your problem then close this issue.

I have already bound custom-user service in application.ts . See here (https://github.com/pratikjaiswal15/loopbackauthrization/blob/master/src/application.ts#L77)

You seem to have implemented authorization differently. I am looking for the standard way with @authorize decorator.
Can you help with that approach? Thanks

Ya you bind it but that is showing the error because of the wrong setup

I have already bound custom-user service in application.ts . See here (https://github.com/pratikjaiswal15/loopbackauthrization/blob/master/src/application.ts#L77)

Please look at the points I mention in the above comment and my implementation is almost the same.

It is not the wrong setup. Actually I changed data types in the user and user-credential model in npm package authentication/jwt. That is the reason you were getting errors.
Anyway, the actual issue is authentication not sending role property to authorization.

Hey, @pratikjaiswal15 Can you tell me where are you creating the jwt strategy. I am not able to find in your project.

Hey, @pratikjaiswal15 Can you tell me where are you creating the jwt strategy. I am not able to find in your project.

Looks like it's a new built in one: https://github.com/pratikjaiswal15/loopbackauthrization/blob/50c5cccb2920e6740b3e5778534ac7db72e364c3/src/application.ts#L3

See extension source.

Hey, @pratikjaiswal15 Can you tell me where are you creating the jwt strategy. I am not able to find in your project.

Looks like it's a new built in one: https://github.com/pratikjaiswal15/loopbackauthrization/blob/50c5cccb2920e6740b3e5778534ac7db72e364c3/src/application.ts#L3

Yes

@dougal83 can you please tell why property role is not getting passed from authentication to authorization?

The best person to answer questions on this is @jannyHou who has been working on this brand new component. Janny, how would you recommend to proceed?

Yes @pratikjaiswal15 you are right you are converting into profile object where you add everything like this

const profile = {
      [securityId]: user.id!.toString(),
      name: user.name,
      id: user.id,
      email: user.email,
      role: user.role,
      address: user.address
    }

after this, you are sending this to generate token right?
and for this, you are using the predefined method of @loopback/authentication-jwt which will take only these parameters to create a token

 const userInfoForToken = {
      id: userProfile[securityId],
      name: userProfile.name,
      email: userProfile.email,
    };

https://github.com/strongloop/loopback-next/blob/62231bf44b57c37d450b49f8e546b7066a653b80/examples/access-control-migration/src/components/jwt-authentication/services/jwt.service.ts#L54

But you need role also in your token for that you can add it by your own.

Oh yes. Let me try this

Hey, @pratikjaiswal15 Can you tell me where are you creating the jwt strategy. I am not able to find in your project.

Looks like it's a new built in one: https://github.com/pratikjaiswal15/loopbackauthrization/blob/50c5cccb2920e6740b3e5778534ac7db72e364c3/src/application.ts#L3

See extension source.

@dougal83 : He has bind his custom strategy here
https://github.com/pratikjaiswal15/loopbackauthrization/blob/50c5cccb2920e6740b3e5778534ac7db72e364c3/src/application.ts#L76-L77

Hi guys lets catch up quickly. I was not hoping it to be so long :100:

Well @pratikjaiswal15 3 quick question to you:
1) Where you have defined your roles.
2) Where at the time of user creation you have bind the roles with user.
3) Are you using some sort of permissions model.

No still not working.

As per shopping example role should be in authorizationCtx.principals[0]https://github.com/strongloop/loopback4-example-shopping/blob/d0822a31bed831504b8655bc057c0de6473ece09/packages/shopping/src/services/basic.authorizor.ts#L26

But in my case authorizationCtx.principals[0] contains only id and name

As per shopping example role should be in authorizationCtx.principals[0]https://github.com/strongloop/loopback4-example-shopping/blob/d0822a31bed831504b8655bc057c0de6473ece09/packages/shopping/src/services/basic.authorizor.ts#L26

But in my case authorizationCtx.principals[0] contains only id and name

Ya you have to add that what we discuss above and all the part which are mentioned by @madaky

Where you have defined your roles.
Where at the time of user creation you have bind the roles with user.
Are you using some sort of permissions model.

No. I have property role in user model. And while making post request to user from frontend or loopback explorer I am specifing property role as admin or user.
Like this

{ "name " : "abc",
    "Email : "[email protected]",
    "role" :  " admin"
}

But you don't have password property in the user model.

@HrithikMittal password property is stored with another model called user-credential which has has one relationship with user model

@madaky

  1. Where you have defined your roles.
  2. Where at the time of user creation you have bind the roles with user.
  3. Are you using some sort of permissions model.

Answers

  1. role is defined as property in user model same as shopping example.
  2. At the time of creating user role is specified like above
  3. No permission model

@pratikjaiswal15 : Can you clean up code. If possible rebuild the code from starting. and you will get it resolved.

Finally, I created my own token service. And it resolved the issue.

Yes @pratikjaiswal15 you are right you are converting into profile object where you add everything like this

const profile = {
      [securityId]: user.id!.toString(),
      name: user.name,
      id: user.id,
      email: user.email,
      role: user.role,
      address: user.address
    }

after this, you are sending this to generate token right?
and for this, you are using the predefined method of @loopback/authentication-jwt which will take only these parameters to create a token

 const userInfoForToken = {
      id: userProfile[securityId],
      name: userProfile.name,
      email: userProfile.email,
    };

https://github.com/strongloop/loopback-next/blob/62231bf44b57c37d450b49f8e546b7066a653b80/examples/access-control-migration/src/components/jwt-authentication/services/jwt.service.ts#L54

But you need role also in your token for that you can add it by your own.

" for that you can add it by your own. " -- Could you please give guidance on how to do this?

@ianjenkinsii You can have your own jwt.service.ts and bind it like this.
this.bind(TokenServiceBindings.TOKEN_SERVICE).toClass( JWTService, );

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zero-bugs picture zero-bugs  路  3Comments

mightytyphoon picture mightytyphoon  路  3Comments

cloudwheels picture cloudwheels  路  3Comments

aceraizel picture aceraizel  路  3Comments

rexliu0715 picture rexliu0715  路  3Comments