Meta Description: Choose between SAML vs OIDC federation for your cloud — understand token formats, trust flows, and which protocol fits your IdP and workload mix.
What Is Cloud IAM → Authentication vs Authorization → IAM Roles vs Policies → AWS IAM Deep Dive → GCP Resource Hierarchy IAM → Azure RBAC Scopes → OIDC Workload Identity → AWS IAM Privilege Escalation → AWS Least Privilege Audit → SAML vs OIDC Federation
TL;DR
- Federation means downstream systems trust the IdP’s signed assertion — they never see credentials and don’t manage them independently
- SAML is XML-based, browser-oriented, the enterprise standard; OIDC is JWT-based, API-native, the modern protocol for workload identity and consumer SSO
- In OIDC trust policies, the
subcondition is the security boundary — omitting it means any GitHub Actions workflow in any repository can assume your role - Validate all JWT claims: signature,
iss,aud,exp,sub— libraries do this, but need correct configuration (especiallyaud) - The IdP is the trust anchor: compromise the IdP and every downstream system is compromised. Treat IdP admin access with the same controls as your most sensitive system.
- JIT provisioning and Conditional Access extend federation from “who are you” to “are you in an appropriate context right now”
The Big Picture
FEDERATION: HOW TRUST FLOWS FROM IdP TO DOWNSTREAM SYSTEMS
Identity Provider (Okta / Entra ID / Google / AD FS)
┌──────────────────────────────────────────────────────────────────┐
│ User or workload authenticates → IdP issues signed assertion │
│ │
│ ┌──────────────────────────┐ ┌───────────────────────────┐ │
│ │ SAML Assertion (XML) │ │ OIDC ID Token (JWT) │ │
│ │ RSA-signed, 5–10 min │ │ RS256-signed, ~1 hr │ │
│ │ Audience: SP entity ID │ │ aud: client ID │ │
│ │ Subject: user identity │ │ sub: specific workload │ │
│ └───────────┬──────────────┘ └──────────┬────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ human SSO │ workload identity
▼ ▼
┌─────────────────────────┐ ┌───────────────────────────────────┐
│ SP validates signature │ │ AWS STS / GCP STS validates │
│ + audience + timestamp │ │ signature + iss + aud + sub │
│ → console session │ │ → AssumeRoleWithWebIdentity │
└─────────────────────────┘ └───────────────────────────────────┘
Security bound: IdP security bounds every system that trusts it
Disable in Okta → access revoked everywhere that trusts Okta
Introduction
Before federation existed, every system had its own user database. Your Jira account. Your AWS account. Your Salesforce account. Your internal wiki. Each one had its own password, its own MFA, its own offboarding process. When an engineer joined, someone had to create accounts in every system. When they left, you hoped whoever processed the offboarding remembered to deactivate all of them.
I’ve done that audit — the one where you’re trying to figure out if a former employee still has access to anything. You go system by system, cross-reference against HR records, find accounts that exist in places you’ve forgotten the company even uses. In one environment I found an ex-engineer’s account still active in a vendor portal six months after they left, because that system was set up by someone who had since also left the company, and nobody had documented it.
Federation solves this structurally. One identity provider. One place to authenticate. One place to revoke. Every downstream system trusts the IdP’s assertion rather than managing credentials independently. Disable someone in Okta and they lose access everywhere that trusts Okta — immediately, without a checklist.
This episode is how federation actually works at the protocol level, because understanding the mechanism is what lets you design it securely. A federation setup with a trust policy that accepts assertions from any OIDC issuer is worse than no federation — it’s a false sense of security.
The Federation Model
Identity Provider (IdP) Service Provider (SP) / Relying Party
(Okta, Google, AD FS, Entra ID) (AWS, Salesforce, GitHub, your app)
│ │
│ 1. User authenticates to IdP │
│ (password + MFA) │
│ │
│ 2. IdP generates a signed assertion │
│ (SAML response or OIDC ID Token) │
│ ──────────────────────────────────────── ▶│
│ │
│ 3. SP validates the signature │
│ (using IdP's public certificate │
│ or JWKS endpoint) │
│ 4. SP maps identity to local permissions │
│ 5. SP grants access │
The SP never sees the user’s password. It never has one. It trusts the IdP’s cryptographic signature — if the assertion is signed with the IdP’s private key, and the SP trusts that key, the identity is accepted.
This trust chain has one critical property: the security of every SP is bounded by the security of the IdP. Compromise the IdP, and every system that trusts it is compromised. This is why IdP security deserves the same attention as the most sensitive system it gates access to.
SAML 2.0 — The Enterprise Standard
SAML (Security Assertion Markup Language) is XML-based, verbose, and battle-tested. Published in 2005, it’s the protocol behind most enterprise SSO deployments. When your company says “use your corporate login for this vendor app,” SAML is usually the mechanism.
How a SAML Login Flows
1. User visits AWS console (the Service Provider)
2. AWS checks: no active session → redirect to IdP
→ https://company.okta.com/saml?SAMLRequest=...
3. Okta authenticates the user (password, MFA)
4. Okta generates a SAML Assertion — a signed XML document containing:
- Who the user is (Subject, typically email)
- Their attributes (group memberships, custom attributes)
- When the assertion was issued and when it expires (valid 5-10 minutes typically)
- Which SP this is for (Audience restriction)
- Okta's digital signature (RSA-SHA256 or similar)
5. Browser POSTs the assertion to AWS's ACS (Assertion Consumer Service) URL
6. AWS validates the signature against Okta's public cert (retrieved from Okta's metadata URL)
7. AWS reads the SAML attribute for the IAM role
8. AWS calls sts:AssumeRoleWithSAML → issues temporary credentials
9. User gets a console session — no AWS credentials were ever stored anywhere
What a SAML Assertion Actually Looks Like
<saml:Assertion>
<saml:Issuer>https://okta.company.com</saml:Issuer>
<saml:Subject>
<saml:NameID>[email protected]</saml:NameID>
</saml:Subject>
<saml:AttributeStatement>
<!-- This attribute tells AWS which IAM role to assume -->
<saml:Attribute Name="https://aws.amazon.com/SAML/Attributes/Role">
<saml:AttributeValue>
arn:aws:iam::123456789012:role/EngineerRole,arn:aws:iam::123456789012:saml-provider/OktaProvider
</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
<!-- Critical: time bounds on this assertion -->
<saml:Conditions NotBefore="2026-04-11T09:00:00Z" NotOnOrAfter="2026-04-11T09:05:00Z">
<saml:AudienceRestriction>
<!-- Critical: this assertion is ONLY valid for AWS -->
<saml:Audience>https://signin.aws.amazon.com/saml</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<ds:Signature>... RSA-SHA256 signature over the above ...</ds:Signature>
</saml:Assertion>
The Audience restriction and the NotOnOrAfter timestamp are two of the most security-critical fields. The audience ensures this assertion can’t be reused for a different SP. The timestamp ensures it can’t be replayed after expiry.
Setting Up SAML Federation with AWS
# Register Okta as a SAML provider in AWS IAM
aws iam create-saml-provider \
--saml-metadata-document file://okta-metadata.xml \
--name OktaProvider
# Create the IAM role that federated users will assume
aws iam create-role \
--role-name EngineerRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:saml-provider/OktaProvider"
},
"Action": "sts:AssumeRoleWithSAML",
"Condition": {
"StringEquals": {
"SAML:aud": "https://signin.aws.amazon.com/saml"
}
}
}]
}'
# In Okta: configure the AWS IAM Identity Center app
# Attribute mapping: https://aws.amazon.com/SAML/Attributes/Role
# Value: arn:aws:iam::123456789012:role/EngineerRole,arn:aws:iam::123456789012:saml-provider/OktaProvider
# Set maximum session duration (8 hours is reasonable for human access)
aws iam update-role \
--role-name EngineerRole \
--max-session-duration 28800
SAML Attack Surface
| Attack | What It Does | Why It Works | Prevention |
|---|---|---|---|
| XML Signature Wrapping (XSW) | Attacker inserts a malicious assertion, wraps it around the legitimate signed one; some SPs validate the wrong element | SAML’s XML structure is complex; naive signature validation checks the signed element, not the element the SP reads | Use a vetted SAML library — never hand-roll parsing |
| Assertion replay | Steal a valid assertion (e.g., via network intercept) and replay it before NotOnOrAfter |
If the SP doesn’t track used assertion IDs, the same assertion can be used multiple times | Short expiry; SP tracks seen assertion IDs |
| Audience bypass | SP doesn’t verify the Audience field |
An assertion issued for SP A can be used at SP B | Always validate Audience matches your SP entity ID |
XML Signature Wrapping is the most interesting attack historically — it was how security researchers demonstrated SAML implementations in AWS, Google, and others could be bypassed before vendors patched their libraries. The lesson: SAML is complex enough that rolling your own parser is asking for a vulnerability.
OpenID Connect (OIDC) — The Modern Protocol
OIDC is JSON-based, REST-native, and designed for the web and API-first world. Built on top of OAuth 2.0, it’s the protocol behind “Sign in with Google,” GitHub’s OIDC tokens for Actions, and workload identity federation across cloud providers.
Token Anatomy
An OIDC ID Token is a JWT — three base64-encoded parts separated by dots:
Header.Payload.Signature
Header:
{
"alg": "RS256", ← signing algorithm
"kid": "key-id-123" ← which key signed this (for JWKS rotation)
}
Payload (the claims):
{
"iss": "https://accounts.google.com", ← who issued this token
"sub": "108378629573454321234", ← stable user identifier (not email)
"aud": "my-app-client-id", ← who this token is for
"exp": 1749600000, ← expires at (Unix timestamp)
"iat": 1749596400, ← issued at
"email": "[email protected]",
"email_verified": true,
"hd": "company.com" ← hosted domain (Google Workspace)
}
Signature: RSA-SHA256(base64(header) + "." + base64(payload), idp_private_key)
The relying party (your application, or AWS STS) validates the signature using the IdP’s public keys — available at the JWKS endpoint (/.well-known/jwks.json). The signature verification proves the token was issued by the expected IdP and hasn’t been tampered with since.
The Full OIDC Token Exchange (GitHub Actions → AWS)
# GitHub Actions automatically provides an OIDC token in the runner environment
# The token contains: iss=token.actions.githubusercontent.com, repo, ref, sha, run_id, etc.
# Step 1: Fetch the OIDC token from GitHub's token service
TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r '.value')
# Step 2: Present to AWS STS for exchange
aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::123456789012:role/GitHubActionsRole \
--role-session-name github-deploy \
--web-identity-token "${TOKEN}"
# STS performs these validations:
# 1. Fetch GitHub's JWKS: https://token.actions.githubusercontent.com/.well-known/jwks
# 2. Verify signature is valid
# 3. Verify iss = "token.actions.githubusercontent.com" (matches OIDC provider)
# 4. Verify aud = "sts.amazonaws.com"
# 5. Verify sub matches the trust policy condition
# 6. Verify exp is in the future
The trust policy condition on the IAM role is what prevents any GitHub repository from assuming this role:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}
}
}]
}
The sub condition is the security boundary. repo:my-org/my-repo:ref:refs/heads/main means: only runs triggered from the main branch of my-org/my-repo can assume this role. A pull request from a fork, a run from a different repo, or a run from a different branch — all get a different sub claim and the assumption fails.
I’ve reviewed trust policies that omit the sub condition and just check aud. That means any GitHub Actions workflow — in any repository, owned by anyone — can assume that role. That’s not a misconfiguration to be theoretical about: public GitHub repositories exist, and they can trigger GitHub Actions.
OIDC Validation Checklist
Every application that validates OIDC tokens must check all of these:
✓ Signature valid (using IdP's JWKS endpoint — not a hardcoded key)
✓ iss matches the expected IdP URL
✓ aud matches your application's client ID (not just "any audience")
✓ exp is in the future
✓ nbf (not before), if present, is in the past
✓ iat is recent (within your clock skew tolerance)
✓ For workload identity: sub is pinned to the specific workload
Skipping aud validation is the most common mistake. A token issued for application A with aud: app-a-client-id should not be accepted by application B. Without audience validation, any application in your system that can obtain a token for the IdP can reuse it at any other application. Libraries like python-jose and jsonwebtoken validate aud by default — but they need to be configured with the expected audience value.
Enterprise Federation Patterns
Multi-Account AWS with IAM Identity Center + Okta
The pattern I deploy in every multi-account AWS environment:
Okta (IdP)
└── IAM Identity Center
├── Account: prod → Permission Sets: ReadOnly, DevOps
├── Account: staging → Permission Sets: Developer
├── Account: shared → Permission Sets: NetworkAdmin, SecurityAudit
└── Account: sandbox → Permission Sets: Admin (sandbox only)
# Engineers access accounts through Identity Center portal
aws configure sso
# Prompts: SSO start URL, region, account, role
aws sso login --profile prod-readonly
# List available accounts and roles (useful for tooling and scripts)
aws sso list-accounts --access-token "${TOKEN}"
aws sso list-account-roles --access-token "${TOKEN}" --account-id "${ACCOUNT_ID}"
# Get temporary credentials for a specific account/role
aws sso get-role-credentials \
--account-id "${ACCOUNT_ID}" \
--role-name ReadOnly \
--access-token "${TOKEN}"
When an engineer is offboarded from Okta, they lose access to every AWS account immediately. No individual IAM user deletion across 20 accounts. No access key hunting. One action in Okta, complete revocation.
Just-in-Time (JIT) Provisioning
Rather than creating user accounts in every downstream system ahead of time, JIT provisioning creates accounts on first login:
- User authenticates to IdP
- SAML/OIDC assertion includes group memberships and attributes
- SP receives assertion, checks if a user account exists for this
sub - If not: create the account with attributes from the assertion
- Grant access based on group claims
- On subsequent logins: update the account’s attributes if claims changed
The security property: when a user is disabled in the IdP, their account in downstream systems becomes inaccessible even if the account object still exists. There’s nothing to log in with. JIT accounts don’t survive IdP deletion — they’re inactive shells that produce no risk.
The IdP Is the Trust Anchor — Protect It Accordingly
The entire security of a federated system is bounded by the security of the IdP. If an attacker can log into Okta as an admin, they can issue valid SAML assertions for any user, for any role, to any SP that trusts Okta. Every downstream system is compromised simultaneously.
This is not theoretical. In the 2023 Caesars and MGM Resorts attacks, initial access was achieved through social engineering against identity provider support — not through technical exploitation of cloud infrastructure. Once identity infrastructure is compromised, everything downstream follows.
What this means practically:
- MFA for all IdP admin accounts — hardware FIDO2 keys, not TOTP. TOTP codes can be phished in real-time. Hardware keys cannot.
- PIM / JIT access for IdP configuration changes — no standing admin access
- Separate monitoring and alerting for IdP admin activity
- Audit who can modify SAML/OIDC configurations and attribute mappings in the IdP — these are the levers for privilege escalation
- Narrow audience restrictions — configure which SPs can receive assertions; don’t create a wildcard IdP configuration that serves all SPs
Conditional Access — Adding Context to Federation
Modern IdPs support Conditional Access policies that restrict when assertions are issued:
// Entra ID Conditional Access: require MFA + compliant device for AWS access
{
"conditions": {
"applications": {
"includeApplications": ["AWS-Application-ID-in-Entra"]
},
"users": {
"includeGroups": ["all-employees"]
},
"locations": {
"excludeLocations": ["NamedLocation-CorporateNetwork"]
}
},
"grantControls": {
"operator": "AND",
"builtInControls": ["mfa", "compliantDevice"]
}
}
This policy: when an employee accesses AWS from outside the corporate network, they must use MFA on a device that MDM has verified as compliant. From inside the network, the policy still applies but the named location exclusion can relax certain requirements.
Conditional Access is how you move beyond “authenticated to IdP” as the only gate. Device health, network location, risk score — these become inputs to the access decision.
Framework Alignment
| Framework | Reference | What It Covers Here |
|---|---|---|
| CISSP | Domain 5 — Identity and Access Management | Federation is the mechanism for extending identity trust across organizational boundaries |
| CISSP | Domain 3 — Security Architecture | Trust relationships must be explicitly designed; overly broad federation trust is an architectural failure |
| ISO 27001:2022 | 5.19 Information security in supplier relationships | Federation with third-party IdPs and SPs establishes a cross-organizational trust boundary that must be governed |
| ISO 27001:2022 | 8.5 Secure authentication | SAML and OIDC are the secure authentication protocols for federated access — token validation requirements |
| ISO 27001:2022 | 5.17 Authentication information | Credential lifecycle in federated systems — no passwords distributed to SPs; IdP manages authentication |
| SOC 2 | CC6.1 | Federated identity is the access control mechanism for human access to cloud environments in CC6.1 |
| SOC 2 | CC6.6 | Logical access from outside system boundaries — federation with external IdPs and partner organizations |
Key Takeaways
- Federation means downstream systems trust the IdP’s signed assertion — they never see credentials and don’t need to manage them independently
- SAML is XML-based, browser-oriented, widely supported for enterprise SSO; OIDC is JWT-based, API-friendly, the protocol for modern workload identity and consumer SSO
- In OIDC, the
subcondition in trust policies is what prevents any workload from assuming any role — omitting it is a critical misconfiguration - Validate all JWT claims: signature,
iss,aud,exp,sub— libraries do this, but they need correct configuration - The IdP is the trust anchor — its security posture bounds the security of every system that trusts it. Treat IdP admin access with the same controls as your most sensitive systems.
- JIT provisioning and Conditional Access extend federation from “who are you” to “are you in an appropriate context right now”
What’s Next
EP11 brings this into Kubernetes — RBAC, service account tokens, and how the Kubernetes authorization layer interacts with cloud IAM. Two separate systems, both requiring security. A gap in either becomes a gap in both.
Next: Kubernetes RBAC and AWS IAM
Get EP11 in your inbox when it publishes → linuxcent.com/subscribe