Skip to main content

Translate legacy tokens to SlashID tokens

Context

Migrating to a new identity provider doesn't need to be hard. In this guide, we describe how to perform a migration simply and incrementally with no downtime. Our approach further allows to start a migration without the need for modification of your existing codebase.

This guide assumes that your system supports authentication based on request header or cookies and requires a Gate deployment in your infrastructure.

Coupled with Translate SlashID to existing tokens it helps you migrate your backend one service or to only migrate a subset of backend services without affecting the rest of your identity logic.

Solution

For this migration approach, you can use Gate's Token upgrade plugin.

UserYour systemLoad balancerToken mapping endpointDestination endpointGateSlashID APIToken from configured headerPerson handlesorSlashID person IDMint tokenMinted JWT tokenHTTP requestwith legacy authenticationHTTP requestHTTP request withminted JWT token

Most of the functionality is provided by Gate out of the box. You need only to implement a token mapping endpoint.

info

This migration guide is designed for zero-downtime migration. If you are fine with temporary downtime or synchronization of deployments, you can skip the 3 and 4 steps.

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

Gate needs to know how to map the token received to a SlashID token. To do that, you need to implement a token mapping endpoint. Gate POST to the endpoint requests with the following format:

{
"token": "Token from the original request header"
}

Your token mapping endpoint should return the SlashID person ID or person handles corresponding to the input token.

Optionally, you can also return a list of custom claims that should be added to the SlashID token minted through Gate. Certain claims are reserved, you can find a detailed list of reserved claims and the format to specify custom claims in the mint token API docs. Custom claims are optional.

The examples below are valid responses sent from a token mapping endpoint.

Response with user handles

{
"handles": [
{
"type": "email_address",
"value": "user@user.com"
}
],
"custom_claims": {
"custom_attribute_1": 42,
"custom_attribute_2": {
"sub_attr": 24
}
}
}

All valid handles types can be found in the API documentation.

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 start to add a new header to requests 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 temporarily store the SlashID token in the TemporaryAuthorization header to keep backward compatibility. When all your services have migrated to SlashID, we can change the TemporaryAuthorization to Authorization and get rid of the legacy 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=TemporaryAuthorization

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

info

To reload configuration without downtime, please run gate reload.

Step 4 - Update endpoint or service to use the new header

Now that the SlashID token is propagated through the TemporaryAuthorization header, you can modify the service or endpoint you wish to migrate to SlashID to read the user token from TemporaryAuthorization instead of Authorization.

We recommend preparing your service or endpoint for the sunsetting of TemporaryAuthorization header once the migration is completed. You can do it in two ways:

  • introduce a configuration parameter that defines where to look for the JWT token.
  • modify your service to accept both legacy and SlashID tokens in the Authorization token.

After deploying that change, you can move on to the next step.

Step 5 - Override the old header

Once you have tested the proper functioning of each service with the SlashID token, we can override the old Authorization token. In this phase we still keep the TemporaryAuthorization header, so you can execute step 6 without downtime.

caution

All previous steps were backward compatible. After executing that step old header will be overridden. Please ensure that your service does work properly after implementing that step.

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,TemporaryAuthorization

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

Step 6 - Complete the migration

Depending on which way you decide to go in step 4, you should now switch your service or endpoint to use the Authorization header remove the temporary header.

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 plugin execution order.

Example

Configuring Gate

This is an example configuration for Gate such that the */api/generic endpoint receives a translated SlashID token instead of the original legacy one.

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,
)

Conclusion

Your services have gracefully switched over from your legacy IdP tokens to SlashID tokens without any downtime or any significant risk to your infrastructure.