The Broken Promise of OIDC

Founder and CEO
- The Myth of Universal Compatibility
- Category 1: The Clear Spec Violations
- Category 2: The "Technically Correct" Headaches
- Advice for Developers
We’ve all been there. You’re building a new application, you grab a certified OpenID Connect (OIDC) library, and you point it at your identity provider. It’s supposed to be plug-and-play. After all, OIDC is a standard, right? It’s the universal language of identity.
But then, your app crashes.
Specifically, it crashes with a cryptic unmarshalling error or a validation failure that makes no sense because you followed the RFC to the letter.
The Myth of Universal Compatibility
Ideally, an OIDC library should be able to connect to any certified provider using a unified codebase. In reality, the landscape is fragmented. Major providers and clients often prioritize backward compatibility or internal ecosystem logic over strict adherence to the OpenID Connect Core 1.0 specification.
For developers working in loosely typed languages like JavaScript, these deviations are often invisible. But for those of us working in strongly typed languages like Go (the language powering Zitadel), these "minor deviations" are annoying.
A string where a number should be isn't just a quirk; it's a runtime panic waiting to happen. An unmarshalling error doesn't care if the spec violation was "minor."
After years of building an identity platform that has to talk to everyone, we’ve categorized these issues into two buckets: Clear Spec Violations (where the provider is objectively wrong) and Implementation Headaches (where the provider—or the ecosystem—is technically allowed to do it, but it hurts anyway).
Category 1: The Clear Spec Violations
These aren't matters of opinion; they are conflicts with the normative text of the OIDC Core specification.
1. The updated_at Type Mismatch (Auth0)
The OIDC spec is explicit about how to represent time: updated_at must be a JSON number representing the number of seconds from 1970-01-01. It is designed to be machine-readable. Yet, in certain legacy configurations, Auth0 (and others) returned this as an ISO-8601 string (e.g., "2021-01-14T...").
For a developer using a dynamic language, this passes unnoticed. But if your Go struct expects the int64 mandated by the spec, the JSON unmarshaller fails immediately. You are forced to write a custom unmarshaller that checks the type at runtime and parses it conditionally, adding unnecessary complexity to a standard field.
2. The email_verified String Problem (AWS Cognito & Google)
This is perhaps the most common violation in the ecosystem. Section 5.1 dictates that email_verified is a boolean. However, AWS Cognito explicitly documents that it returns this as a string, particularly in userinfo responses.
Even Google is a culprit here. In their official OpenID Connect documentation, the ID Token example explicitly shows "email_verified": "true" as a string. This contradicts both the OIDC Core specification and their own discovery documents, leaving developers to guess which type they will actually receive at runtime.
In JavaScript, the string "true" is truthy, so the code passes. In Rust or Go, trying to unmarshal the string "true" into a bool field throws a hard type error, crashing the authentication flow entirely.
3. The "Success" Error (GitHub)
GitHub is a ubiquitous choice for "Social Login," leading many developers to integrate it alongside standard OIDC providers. However, this convenience comes at a cost: GitHub's implementation violates core OAuth 2.0 error handling rules, which in turn breaks the OIDC interoperability that relies on them.
OpenID Connect is built strictly on top of OAuth 2.0 and inherits its transport mechanisms. OAuth 2.0 (RFC 6749, Section 5.2) explicitly mandates that an authorization server must return an HTTP 400 (Bad Request) when a request fails. GitHub, however, frequently returns an HTTP 200 OK status code for failed requests, while burying the actual error details inside the JSON response body.
For a strict client, this is a breaking change. The client sees a 200 OK, assumes the request was successful, and attempts to parse an access token or ID token. It then fails (often with a confusing unmarshalling error) because it received an error object instead of the expected token. This forces OIDC libraries to write specific "GitHub-flavored" code to peek inside successful responses for hidden errors, defeating the purpose of a standardized protocol.
4. Invalid Locale Tags (Spotify)
OIDC Discovery requires ui_locales_supported to use valid BCP 47 language tags. Spotify’s discovery document, however, includes "bp"—presumably for Brazilian Portuguese—instead of the valid pt-BR (which also is listed).
This might seem trivial, but strict language tag parsers used during the discovery phase will panic or throw an error when encountering the invalid code. The result? The entire application fails to start because it cannot validate the provider's configuration.
Category 2: The "Technically Correct" Headaches
These providers might not be violating the strict letter of the law, but they are certainly violating the spirit of interoperability.
5. The Infrastructure Trap: Ignored Key Rotation (Istio, Envoy)
Sometimes, the problem isn't the identity provider—it's the pipes connecting us.
OIDC is designed to support seamless key rotation without downtime. The mechanism is simple: the kid (Key ID) header in a token tells the client which specific key was used to sign it. If the client doesn't recognize the kid, the spec implies it should check the provider's JWKS endpoint to see if a new key has been published. This ensures that security keys can be rotated frequently without breaking active sessions.
However, standard infrastructure tools often fail to honor this dynamic behavior. We found that widely used service meshes (like Istio and Envoy-based proxies) frequently treat the JWKS as a static resource. They cache keys on startup and only refresh them on a fixed schedule—often 10 minutes or longer—completely ignoring the kid signal in the token header that screams "I have a new key!"
The result is a self-inflicted outage. When a compliant IdP (like Zitadel) rotates a key to improve security, it immediately starts signing tokens with the new key. The infrastructure layer, blindly relying on its stale cache, rejects these valid tokens with "unknown key" errors. This forces providers into an awkward compromise: weakening their security posture by disabling auto-rotation just to prevent standard infrastructure from causing downtime.
6. The "Zombie Nonce" (Microsoft Entra ID)
The spec implies a bidirectional contract: if a client sends a nonce, the provider must return it. If the client doesn't send one, the provider generally shouldn't return one. Yet, some flows in Microsoft Entra ID (formerly Azure AD) return a nonce in the ID Token even if the client never requested it.
Strict clients often treat an unsolicited nonce as a potential replay attack or state mismatch, rejecting the valid token. We have to add special exceptions just to ignore this "extra" security feature.
7. The "Common" Issuer Trap (Microsoft Entra ID)
If you are building a B2B app, you often want users from any Microsoft organization to be able to log in. Entra ID handles this via the "common" endpoint, but this convenience breaks the fundamental "Chain of Trust" regarding who the issuer actually is.
Standard OIDC Discovery mandates that the issuer value found in the discovery document must match the iss claim in the returned token exactly. Entra ID makes this impossible in multi-tenant setups. When you use the multi-tenant discovery URL (.../common/v2.0/.well-known/openid-configuration), Entra returns a template rather than a static URL: "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0". (Yes, it literally returns the string {tenantid} with curly braces).
However, when a user actually logs in, the token they receive is issued by a specific tenant. The iss claim contains that tenant’s actual UUID (e.g., .../8888-aaaa.../v2.0). This creates an immediate conflict for OIDC libraries that perform a strict string equality check (Discovery.Issuer == Token.Issuer). Because the template string containing {tenantid} does not equal the specific UUID string, the validation fails. Developers are then forced into a bad choice: either disable issuer validation entirely (a significant security risk) or implement complex, non-standard logic to manually replace the {tenantid} placeholder with the tid (tenant ID) claim from the token header before validating the token.
8. Missing alg Header (Cognito & Entra ID)
While RFC 7517 lists the alg (algorithm) parameter as OPTIONAL in a JSON Web Key (JWK), omitting it creates a headache.
This forces libraries to "guess" the algorithm (usually assuming RS256) or leaves the door open for "Algorithm Confusion" attacks if not handled carefully. By removing explicit intent, these providers weaken the security posture of the entire handshake.
9. The Nested Discovery Path (Keycloak)
The Headache: Unlike the "Issuer Mismatch" in Entra ID where the values don't match, Keycloak creates a problem with finding the values in the first place.
Keycloak uses realm-specific discovery paths (e.g., /realms/myrealm/.well-known/openid-configuration). While this is technically compliant, it trips up many naive clients that expect discovery documents to always live at the root of the domain (example.com/.well-known/...).
If you point a standard client at keycloak.example.com, it hits the root, gets a 404, and fails. You are forced to hardcode the specific realm structure into your configuration ahead of time, breaking the "auto-discovery" promise for clients that just want to point at a domain.
10. Ignoring prompt (GitLab)
The prompt=select_account parameter is supposed to force the Authorization Server to ask the user to select an account. GitLab has a known issue where it ignores this parameter, defaulting to the existing session.
This creates a frustrating loop where users trying to switch accounts are silently logged back in as their previous user. They are unable to switch identities without manually clearing their browser cookies.
Advice for Developers
If there is one lesson here, it's that standards are only as good as their implementation. If you are building an OIDC library, you can't just read the spec; you have to code for the exceptions.
However, applying Postel’s Law ("be permissive on ingress") to security protocols requires extreme caution. We advocate for a split approach: Permissive Parsing, Strict Validation.
- Be Permissive on Data Formats: When unmarshalling JSON, be flexible. If a provider sends a string
"true"for a boolean field, or an ISO-8601 string for a timestamp number, handle it gracefully. These are interoperability quirks, not security threats. - Be Strict on Security Assertions: Never extend this permissiveness to trust decisions. If a token's signature is invalid, if the
nonceis missing when required, or if theaud(audience) claim doesn't match exactly, reject the request. Being "helpful" with cryptographic validation is how vulnerabilities are born. - Be Strict on Egress: When you send data back out, ensure your application emits perfectly compliant requests. Just because others break the spec doesn't mean you should.
Finally, unless you want to maintain a database of these quirks yourself, use a battle-tested library. This is exactly why we put so much effort into the Zitadel OIDC Library—it handles the string vs. bool chaos, the missing headers, and the weird discovery paths so you don't have to.
And if you are looking for an Identity Provider that actually follows the rules (and provides fixes when others don't), give Zitadel a try. We built it because we were tired of debugging everyone else's OIDC implementation.
Sources & Verification
Disclaimer: While we encountered all of these behaviors, cloud ecosystems change regularly. Some of these issues may be patched, while others persist to support legacy clients.
- Auth0 updated_at (String vs Number): Documented in Auth0 Community Threads and Quarkus Issue #43924.
- GitLab prompt=select_account: Confirmed in GitLab Issue #377368.
- AWS Cognito email_verified String: Explicitly stated in AWS Documentation.
- Spotify bp Locale: Visible in the live Spotify Discovery Document.
- Keycloak Realm Discovery: Documented in Keycloak GitHub Issue #29783.
- AWS Cognito Missing alg: Discussed in AWS Labs Issue #6.
- Entra ID Missing alg: Reported in Apache Pulsar Issue #22419.
- Google email_verified String: Visible in the Google OpenID Connect Documentation.
- GitHub "Success" Errors: Documented in GitHub Community Discussion #57068 and observed in Model Context Protocol SDK Issue #1342.
- Entra ID Issuer Mismatch: Confirmed in Spring Security Issue #17948 (where the
{tenantid}placeholder breaks Java clients) and Microsoft Q&A Thread 1464702.