DEV Community

Cover image for Our System Series: JWT Tokens. Identity, Context, and Permissions
Ilya Mikhasik
Ilya Mikhasik

Posted on

Our System Series: JWT Tokens. Identity, Context, and Permissions

Previous: Event Processing

Every request in a microservice system needs context. A service needs to know who is making the request, which project the request belongs to, which account is being used, and what the caller is allowed to do.

In our system, this context is carried by JWT access tokens.

A token is created after a successful login and then passed to the services involved in the request. It contains the user identity together with the project, account, organization, group, and permission context required for authorization.

Login is more than password verification

The User Profiles Service exposes two endpoints for obtaining an access token:

POST /auth/login
POST /auth/token

Both endpoints authenticate a user with a username and password, require a Project-ID header, and can receive an Account-ID header.

POST /auth/login returns a full login response. It includes the access token together with selected user data, abbreviated profile data, organization information, permissions, and groups.

POST /auth/token returns only the access token and token type. It is intended for callers that already have the user context they need and only require a new Bearer token.

The authentication process begins by locating the active user by login and verifying the password. Before password verification, the service checks account flags that may require the user to restore or change their password.

But a successful password check is only the beginning.

The service must also determine where the user is acting in the system. The same user may have access to different projects, accounts, organizations, groups, and operations. A useful access token therefore needs to carry more than a user ID.

What the token contains

The access token is a signed JWT containing a sub object. This object represents the security context created during login.

The basic identity and context fields are:
user_id
profile_id
project_id
account_id
device_id
user_ip

Depending on the user’s relationships and assigned access, the token also contains:
permissions
groups
organizations

A simplified token payload looks like this:
{
"sub": {
"user_id": "898ccff4-ba19-4a52-a3e3-4b631047e6a9",
"profile_id": "898ccff4-ba19-4a52-a3e3-4b631047e6a9",
"project_id": "2c03471b-7792-4f9a-aa8a-6811810959f0",
"account_id": "9fe20687-4036-48ba-951d-458092012288",
"permissions": {
"Quest": [
"get_quests",
"post_quest"
]
},
"groups": [
"organization"
],
"organizations": [
"f607b439-d88c-41c8-8581-5655f71722cf"
]
},
"iat": 1790254220,
"exp": 1790297420
}

The API response also returns selected user and profile details. For example, the caller receives user information, account flags, an abbreviated profile, and the Bearer token itself.

The token is what subsequent services use to identify the caller and evaluate access.

Building the context

The token’s project and account information is not simply copied from request headers. The User Profiles Service resolves the user’s relationships before placing that information into the token.

First, the service verifies that the user is connected to the current project through a project-user link.

It then looks for account-user links within that project. If there is one matching account, the service uses it. If several accounts are available, the caller specifies the intended account through the Account-ID header.

The service then verifies that the selected account is connected to the project through a project-account link. Finally, it retrieves the organizations to which the user belongs within that project and account.

User
→ project-user link
→ account-user link
→ project-account link
→ organization memberships
→ token context

This is where the graph-like model becomes part of authorization. The links that connect users, projects, accounts, and organizations are used to construct the context in which a user can act.

Permissions and groups

Permissions are retrieved from the permissions service and added to the JWT under the relevant service name.

For example:
{
"permissions": {
"FletcherGoods": [
"post_goods",
"patch_goods_goods_id",
"delete_goods_goods_id",
"patch_goods_goods_id_status"
],
"Quest": [
"get_quests",
"patch_quest_quest_id",
"post_task"
]
}
}

The structure provides a direct mapping between a service and the operations permitted for that user.

Groups provide another part of the authorization context. A user may belong to one or more groups within a project, and those groups are included in the token alongside individual permissions.

The /auth/login endpoint can also receive a requested permission set. If it does, the service filters that request against the user’s actual permissions. The resulting token contains only permissions already assigned to that user.

Signing tokens

Tokens use RS256, an RSA signature algorithm based on SHA-256. The private key signs tokens, while services use the corresponding public key to verify them.

Toknes also contain two time-based claims:
iat — issued at
exp — expiration time

These record when a token was created and when it stops being accepted.

After a token has been issued, protected endpoints use FastAPI’s OAuth2PasswordBearer dependency to extract the Bearer token from the request. The service verifies it with the public key, reads the user ID from sub, and loads the current user record from the registry.

Invalid, expired, malformed, or userless tokens become application-specific authorization errors.

Different lifetimes for different flows

The normal access-token lifetime is controlled through configuration. The service also supports two scope-based lifetime modes:
short:
infinite:

Short tokens default to five minutes if no explicit duration is supplied. They are used for focused actions such as password restoration.

For example, a password-restore token contains a reduced security context:
{
"user_id": "<user UUID>",
"project_id": "<project UUID>",
"permissions": {
"UserProfiles": [
"patch_auth_password_trusted"
]
}
}

The trusted password-change endpoint validates the token’s remaining lifetime before allowing the password update.

This gives the password-recovery flow a token with a specific purpose and a short usable period, rather than requiring the full set of permissions carried by a normal access token.

Closing perspective

JWTs connect authentication to the rest of the system.

A password verifies the user’s identity at login. The user’s links to projects, accounts, and organizations establish the context in which that identity is operating. Groups and permissions determine which operations are allowed. The resulting JWT carries that context to the services handling later requests.

In the next article, I will describe Cerberus, the permissions service. I will look more closely at how authentication, authorization, and permission checks are applied by services after a token has been issued.

Top comments (0)