The promise of SaaS identity is compelling: hand off authentication to a managed service, never patch an identity server again, inherit SOC 2 and ISO 27001 compliance, and get global availability for free. Entra ID, Okta, Ping Cloud, Auth0 – pick one, integrate via OIDC, and move on to building your product.

For 80% of enterprises, this works. Employees log in to internal apps. Standard OAuth2 authorization code flow. MFA via Microsoft Authenticator or Okta Verify. Token issued, resource accessed, session ends. The spec covers it. The SaaS IDP handles it. Everyone is happy.

Then there’s the other 20%.

The Customization Cliff

The OAuth 2.0 and OpenID Connect specifications define a handful of grant types (authorization code, client credentials, device code, refresh token) and a few extension points (custom scopes, claims, token exchange via RFC 8693). These cover the standard cases well.

But real-world identity requirements routinely exceed the spec:

  • Step-up authentication – require MFA mid-session for a sensitive operation (transferring money, changing account settings), not just at login
  • Progressive profiling – collect more user data across multiple sessions rather than requiring a complete registration upfront
  • Risk-based / adaptive authentication – change the auth flow based on device fingerprint, geolocation, behavioral signals, or threat intelligence
  • Account linking – merge a social login identity with an existing enterprise account
  • Anonymous-to-authenticated session promotion – user browses anonymously, adds items to a cart, then registers or logs in without losing their session state
  • Multi-tenant flows – different authentication requirements per tenant within the same application
  • Custom MFA channels – WhatsApp OTP, hardware tokens, biometric verification via external providers

ForgeRock’s Authentication Trees (now called Journeys in PingOne Advanced Identity Cloud) were built for exactly this. Each node in a tree was a decision point: authenticate with password, then check device fingerprint, then branch to SMS MFA if the device is new, or skip MFA if the device is trusted and the IP is known. Nodes could call external services, set session attributes, transform tokens, redirect to external IDPs, or execute arbitrary server-side logic. You could build flows that the OAuth spec never contemplated.

The question is: can SaaS IDPs match this?

IDPCustomization LevelMechanismArbitrary Flow Logic?
ForgeRock (on-prem/PingOne AIC)Very HighAuthentication Trees/Journeys – visual flow builder with custom JS/Java nodesYes – each node can branch, call external APIs, transform data
Ping DaVinciHighVisual flow builder with connectors – similar concept to ForgeRock treesYes – connectors to external services, conditional branching
Auth0Moderate-HighActions – arbitrary Node.js code at specific pipeline stages (login, registration, token exchange)Partially – code runs at fixed trigger points, not arbitrary flow positions
OktaModerateInline Hooks + Event Hooks – call external APIs at specific pipeline stagesPartially – hooks at fixed points, can modify tokens/claims but can’t restructure the flow
Entra IDLow-ModerateConditional Access policies (powerful but opinionated), B2C Custom Policies (XML, notoriously painful)No – policies are rule-based, not flow-based. B2C Custom Policies exist but are so complex that most teams avoid them

The gap between ForgeRock/DaVinci and Entra is enormous. Auth0 and Ping DaVinci are reasonable compromises – SaaS with meaningful customization. But Entra, which is what most enterprises default to because they’re already Microsoft shops, is where the cliff is steepest.

The Entra Paradox: Buy SaaS, Build Custom Anyway

Here is a pattern that repeats across enterprises:

  1. Company adopts Entra ID as the corporate IDP. Employees authenticate to internal apps. Works perfectly.
  2. Company launches a consumer-facing application. Needs anonymous browsing, social login, progressive registration, risk-based MFA, custom branding per locale.
  3. Entra B2C is evaluated. Custom Policies are XML-based, poorly documented, and debugging them is an exercise in reading Azure log tables with 30-second propagation delays. The team burns two sprints trying to build a custom registration flow and gives up.
  4. The team builds a custom authentication layer – a Spring Boot / Node.js service that handles the consumer auth flows, issues its own session tokens, and calls Entra only for employee-facing SSO.
  5. The company now runs two identity systems: Entra for workforce, custom code for consumers. The custom layer has its own user store, its own session management, and its own token logic.

This defeats the entire purpose of adopting a SaaS IDP. The “custom IDP in front of Entra” is essentially a poor man’s ForgeRock – except ForgeRock gave you audited, security-reviewed building blocks, and your custom Spring Boot service gives you code written by developers who are not identity specialists.

The Security Disaster of DIY Auth

This is the part that keeps me up at night. Most software engineers are not identity engineers. Identity and access management is a specialization with its own threat models, attack vectors, and subtle failure modes. When teams are forced to build custom auth layers – because their SaaS IDP can’t handle the use case – they make predictable, dangerous mistakes:

Tokens in localStorage. The custom layer issues JWTs and the frontend stores them in localStorage. This is XSS-vulnerable – any injected script can read the token and exfiltrate it. HttpOnly cookies exist for exactly this reason, but the developer didn’t know that because they’re not an identity engineer.

Rolling own JWT validation. The custom service validates JWTs by checking the signature and expiry. It doesn’t validate the aud (audience) claim, the iss (issuer) claim, or the azp (authorized party). An attacker with a valid token from a different application can use it here. Or worse: the validation accepts alg: none because the library defaults to permissive mode (the “algorithm confusion” attack that has breached multiple production systems).

Predictable password reset tokens. The custom auth layer generates reset tokens using UUID.randomUUID() or, in the worst cases, sequential integers or timestamps. An attacker can enumerate or predict tokens and reset any user’s password.

Missing CSRF protection. The login endpoint accepts POST requests without CSRF tokens. An attacker crafts a page that auto-submits a login form with the attacker’s credentials, logging the victim into the attacker’s account (login CSRF), then capturing anything the victim types.

Session fixation. The custom layer reuses the same session ID before and after authentication. An attacker sets a known session ID in the victim’s browser, waits for the victim to log in, then hijacks the now-authenticated session.

Insecure “remember me.” The custom layer implements persistent login by storing the user ID in an unsigned cookie. Changing the user ID in the cookie logs you in as a different user.

Logging tokens and secrets. The custom layer logs the full HTTP request for debugging, including Authorization headers with bearer tokens. The log aggregator now contains valid access tokens that anyone with log access can use.

These are not hypothetical. Every one of these has been found in production systems built by competent backend engineers who were simply not trained in identity security.

The irony is sharp. Entra’s inflexibility is the direct cause of these custom layers. The company bought Entra to avoid building identity infrastructure. Entra couldn’t handle the use case. The team built custom identity code. The custom code has security vulnerabilities that Entra – or ForgeRock, or Auth0 – would never have.

ForgeRock’s Authentication Trees, Auth0’s Actions, and Ping DaVinci’s Connectors are secure, composable building blocks. They let you customize the flow without writing raw auth code. The security of each building block is reviewed, tested, and maintained by identity specialists. Your custom Spring Boot auth service has none of these properties.

DIY auth on top of Entra gives you the worst of both worlds: vendor lock-in for workforce identity AND security risk for consumer identity.

Anonymous Sessions: The IDP Gap

A common e-commerce flow: a user browses your site without logging in, adds items to a cart, and eventually registers or logs in to complete the purchase. The application needs to track the anonymous session and merge it with the authenticated identity on login.

Entra ID requires users to exist in the directory before issuing any token. There is no concept of an anonymous or transient session in the IDP. The user must register, be provisioned, and exist as a directory object before Entra will authenticate them.

ForgeRock could handle this: create a transient session with a limited-scope token (no directory entry required), track the user’s activity, and on registration, promote the transient session to a full authenticated session and link the accumulated state.

The counter-argument: is an anonymous session an IDP concern or an application concern? A purist would say the application should manage anonymous state (a server-side session, a cookie with a cart ID) and only involve the IDP when the user actually authenticates. The IDP handles identity; the application handles pre-identity state.

In practice, the separation is messy. The anonymous session needs some form of token or identifier. The application builds its own mini session management system – cookies, server-side state, expiry logic. When the user logs in via the IDP, the application must link the anonymous session to the authenticated user. Now you have two parallel session systems: the IDP’s OIDC tokens and the application’s anonymous session cookies. The integration point between them is custom code – and we’ve already discussed what happens when non-identity engineers write custom auth code.

When OAuth2 Is Overkill

Here is a question that almost nobody asks: do you actually need OAuth2?

The full OAuth2 + OIDC flow for a web application:

  1. User clicks “Log in”
  2. Redirect to authorization endpoint with client_id, redirect_uri, scope, state, code_challenge (PKCE)
  3. User authenticates at the IDP
  4. IDP redirects back with an authorization code
  5. Backend exchanges the code for tokens (ID token + access token + refresh token)
  6. Backend validates the ID token (signature, issuer, audience, expiry, nonce)
  7. Backend stores the access token (for API calls) and refresh token (for renewal)
  8. Client receives a session cookie or the tokens directly
  9. On token expiry, the refresh token is used to get new tokens
  10. On logout, tokens are revoked at the IDP

Now consider a traditional server-rendered web application with a single backend. No microservices. No third-party API delegation. The backend serves HTML and handles all business logic.

The same flow with a server-side session:

  1. User submits username and password
  2. Backend validates credentials against its database (or LDAP, or delegates to an IDP via a simple redirect)
  3. Backend creates a server-side session (stored in Redis, a database, or memory)
  4. Backend sets a Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict header
  5. Every subsequent request includes the cookie automatically
  6. Backend looks up the session by ID, gets the user context
  7. On logout, the session is deleted server-side – instant revocation

