Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional properties in JWT token not accessible #9914

Open
apfz opened this issue Sep 1, 2023 · 3 comments
Open

Additional properties in JWT token not accessible #9914

apfz opened this issue Sep 1, 2023 · 3 comments
Labels

Comments

@apfz
Copy link

apfz commented Sep 1, 2023

Describe the bug

I am trying to access my User model's "roles" properties. I am using the @loopback/authentication-jwt package for JWT authentication.

I have tried to bind a custom JWTService with application.ts as follows:

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

The custom JWT Service is as follows:

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 '@loopback/authentication-jwt';

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 fields 'iat' and 'exp', nor 'email' to the user profile
      userProfile = Object.assign(
        {[securityId]: '', name: ''},
        {
          [securityId]: decodedToken.id,
          name: decodedToken.name,
          id: decodedToken.id,
          roles: decodedToken.roles,
        },
      );
    } 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,
      roles: userProfile.roles,
    };
    // 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;
  }
}

Now I decorated an endpoint in the following way:

@post('/faqs', {
    security: OPERATION_SECURITY_SPEC,
    responses: {
      '200': {
        description: 'Faq model instance',
        content: {'application/json': {schema: getModelSchemaRef(Faq)}},
      },
    },
  })
  @authenticate('jwt')
  @authorize({
    allowedRoles: ['faqs'],
    voters: [basicAuthorization],
  })

But the JWT service never seems to get triggered.

The basicAuthorization of my decorator is as follows:

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> {
  // No access if authorization details are missing
  let currentUser: UserProfile;
 
  if (authorizationCtx.principals.length > 0) {
    const user = _.pick(authorizationCtx.principals[0], [
      'id',
      'name',
      'roles',
    ]);
    currentUser = {[securityId]: user.id, name: user.name, roles: user.roles};
  } 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;
}

However, roles is undefined here.

How can I solve this?

Logs

No response

Additional information

No response

Reproduction

@apfz apfz added the bug label Sep 1, 2023
@johndpope
Copy link

@apfz
Copy link
Author

apfz commented Oct 24, 2023

@johndpope Thanks. The lbx-jwt repository is really helpful. I have replaced my code with the repository, as I couldn't quite figure out what went wrong with my implementation.

@happyjoe1978
Copy link

Perhaps it was an import package error.
application.ts

import {JWTService} from './services/jwt.service';

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants