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.
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.
- Environment variables
- HCL
- JSON
- TOML
- YAML
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.
gate = {
plugins = [
// ...
{
name = "token-translation-upgrade"
configuration = {
header_with_token = "Authorization"
map_token_endpoint = "<Map token endpoint>"
slashid_org_id = "<SlashID Org ID>"
slashid_api_key = "<SlashID API key>"
target_headers = "Authorization"
}
}
// ...
]
}
{
"gate": {
"plugins": [
// ...
{
"name": "token-translation-upgrade",
"configuration": {
"header_with_token": "Authorization",
"map_token_endpoint": "<Map token endpoint>",
"slashid_org_id": "<SlashID Org ID>",
"slashid_api_key": "<SlashID API key>",
"target_headers": "Authorization"
}
}
// ...
]
}
}
[[gate.plugins]]
name = "token-translation-upgrade"
configuration.header_with_token = "Authorization"
configuration.map_token_endpoint = "<Map token endpoint>"
configuration.slashid_org_id = "<SlashID Org ID>"
configuration.slashid_api_key = "<SlashID API key>"
configuration.target_headers = "Authorization"
gate:
plugins:
// ...
- name: token-translation-upgrade
configuration:
header_with_token: Authorization
map_token_endpoint: <Map token endpoint>
slashid_org_id: <SlashID Org ID>
slashid_api_key: <SlashID API key>
target_headers: Authorization
// ...
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": "[email protected]",
"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": "[email protected]"
},
"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.
- Python
- TypeScript
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 SlashID 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,
)
import * as pbkdf2 from "pbkdf2";
import * as jwt from "jsonwebtoken";
interface DatabaseUser {
username: string;
name: string;
user_roles: string[];
}
interface MapTokenRequest {
token: string;
}
interface Handle {
type: string;
value: string;
}
interface MapTokenResponse {
person_id: null;
handles: Handle[];
custom_claims: Record<string, any>;
}
function getUser(username: string | null): DatabaseUser | null {
try {
if (username === "[email protected]") {
return {
username: username,
name: "Regular User",
user_roles: ["user"],
};
}
if (username === "[email protected]") {
return {
username: username,
name: "Admin User",
user_roles: ["user", "admin"],
};
}
} catch (error) {
// Handle error here if needed
}
return null;
}
async function mapToken(
request: MapTokenRequest,
): Promise<MapTokenResponse | { statusCode: number }> {
let reqToken = request.token;
if (!reqToken.toLowerCase().startsWith("bearer ")) {
return { statusCode: 204 };
}
reqToken = reqToken.substring("bearer ".length);
let token: any;
try {
token = jwt.verify(reqToken, TOKEN_SIGNING_KEY, { algorithms: ["HS256"] });
} catch (e) {
throw {
statusCode: 401,
detail: "Bearer token is invalid",
};
}
const username = token.username;
if (username === undefined) {
throw {
statusCode: 401,
detail: "Bearer token doesn't contain username",
};
}
const user = getUser(username);
if (user === null) {
throw {
statusCode: 401,
detail: "User is not mapped to SlashID person",
};
}
const customClaims: Record<string, any> = { ...user };
customClaims.foo = [1, 2, 3];
customClaims.orig_token = reqToken;
return {
person_id: null,
handles: [{ type: "email_address", value: username }],
custom_claims: customClaims,
};
}
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.