Mapping external IdP claims to local claims #3157
Replies: 7 comments 14 replies
|
Strong +1, Thunder needs this, and putting the mapping on the IdP (the trust relationship) instead of scattering mappers across apps/scopes is the right call. Sharing one mapping across token exchange and federated login keeps both paths from drifting. Great initiative. Three things I'd want to nail down: 1. Don't make pass-through the silent default. "Unmapped claims pass through, mapped wins" is convenient but a footgun, an external claim whose name happens to collide with a local one silently becomes the local value with no mapping ever declared. I'd lean toward requiring an explicit mapping for every claim we forward (allowlist posture); drop or namespace the rest. Pass-through can stay as an opt-in dev convenience. 2. Define precedence, not just renaming. "Mapped value wins" only covers two external claims hitting the same target. The harder collision is mapped external value vs. the attribute already on the local user, which is live for us since we have JIT provisioning + account linking ( 3. Shape transforms, not just names. IdPs differ in value shape, roles as space-delimited string vs. array, On the open question, I'd skip the global/default mapping; per-IdP is the right granularity, and a default+override layer just creates precedence confusion. |
|
|
Hi @sadilchamishka , How are we planning to represent the internally used claims? We may also need mapping for user stores, etc. too in the future, so better to make it as a reusable transformation layer as you suggested. And have we considered whether mapping from internal representation -> external formats is needed too with this effort? e.g. mapping our internal claims to OIDC/SCIM dialects, etc. Here is a related issue created sometime back #557 |
Refined model
Model{
"name": "Asgardeo",
"type": "OIDC",
"properties": [
{
"name": "issuer",
"value": "https://api.asgardeo.io/t/sadiltestchoreo/oauth2/token"
},
{
"name": "jwks_endpoint",
"value": "https://api.asgardeo.io/t/sadiltestchoreo/oauth2/jwks"
},
{
"name": "token_exchange_enabled",
"value": "true"
},
{
"name": "client_id",
"value": "YN5R_Sw1rCX9spEHeNfz4SPPM6wa",
"isSecret": false
}
],
"attributeConfiguration": {
"userTypeResolution": {
"externalAttribute": "user_type",
"valueMapping": {
"employee": "Employee",
"customer": "Person"
},
"default": "Person"
},
"transforms": [
{
"externalAttribute": "given_name",
"transform": {
"type": "toUpperCase"
}
},
{
"externalAttribute": "org_name",
"valueMapping": {
"Acme Corp": "ACME CORPORATION"
}
}
],
"sync": {
"default": "override",
"attributes": [
{
"externalAttribute": "given_name",
"policy": "preserve-local"
}
]
},
"accountLinking": {
"attributes": [
"email"
]
},
"userTypeAttributeMappings": [
{
"userType": "Person",
"attributes": [
{
"externalAttribute": "given_name",
"localAttribute": "firstName"
},
{
"externalAttribute": "address.email",
"localAttribute": "email"
},
{
"externalAttribute": "org_name",
"localAttribute": "orgName"
}
]
}
]
}
}Structure
Processing pipeline
Design principleEach concern lives where it actually varies:
PhasingPhase 1 (shipped) is |
Why do we need to prevent mapping these claims? Well we can keep some claims such as iss, exp, nbf, etc reserved. But some claims we might still need to allow mappings. Ex: sub to userId |
Can't we take this also to the same effort? It's just special casing userid mapping right? Maybe on a seperate thought, rather than special casing it, we may able to map it to the userid claim as well in the claim mappings. Is there any other use case to have subject claim separately? |
Implementation update — what shipped vs. the refined modelThe implementation for this effort landed in #3127. A couple of things changed from the refined model above, based on review feedback: 1. "user type" → "entity type" in the model. "attributeConfiguration": {
"entityTypeResolution": { "default": "Person" },
"entityTypeAttributeMappings": [
{
"entityType": "Person",
"attributes": [
{ "externalAttribute": "given_name", "localAttribute": "firstName" },
{ "externalAttribute": "address.email", "localAttribute": "email" }
]
}
]
}2. Mapping moved to the authn service layer (not the flow executor). 3. Entity-type resolution no longer drives JIT provisioning. What's intentionally deferred (kept as additive roadmap, not implemented): Re: reserved claims / subject mapping (@ThaminduDilshan's points) — the reserved set still blocks mapping |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
Propose a single, IdP-level claim mapping mechanism that translates an external identity provider's claims into ThunderID's local claims. The same mapping is reused by both paths that bring external identities into ThunderID: RFC 8693 token exchange and federated login.
Problem
External IdPs emit claims under their own names and shapes (e.g.
given_name,org_name, URI-style names, or structured/nested objects). ThunderID and its applications expect local claims. Today external claims flow through verbatim, so issued tokens carry the right claims only if the external names happen to match. We need a way to normalize external claims to local claims.Proposed model
Attach the mapping to the identity provider as configuration — a mapping from the IdP's external claims to ThunderID's local claims. It is part of the trust relationship with that IdP, defined once and reused everywhere ThunderID consumes that IdP's identity.
The mapping is represented as a native JSON structure on the IdP (the configuration store is already
JSONB), and supports both flat and complex/nested claims — i.e. selecting a nested field out of a structured external claim, or mapping into a nested local attribute — not only flat top-level name-to-name.Where it applies
Two entry points share the same mapping:
Key design decisions
sub,iss,scope, …) cannot be mapping targets, so a mapping can't alter token identity or authorization semantics.Open questions
All reactions