Passive users migration
Context
Migrating a big user base is a non-trivial task especially without causing downtime. Gate enables you to migrate your users passively without invalidating their sessions or resetting their credentials.
Solution
Gate can inspect HTTP requests and every time an unknown identity token is sent, Gate can create a corresponding user in SlashID. After a user is created in SlashID, Gate adds custom headers containing the SlashID person ID to the request and forwards it to your backend.
To execute the migration, you can use the Users mirroring plugin.
Most of the functionality is provided by Gate out of the box. You only need to implement a token mapping endpoint.
Example
Configuring Gate
This is an example configuration for Gate such that when a user navigates to */api/generic
, Gate generates
a new user in SlashID.
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",
street_address="1234 Main Street, Apartment 101, Manvel 77578, Texas, USA",
password_hash=pbkdf2_sha256.hash(username),
user_roles=["user"],
)
if username == f"admin@{vendor_domain}":
return DatabaseUser(
username=username,
name=f"Admin {vendor_name} User",
street_address="666 Greenwich Street, New York 10009, New York, USA",
password_hash=pbkdf2_sha256.hash(username),
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,
)
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.