loopbackio / loopbackio/loopback-next
Additional properties in JWT token not accessible
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 5.1k
- Forks
- 1.1k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 27
Description
### 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 {
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 {
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 {
// 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
-
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the application.ts TOKEN_SERVICE binding, then trace the @authenticate('jwt') and @authorize flow for the /faqs endpoint to see how the verified profile becomes authorizationCtx.principals. Reproduce the request with a token containing roles and determine where that property is lost; the issue is done when the authorization context exposes the expected roles or the supported limitation is clearly established.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, authentication, authorization
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 28/100