Concepts
SAML attribute mapping
An assertion tells you who signed in. What it does not tell you is what the fields are called — that was decided by whoever configured the identity provider, possibly years ago, possibly by somebody who has left. Mapping is the work of turning those names into the fields your application actually uses.
The same person, five different shapes
There is no agreed vocabulary for SAML attributes. One provider sends an attribute called mail; another sends a long claim URI ending in emailaddress; a third sends nothing but a name ID and expects you to treat it as an email address. All three are describing the same person, and all three are behaving correctly according to their own documentation.
This is not a protocol flaw so much as a consequence of history. SAML predates the idea that hundreds of applications would each need the same handful of fields, and the specification deliberately says nothing about what attributes should be called. What emerged instead is a set of conventions that overlap without agreeing.
The practical consequence is that a SAML integration written against one customer's provider breaks on the second customer. Teams that write their own SAML handling discover this on the day a large deal depends on it, which is a bad day to learn that firstName is sometimes givenName and sometimes a URI thirty characters long.
What the providers actually send
The table below covers the attribute names that account for most real-world connections. It is not exhaustive, and it is not a substitute for looking at what a specific customer sends — but it explains why a normalisation layer exists at all.
| Normalised field | Commonly sent as | Notes |
|---|---|---|
| mail, email, emailaddress, or the claims URI ending in /emailaddress | Sometimes only present as the name ID, with no attribute at all. | |
| first_name | givenName, firstName, first_name, or the claims URI ending in /givenname | Occasionally absent; some directories store only a display name. |
| last_name | sn, surname, lastName, or the claims URI ending in /surname | sn is the LDAP heritage showing through. |
| groups | groups, memberOf, Group, or the claims URI ending in /groups | May arrive as several single-valued attributes rather than one multi-valued one. |
| idp_id | The NameID, or an explicit objectGUID / oid / uid attribute | The value to key on. Never key on email. |
What you receive instead
Paycux resolves these names before your callback runs, so the profile you receive has the same shape whichever provider produced it. Everything that arrived is still available under rawAttributes, which is where you look when a customer insists they are sending a department and you cannot find it.
1{2 "id": "prof_01HQ8ZK3M4N5P6R7S8T9V0W1X2",3 "connectionId": "conn_01HQ8ZK3M4N5P6R7S8T9V0W1X2",4 "connectionType": "OktaSAML",5 "organizationId": "org_01HQ8ZK3M4N5P6R7S8T9V0W1X2",6 "idpId": "00u1a0ufowBJlzPlk357",7 "email": "avery@foo-corp.example",8 "firstName": "Avery",9 "lastName": "Lindqvist",10 "groups": ["Engineering", "All Staff"],11 "rawAttributes": {12 "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "avery@foo-corp.example",13 "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname": "Avery",14 "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname": "Lindqvist",15 "department": "Platform",16 "employeeNumber": "4812"17 }18}
Mapping something we do not normalise
Standard fields are handled for you; anything specific to your product is not. A customer sending a cost centre, an employee number or a licence tier needs an explicit mapping, configured per connection in the dashboard so that one customer's costCenter and another's cost_center both land on the same field.
Read the unmapped value from rawAttributes while you are working out what a customer sends, then add the mapping so your code stops depending on a provider-specific key.
- 1Sign in once with a test account and log the whole rawAttributes object. This is faster than reading the customer's provider documentation.
- 2Identify the exact key, including case. Providers are inconsistent about capitalisation and some send the same value under two keys.
- 3Add the mapping on the connection in the dashboard, under identity provider attributes.
- 4Sign in again and confirm the value now appears as a normalised field rather than only in rawAttributes.
- 5Handle the absent case in code anyway. A mapping configured for one customer does not exist for the next one.
Treat every mapped attribute as optional. The connection that has sent a department on every sign-in for six months will stop the week the customer restructures their directory, and an application that throws on a missing attribute becomes an outage for that tenant.
Reading attributes without trusting them
Attributes are asserted by the customer's identity provider, which means they are trustworthy about identity and merely informative about everything else. A groups claim says what the directory believes; it does not say what the person should be allowed to do in your application. Map groups to your own roles rather than checking group names in your request path, or a customer who renames a group in their directory silently revokes access in your product.
1const { profile } = await paycux.sso.getProfileAndToken({ code });23// idpId kalicidir; e-posta degisebilir.4const user = await users.upsertByIdpId(profile.idpId, {5 email: profile.email,6 firstName: profile.firstName ?? null,7 lastName: profile.lastName ?? null,8});910// Gruplar bilgi amaclidir — yetki kendi rol modelinden gelir.11const role = mapGroupsToRole(profile.groups ?? []);12await memberships.assign(user.id, profile.organizationId, role);