Skip to main content

Token enrichment: Add custom claims

Context

Often you want to propagate tokens with custom claims coming from internal databases or service where data about the identity is stored.

Gate helps adding custom claims to a token and minting a new one accordingly.

Solution

You can enrich and mint new tokens with custom claim using SlashID the mint token API docs.

As a prerequisite, you should have Gate deployed in your infrastructure.

Step 1 - Run Gate in transparent mode.

You should also have Gate deployed in your infrastructure. You should run Gate in transparent mode to ensure that everything was correctly deployed.

Step 2 - Implement a token mapping endpoint

Response with person ID

{
"person_id": "e62bd107-2563-4e36-bfb9-954fffba6070",
"custom_claims": {
"custom_attribute_1": 42,
"custom_attribute_2": {
"sub_attr": "value"
}
}
}

SlashID looks up the handle or person ID and generates a JWT token for that user with the custom_claims in the request.

Step 3 - Enable the token-translation-upgrade plugin

Now you can replace the tokens that are going through Gate. Let's assume that your legacy headers are sent in the Authorization header.

note

If your users are authorized based on cookies, you should use the Cookie instead of the Authorization header.

We will replace the existing token with the newly minted token from SlashID and stiry it in the Authorization header.

The entire plugin configuration reference can be found on the Token upgrade plugin page.

GATE_PLUGINS_<PLUGIN NUMBER>_NAME=token-translation-upgrade
GATE_PLUGINS_<PLUGIN NUMBER>_CONFIGURATION_HEADER_WITH_TOKEN=Authorization
GATE_PLUGINS_<PLUGIN NUMBER>_CONFIGURATION_MAP_TOKEN_ENDPOINT=<Map token endpoint>
GATE_PLUGINS_<PLUGIN NUMBER>_CONFIGURATION_SLASHID_ORG_ID=<SlashID Org ID>
GATE_PLUGINS_<PLUGIN NUMBER>_CONFIGURATION_SLASHID_API_KEY=<SlashID API key>
GATE_PLUGINS_<PLUGIN NUMBER>_CONFIGURATION_TARGET_HEADERS=Authorization

In the Environment variables configuration, <PLUGIN NUMBER> defined the plugin execution order.

Example

Configuring Gate

This is an example configuration for Gate such that the */api/generic endpoint receives an enriched SlashID token with custom claims.

Note how the translator plugin invokes the webhook at http://backend:8000/map_token to perform the translation.

slashid_config: &slashid_config
slashid_org_id: { { .env.SLASHID_ORG_ID } }
slashid_api_key: { { .env.SLASHID_API_KEY } }
slashid_base_url: { { .env.SLASHID_BASE_URL } }

gate:
port: 8080
log:
format: text
level: trace

default:
target: http://backend:8000

plugins:
- id: translator_up
type: token-translation-upgrade
enable_http_caching: true
enabled: false
parameters:
<<: *slashid_config
header_with_token: Authorization
map_token_endpoint: http://backend:8000/map_token

urls:
# The /api/generic endpoint names the translation upgrade plugin and
# the endpoint will receive a SlashID token
- pattern: "*/api/generic"
target: http://backend:8000
plugins:
translator_up:
enabled: true

An example token mapping endpoint

For this example, let's assume the incoming legacy token is of the form:

{
"typ": "internal_token_format_1",
"username": "user@example.com",
"name": "Regular YourBrand User"
}

The map_token function takes in the legacy token, extracts the user by looking up the username in the legacy token and extracts the user from it (get_user). The webhook returns a json object of the form:

{
"handles": {
"email_address": "user@example.com"
},

"custom_claims": {
"foo": [1, 2, 3],
"orig_token": ""
}
}

In the background, SlashID checks whether a user with that handle exists if so it mints a token for that user including the custom claims passed in. Otherwise it first creates a new user with SlashID and then returns the token.

def get_user(username: Optional[str]) -> Optional[DatabaseUser]:
try:
if username == f"user@{vendor_domain}":
return DatabaseUser(
username=username,
name=f"Regular {vendor_name} User",
user_roles=["user"],
)
if username == f"admin@{vendor_domain}":
return DatabaseUser(
username=username,
name=f"Admin {vendor_name} User",
user_roles=["user", "admin"],
)
except:
pass
return None

async def map_token(request: MapTokenRequest) -> MapTokenResponse | Response:
logger.info(f"/map_token: request={request}")

req_token = request.token
if not req_token.lower().startswith("bearer "):
return Response(status_code=status.HTTP_204_NO_CONTENT)

req_token = req_token[len("bearer ") :]
logger.info(f"/map_token: req_token={req_token}")
try:
token = jwt.decode(req_token, TOKEN_SIGNING_KEY, algorithms=["HS256"])
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Bearer token {req_token} is invalid: {e}",
)

username: Optional[str] = token.get("username")
if username is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Bearer token {req_token} doesn't contain username",
)

user = get_user(username)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"User {user} is not mapped to /id person",
)

custom_claims= vars(user)
custom_claims["foo"] = [1, 2, 3]
custom_claims["orig_token"] = req_token

return MapTokenResponse(
person_id=None,
handles=[Handle(type="email_address", value=username)],
custom_claims=custom_claims,
)

Your services will now receive a token that looks like the following. Notice how the foo and orig_token claims are added to the token:

{
"aud": "c921477d-27f2-6bc2-4e3f-ab91a1c65f73",
"authenticated_methods": ["api"],
"exp": 1681720646,
"first_token": false,
"foo": [1, 2, 3],
"groups": [],
"groups_claim_name": "groups",
"iat": 1681634246,
"iss": "https://api.sandbox.slashid.com",
"jti": "2a4dbca88332fd40ac0dd717de6aa760",
"oid": "c921477d-27f2-6bc2-4e3f-ab91a1c65f73",
"orig_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJpbnRlcm5hbF90b2tlbl9mb3JtYXRfMSIsInVzZXJuYW1lIjoidXNlckBleGFtcGxlLmNvbSIsIm5hbWUiOiJSZWd1bGFyIFlvdXJCcmFuZCBVc2VyIn0.Tqw1LWvWNwxx8dA6KH_cETeBQq2h11K98UX4MshgBQM",
"person_id": "06414801-354e-7a95-ba08-062f71b3c9de",
"sub": "06414801-354e-7a95-ba08-062f71b3c9de",
"user_roles": ["user"]
}

Conclusion

Your services can now process JWT tokens with the custom claims you defined. Futher, you can emit different tokens with different custom claims for different services to avoid overly large tokens or disclosing sensitive information to services that don't need to have access to it.