Use IAM token to check permission
Introduction
When a client calls a service, the service needs to ensure that the client has the permission to do the action that the client is requesting. The permission is based on the client's role(s) and the resource that the client is trying to access.
Flow
- A client does a request to your service using an IAM token
- The service verifies the token to ensure that token is valid
- The service reads the
ars
attribute in the token to retrieve the calling client's role(s) and to what resource the role is connected to - The service translates the role to permissions that the service understands by calling to for the roles setup in Akkess
- Based on the permissions, microservice either accept or reject the action requested by the client
Example
- A vehicle service has two endpoints one where a user can read the vehicle information and one where the user can write the vehicle information.
- To be able to write the vehicle information the user has to be the owner of the vehicle.
- To be able to read the vehicle information the user has to be the owner or a driver of the vehicle.
Setup
A service has been created in the IAM console with the following configuration:
- Service key: vehicle-service
- Available permissions: READ, WRITE
Roles have been created in the IAM console with the following configurations:
- DRIVER - can READ
- OWNER - can READ and WRITE
Service setup
Roles setup
Pseudocode example using the Akkess Typescript SDK
The example uses Akkess Typescript SDK and the RoleSetupV1
is mocked to show how a data structure could look
like.
Please check OpenAPI spec for Policy API for details.
const validatePermissionExampleFlow = async () => {
/* The callers IAM token in JWT format the 'iamTokenStrMock' body is:
{
"jti": "5bea691b-9266-4fac-ab5d-42ef36df0bf1",
"iss": "https://api.akkess.io/authorization/v1",
"sub": "664e2be7be413d09058b438e",
"aud": "tenant-66473eee3c495433c03ab606",
"exp": 2228983638,
"iat": 1728980038,
"acc": "account-63c65dee83abd41be9f61104",
"app": "app-66473eb62c62d463f262b982",
"tid": "tenant-66473eee3c495433c03ab606",
"ars": [
{
"r": ["DRIVER"],
"c": ["VIN=VIN-1234567890"]
}
]
}
*/
const iamTokenStrMock =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImM0YjMwM2FmOThkZGU2OWQxNWJmMDlhZGU3OWM2NDA0IiwiY3R5IjoidDEifQ.eyJqdGkiOiI1YmVhNjkxYi05MjY2LTRmYWMtYWI1ZC00MmVmMzZkZjBiZjEiLCJpc3MiOiJodHRwczovL2FwaS5ha2tlc3MuaW8vYXV0aG9yaXphdGlvbi92MSIsInN1YiI6IjY2NGUyYmU3YmU0MTNkMDkwNThiNDM4ZSIsImF1ZCI6InRlbmFudC02NjQ3M2VlZTNjNDk1NDMzYzAzYWI2MDYiLCJleHAiOjIyMjg5ODM2MzgsImlhdCI6MTcyODk4MDAzOCwiYWNjIjoiYWNjb3VudC02M2M2NWRlZTgzYWJkNDFiZTlmNjExMDQiLCJhcHAiOiJhcHAtNjY0NzNlYjYyYzYyZDQ2M2YyNjJiOTgyIiwidGlkIjoidGVuYW50LTY2NDczZWVlM2M0OTU0MzNjMDNhYjYwNiIsImFycyI6W3siciI6WyJEUklWRVIiXSwiYyI6WyJWSU49VklOLTEyMzQ1Njc4OTAiXX1dfQ.e6TsoqbvLtC8aNChCHoHWs7qSt4quckGDQgtevMuQk_8qNI9LqXMHDuMiVfUBYgzuJa4U9h0GOC5Od1Otj6aAEIpWyz4_Jptq7qTTklZio9o10NmkbmSpQDfrGCyLxdtWH6UzORUDlcX36Zl-B_utUqklk-jnb5dt0J8g1yu2v2Ck5odExMKLpYdPchrAjyD_o09OtBbAavO__L6dgmHwbx2bsD4CSxY9Xh-jyCvJlDyzukDdUYYpGBRKr19C2ooFIRN8JmAQNieWiTAx9FOEqPMQqdoVZ7wYGxb16xpE4YO9G6AiJxJ4SgiNAN536144L06N9klykL2Ln64IQUxrZxoGRuSx-bXwvMRd-RG8nRjt3kXlY3g9N4dBE0JnkMsmpUPqdKMFGu483fWA_ygW-j1EwIHqt2qQgeWCtlRxZTw-dr7eX0NFf1DaegKpgjj9h7DwRTyp68JUAQShA6wjauiGDKu1j29AjWfYJiVD1cyEvf4W2u9kSiH7Tdp0hTM';
// The VIN to check permission for
const vin = 'VIN-1234567890';
// The service identifier key
const serviceKey = 'vehicle-service';
// READ permission identifier key
const readPermissionKey = 'READ';
// Mock the PolicyRestServiceV1.getAvailableRoles to return the role policy for the application
PolicyRestServiceV1.getAvailableRoles = jest.fn().mockResolvedValue({
applicationId: '63c65dee83abd41be9f61106', // The application ID
changeId: '7425944733333061643', // The change ID - the change ID can be used to see if cached data is outdated
roles: [
{
// Role DRIVER has permission to READ in vehicle-service
key: 'DRIVER',
servicePolicies: [
{
serviceDefinitionSource: 'APPLICATION',
serviceDefinitionKey: 'vehicle-service',
permissions: [
{
key: 'READ',
},
],
},
],
},
{
// Role OWNER has permission to READ and WRITE in vehicle-service
key: 'OWNER',
servicePolicies: [
{
serviceDefinitionSource: 'APPLICATION',
serviceDefinitionKey: 'vehicle-service',
permissions: [
{
key: 'READ',
},
{
key: 'WRITE',
},
],
},
],
},
],
});
// Flow starts here
// Someone calls end-point to read the vehicle information for given vin. GET /vehicle/{vin} in this case GET /vehicle/VIN-1234567890
// The caller has an IAM token in JWT format in the Authorization request header
// 1. We need to ensure that IAM is token is valid - the token is located in the Authorization header prefixed with 'BEARER '
// 2. Ensure that the token has correct signature, issuer and not been expired
// 3. In the token body the caller's access refs are located - check IamJwtPayloadT1.ts. The access refs tell which role the user has on which resources.
// In this case the user is directly connected to the vehicle identifier number (VIN) for the vehicle that the user
// has access to.
// 4. Find the roles that are connected to the VIN supplied in the request GET /vehicle/{vin}
// 5. Check if the roles has the permission to read the vehicle information for the service 'vehicle-service'.
// This information can be found in the roles policy for the application fetched from Akkess API. The roles policy is
// configured in the IAM console for the application.
// 6. If the user has the permission to read the vehicle information return the information otherwise return 403 Forbidden
// Parse the body of the IAM token (JWT)
const iamToken = IamToken.fromToken(iamTokenStrMock); // Check implementation of IamToken.fromToken for details
const accessRefs = iamToken.getBody < IamJwtPayloadT1 > ().ars || []; // The access references from the token
// Use the callers token to fetch the roles policy for the application that is pointed out by the token
// Due to that the policy is rarely updated it can be cached with a sensible cache time.
// Use roleSetupV1.changeId to see if the cached data is outdated
const roleSetupV1 = await PolicyRestServiceV1.getAvailableRoles(ApiUtil.iam({ token: iamTokenStrMock }));
// Use the access references to find the roles connected to the VIN and if the roles has the permission to read the vehicle information
// Custom resource reference is formatted with 'type'='value' - in this case "VIN=VIN-1234567890"
const resourceRef = 'VIN=' + vin;
const rolesConnectedToVin = accessRefs.filter(ar => ar.c?.includes(resourceRef)).flatMap(ar => ar.r);
const canRead =
roleSetupV1.roles
// Filter roles based the roles that user has connected to the VIN
?.filter(role => rolesConnectedToVin.includes(role.key))
// Flatmap to get all service policies
?.flatMap(role => role.servicePolicies || [])
// Filter service policies based on the service definition key. There could be other services configured in the same policy file
.filter(servicePolicy => servicePolicy.serviceDefinitionKey === serviceKey)
// Check if the role is configured with the 'vehicle-service' READ permission
.some(servicePolicy => servicePolicy.permissions.some(permission => permission.key === readPermissionKey)) ||
// If no service policy is found, return false
false;
if (canRead) {
return 'Vehicle information';
} else {
throw new Error('Forbidden');
}
};