No token rotation. No JWT validation. No refresh logic. No PKCE. No token storage decisions. The session lives on the server. The cookie is a pointer. Revocation is instant – delete the session row. No waiting for token expiry, no revocation endpoint, no token introspection.

DimensionOAuth2 + JWTServer-Side Session
RevocationDelayed (until token expires) unless you add token introspection or a blacklistInstant (delete session from store)
Token sizeJWT can be 1-3KB+ (claims, signature) – sent on every requestSession ID is ~32 bytes
Client complexityMust handle token storage, refresh logic, silent renewalZero – browser handles cookies automatically
Validation costCryptographic signature verification on every requestSession store lookup (Redis: <1ms)
Distributed systemsStateless – any server can validate the token independentlyRequires shared session store (Redis, database)
Third-party API accessAccess token can be forwarded to other servicesNot applicable without additional mechanism
Mobile appsTokens work nativelySession cookies work but less idiomatically

When OAuth2 is necessary:

  • Microservice architectures where multiple backends need to validate the user’s identity independently
  • Third-party API delegation (the original OAuth2 use case – “let this app access my Google Drive”)
  • Mobile and native apps where browser cookies don’t apply naturally
  • SPAs calling multiple backend services on different domains

When a session cookie is sufficient:

  • Server-rendered web applications (Rails, Django, Spring MVC with Thymeleaf)
  • Monolithic backends that serve a single frontend
  • Applications where all auth decisions happen server-side
  • Internal tools and admin panels

The industry has a tendency to default to OAuth2 for everything because it’s the “modern” approach. But OAuth2 was designed to solve the delegation problem – “let a third party access resources on behalf of the user.” If there is no third party and no delegation, the ceremony of authorization codes, token exchanges, and refresh rotation adds complexity without adding value. The good old JSESSIONID did the job for twenty years and still does it for applications that don’t need token delegation.

Where Each IDP Actually Fits

Use CaseRecommended IDPWhy
Workforce identity (employees → internal apps)Entra ID, OktaStandard OIDC, Conditional Access, device compliance. This is what they’re built for.
Consumer identity with moderate customizationAuth0Actions provide meaningful customization. Good social login support. Reasonable pricing.
Complex enterprise + consumer flowsPing DaVinciVisual flow builder closest to ForgeRock’s trees. Connectors for external services. Conditional branching.
Maximum customization, team available to operateForgeRock / PingOne AIC (self-hosted or managed)Authentication Trees/Journeys. Arbitrary logic. Full control. But you need a team to run it.
Small teams, homelab, open sourceKeycloak, AuthentikSurprisingly capable. OIDC-compliant. Free. Good enough for most small-to-medium deployments.
Simple server-rendered web appSession cookies + your framework’s authNo IDP needed. Spring Security sessions, Django auth, Rails sessions. Add an IDP only when you need SSO or delegation.

The critical mistake: using Entra for consumer-facing identity. Entra is an employee directory with an OIDC endpoint. It is not a consumer identity platform. Using it as one leads to the paradox described above: buying SaaS, then building custom code because the SaaS can’t handle the use case.

Conclusion

The identity industry is stuck in an awkward middle ground:

SaaS IDPs (especially Entra) solve the 80% case – workforce SSO, standard OIDC, MFA – but hit a customization cliff for anything beyond the spec. When teams encounter that cliff, they build custom auth layers that are less secure than what the SaaS IDP replaced.

On-prem IDPs (ForgeRock, early Ping) offered maximum flexibility through composable, security-reviewed building blocks. But they required dedicated teams to operate, patch, and secure – a cost many organizations couldn’t justify.

Custom code fills the gap between the two, but most engineers are not identity engineers. The custom auth layers they build contain predictable, dangerous security vulnerabilities. The irony: the SaaS IDP’s inflexibility is the direct cause of this insecure custom code.

And underlying all of this, a simpler question that rarely gets asked: does this application actually need OAuth2? For a significant number of web applications – server-rendered, single-backend, no API delegation – the entire token ceremony adds complexity without adding value. A server-side session with an HttpOnly cookie provides the same user experience with instant revocation, zero client-side token management, and decades of battle-tested implementations.

The right approach is not to pick the most “modern” tool. It’s to evaluate honestly:

  1. Do you need an IDP at all? If it’s a simple web app with its own user store and no SSO requirement, session-based auth is fine.
  2. If you need an IDP, do you need customization? If standard OIDC flows cover your use case, any SaaS IDP works.
  3. If you need customization, don’t pick Entra. Pick Auth0 or Ping DaVinci – SaaS with composable flow builders.
  4. If you find yourself building a custom auth layer in front of your SaaS IDP, stop and reconsider. You’ve already lost the battle the SaaS was supposed to win. Either pick a more flexible IDP or accept the constraints of the one you have. The worst outcome is both: vendor lock-in and custom security code.