1

I have successfully implemented jwt auth in my lb4 app using custom service which implements userservice from @loopback/authentication-jwt. Everything is working fine for authentication.

But when I go for authorization AuthorizationMetadata contains only two fields - id and name. While issuing token I have used many fields one of which was role.

But now all other fields like role or email are undefined. Thus whenever I try to access controller protected with authorize decorator I get 501 access denied.

I can't get why other properties are undefined.

Thank you in advance

custom-service

export class CustomUserService implements UserService<User, Credentials> {

// other code

      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
      }
}

login controller

import {authenticate, TokenService, UserService} from '@loopback/authentication';
export class UserController {

constructor(@inject(SecurityBindings.USER, {optional: true})
    public users: UserProfile,){}

//@get login
 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};

}

authorizer.ts

import {AuthorizationContext, AuthorizationDecision, AuthorizationMetadata} from '@loopback/authorization';

export async function basicAuthorization(
  authorizationCtx: AuthorizationContext,
  metadata: AuthorizationMetadata,
): Promise<AuthorizationDecision> {
  // No access if authorization details are missing
  let currentUser: UserProfile;
  if (authorizationCtx.principals.length > 0) {
    const user = _.pick(authorizationCtx.principals[0]
      , [
        'id',
        'name',
        'role',
        'email',
        'address'
      ]);

    console.log(user) // contains only id and name
  // other code
  }
}
pratik jaiswal
  • 1,855
  • 9
  • 27
  • 59

1 Answers1

4

The actual problem was while generating the token, property role wasn't included in TokenService from @loopback/authentication.

So I created custom-token service implementing this TokenService and added a property role while token-generation.

So later loopback-authentication sends this role to loopback-authorization. You can access it in AuthorizationContext.principals[0]

Here is the code

custom-toekn.service.ts

import {TokenService} from '@loopback/authentication';
import {inject} from '@loopback/context';
import {HttpErrors} from '@loopback/rest';
import {securityId, UserProfile} from '@loopback/security';
import {promisify} from 'util';
import {TokenServiceBindings} from '../keys';

const jwt = require('jsonwebtoken');
const signAsync = promisify(jwt.sign);
const verifyAsync = promisify(jwt.verify);

export class JWTService implements TokenService {
  constructor(
    @inject(TokenServiceBindings.TOKEN_SECRET)
    private jwtSecret: string,
    @inject(TokenServiceBindings.TOKEN_EXPIRES_IN)
    private jwtExpiresIn: string,
  ) {}

  async verifyToken(token: string): Promise<UserProfile> {
    if (!token) {
      throw new HttpErrors.Unauthorized(
        `Error verifying token : 'token' is null`,
      );
    }

    let userProfile: UserProfile;

    try {
      // decode user profile from token
      const decodedToken = await verifyAsync(token, this.jwtSecret);
      // don't copy over  token field 'iat' and 'exp', nor 'email' to user profile
      userProfile = Object.assign(
        {[securityId]: '', name: ''},
        {
          [securityId]: decodedToken.id,
          name: decodedToken.name,
          id: decodedToken.id,
          role: decodedToken.role,
        },
      );
    } catch (error) {
      throw new HttpErrors.Unauthorized(
        `Error verifying token : ${error.message}`,
      );
    }
    return userProfile;
  }

  async generateToken(userProfile: UserProfile): Promise<string> {
    if (!userProfile) {
      throw new HttpErrors.Unauthorized(
        'Error generating token : userProfile is null',
      );
    }
    const userInfoForToken = {
      id: userProfile[securityId],
      name: userProfile.name,
      role: userProfile.role,
    };
    // Generate a JSON Web Token
    let token: string;
    try {
      token = await signAsync(userInfoForToken, this.jwtSecret, {
        expiresIn: Number(this.jwtExpiresIn),
      });
    } catch (error) {
      throw new HttpErrors.Unauthorized(`Error encoding token : ${error}`);
    }

    return token;
  }
}

keys.ts

import {TokenService} from '@loopback/authentication';

export namespace TokenServiceConstants {
  export const TOKEN_SECRET_VALUE = 'myjwts3cr3t';
  export const TOKEN_EXPIRES_IN_VALUE = '600';
}



export namespace TokenServiceBindings {
  export const TOKEN_SECRET = BindingKey.create<string>(
    'authentication.jwt.secret',
  );
  export const TOKEN_EXPIRES_IN = BindingKey.create<string>(
    'authentication.jwt.expires.in.seconds',
  );
  export const TOKEN_SERVICE = BindingKey.create<TokenService>(
    'services.authentication.jwt.tokenservice',
  );
}

Then you have to bind this token-service in application.ts

application.ts

import {JWTService} from './services/token-service';
import {TokenServiceBindings, TokenServiceConstants} from './keys';

this.bind(TokenServiceBindings.TOKEN_SECRET).to(
      TokenServiceConstants.TOKEN_SECRET_VALUE,
    );

    this.bind(TokenServiceBindings.TOKEN_EXPIRES_IN).to(
      TokenServiceConstants.TOKEN_EXPIRES_IN_VALUE,
    );

    this.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(JWTService);

controller.ts

import {authenticate, TokenService, UserService} from '@loopback/authentication';
import {Credentials, OPERATION_SECURITY_SPEC, TokenServiceBindings, UserServiceBindings} from '@loopback/authentication-jwt';
import {authorize} from '@loopback/authorization';

export class UserController {
  constructor(
    @repository(UserRepository)
    public userRepository: UserRepository,

    @inject(TokenServiceBindings.TOKEN_SERVICE)
    public jwtService: TokenService,
    @inject(UserServiceBindings.USER_SERVICE)
    public userService: UserService<User, Credentials>,
    @inject(SecurityBindings.USER, {optional: true})
    public users: UserProfile,

  ) {}


@authenticate('jwt')
  @authorize({allowedRoles: ['admin'], voters: [basicAuthorization]})

aasync fund(){}

}
pratik jaiswal
  • 1,855
  • 9
  • 27
  • 59
  • Thanks this works basically i found where i did make a mistak `services.jwt.service` to `services.authentication.jwt.tokenservice` Under keys file. – Neeraj Tangariya Sep 23 '22 at 11:20