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.
Most of the functionality is provided by Gate out of the box. You need only to implement a token mapping endpoint.
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.
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.
- 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=TemporaryAuthorization
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 = "TemporaryAuthorization"
}
}
// ...
]
}
{
"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": "TemporaryAuthorization"
}
}
// ...
]
}
}
[[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 = "TemporaryAuthorization"
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: TemporaryAuthorization
// ...
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.
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.
- 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,TemporaryAuthorization
In the Environment variables configuration, <PLUGIN NUMBER>
defined 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,TemporaryAuthorization"
}
}
// ...
]
}
{
"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,TemporaryAuthorization"
}
}
// ...
]
}
}
[[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,TemporaryAuthorization"
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,TemporaryAuthorization
// ...
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.
- 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 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
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.
- 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 === "user@example.com") {
return {
username: username,
name: "Regular User",
user_roles: ["user"],
};
}
if (username === "admin@example.com") {
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,
};
}
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.