Appearance
10.1 — SSO and Webhook-Based Enterprise Integration
1. What is it?
This chapter covers the two enterprise-integration mechanisms this book hasn't yet dedicated full treatment to: SSO (Single Sign-On) — letting employees authenticate to your AI system using their existing corporate identity, rather than a separate username/password — and webhooks as an enterprise integration pattern specifically (Part 7.6 covered webhooks for your own async job notifications; this chapter covers receiving webhooks from enterprise systems as an integration mechanism). REST APIs (Part 1.2), OAuth (Part 9.4), and RBAC (Part 9.4) were covered in depth earlier — this chapter connects them specifically to the enterprise integration context.
2. Why does it exist?
An enterprise customer almost never wants your AI system to be "one more place employees create a separate login" — every additional identity system is more attack surface (Part 9.4), more administrative burden (provisioning/deprovisioning employees across yet another system), and more friction. SSO exists so your system integrates into the customer's existing identity infrastructure rather than adding a new one. Webhooks, as an inbound integration mechanism, exist so your AI system can react to real-time events happening in the customer's other systems (a new ticket created, a CRM record updated) without needing to constantly poll those systems for changes.
3. What problem does it solve?
SSO solves "how do I let this customer's employees use our AI system with the identity and access controls their IT department already manages" — directly connecting to Part 9.4's RBAC discussion, since SSO providers typically also carry group/role information your system can map into its own authorization model. Webhooks-as-integration solve "how do I keep my AI system's knowledge of external state (Part 3.5's RAG corpus, Part 3.3's tool-accessible data) current without constant, wasteful polling" — directly relevant to Part 8.2's stale-data faithfulness failure mode, since an out-of-date document index is often the result of exactly this kind of missing real-time integration.
4. How does it work internally?
SAML and OIDC — the two dominant SSO protocols
- SAML (Security Assertion Markup Language): an older, XML-based protocol, still very common in traditional enterprise environments — the customer's Identity Provider (IdP, e.g., Okta, Azure AD) authenticates the user and sends your application a signed SAML "assertion" confirming their identity and, often, group memberships.
- OIDC (OpenID Connect): a more modern protocol built on top of OAuth 2.0 (Part 9.4) — the same delegated-authorization flow you learned for third-party integrations, but used specifically for authentication, returning a signed JWT ("ID token") containing the user's identity claims.
Employee tries to log into your AI system
Your system redirects to the customer's IdPOkta/Azure AD/etc.
Employee authenticates with THEIR EXISTING corporate credentialsyour system never sees their password
IdP redirects back with a signed assertion/tokenconfirming identity + group/role memberships
Your system verifies the signature, extracts identity + groupsmaps groups to YOUR RBAC roles (Part 9.4)
The signature verification step is critical and non-negotiable — your system must cryptographically verify the assertion/token actually came from the customer's real IdP and wasn't forged, using the IdP's published public key/certificate, exactly the kind of "never trust, always verify" discipline that's recurred throughout this book applied to identity assertions specifically.
JWT/OIDC pitfalls beyond "verify the signature" — algorithm confusion and missing audience/issuer checks
"Verify the signature" is necessary but dangerously incomplete as a description of a safe OIDC token check, because two entire classes of real, well-documented vulnerabilities live specifically in how that verification is performed, not in whether it's performed at all:
- Algorithm-confusion attacks: a JWT header declares its own signing algorithm (
{"alg": "RS256", ...}), and a naive verification library that trusts this header field will happily verify using whatever algorithm the token claims to use. Two concrete exploits follow directly:alg: none— an attacker crafts a token with{"alg": "none"}and no signature at all; a verifier that doesn't explicitly rejectnonetreats the unsigned token as valid. RS256→HS256 downgrade — the IdP normally signs with RS256 (asymmetric: a private key signs, a public key verifies). An attacker who knows the IdP's public key (published openly, by design) crafts a token with{"alg": "HS256"}and signs it using that public key as an HMAC secret. A verifier that readsalgfrom the token and calls the matching HS256 verification routine will use the same public key as the HMAC secret to check the signature — and since the attacker signed with exactly that value, the forged token verifies as valid, even though the attacker never had access to the IdP's actual private key. - Missing audience (
aud) and issuer (iss) validation: a signature check alone confirms a token was genuinely signed by some trusted issuer key — it says nothing about which application the token was issued for or which IdP tenant issued it. Without an explicitaudiencecheck, a token legitimately issued for a completely different application (but signed by the same IdP) would pass verification and be accepted by yours — a real risk in any environment where one IdP issues tokens for multiple applications, which describes essentially every enterprise IdP deployment. Without an explicitissuercheck, a token from the wrong tenant/environment (a customer's staging IdP tenant, or in a multi-tenant SSO setup, a different customer's tenant entirely) could be accepted if it happens to be signed by a key your verifier is configured to trust.
The fix is to pin the expected algorithm, audience, and issuer explicitly as parameters to the verification call itself — never inferred from the token's own claimed header fields:
python
import jwt # PyJWT
from jwt import PyJWKClient
OIDC_ISSUER = "https://customer.okta.com/oauth2/default"
OIDC_JWKS_URL = f"{OIDC_ISSUER}/v1/keys"
EXPECTED_AUDIENCE = "your-application-client-id"
_jwks_client = PyJWKClient(OIDC_JWKS_URL)
def verify_oidc_id_token(id_token: str) -> dict:
"""
Verifies an OIDC ID token safely: pins the accepted algorithm explicitly
(never read from the token's own header, closing both the `alg: none`
and RS256-to-HS256 downgrade attacks), and validates audience and issuer
explicitly rather than relying on signature verification alone.
Args:
id_token (str): The raw JWT received from the customer's IdP.
Returns:
dict: The verified token claims (identity, group memberships, etc.).
"""
signing_key = _jwks_client.get_signing_key_from_jwt(id_token)
return jwt.decode(
id_token,
signing_key.key,
algorithms=["RS256"], # explicit allowlist — "none" and HS256 are never accepted,
# regardless of what the token's own header claims
audience=EXPECTED_AUDIENCE, # rejects a token issued for a different application
issuer=OIDC_ISSUER, # rejects a token from the wrong IdP tenant/environment
options={"require": ["exp", "iat", "aud", "iss"]}, # reject tokens missing these claims outright
)Note that algorithms=["RS256"] is a list of acceptable algorithms your application chooses and hardcodes — the library uses this list to decide how to verify, and never consults the token's own alg header to make that decision, which is exactly what closes both the alg: none and the RS256→HS256 downgrade paths in one change. audience and issuer being passed explicitly (rather than omitted, which most JWT libraries allow and which silently disables both checks) is the second, equally load-bearing half of a safe decode call — "the signature checks out" and "this token is safe to accept" are not the same claim, and treating them as equivalent is precisely the gap both attack classes exploit.
LDAP/Active Directory — the legacy identity system SAML/OIDC often sits in front of
SAML and OIDC are the protocols a modern cloud IdP (Okta, Azure AD/Entra ID) speaks to your application. But underneath a great many enterprise customers — especially larger, longer-established ones, and almost universally in regulated industries and government — the actual, authoritative identity store is still an on-premises Active Directory, spoken to via LDAP (Lightweight Directory Access Protocol), often predating the customer's cloud IdP adoption by a decade or more. An AI FDE who's only prepared for "connect to their Okta" will hit a wall the first time a customer says "our identity system is on-prem AD, and we're not putting it on the public internet."
LDAP bind authentication (the core operation):
1. Your application connects to the customer's LDAP server (often over
LDAPS — LDAP over TLS — never plaintext LDAP for credentials)
2. Your application attempts to "bind" (authenticate) to the directory
using the END USER'S own supplied credentials as the bind credentials —
if the bind succeeds, the directory itself has just verified the
password; your application never independently validates it
3. On successful bind, your application queries the directory for the
user's group memberships (e.g., CN=Support-L1,OU=Groups,DC=corp,DC=example,DC=com)
4. Those AD group memberships map to your RBAC roles — structurally the
same mapping problem as section 4's IdP-group-to-role mapping, just
sourced from LDAP queries instead of a SAML assertion/OIDC tokenThe common real-world pattern an FDE actually deploys against is a hybrid on-prem/cloud identity bridge: the customer runs Azure AD Connect (or an equivalent sync tool) to mirror their on-prem Active Directory into a cloud IdP (Azure AD/Entra ID, or Okta via its AD agent), and your application integrates with the cloud side via standard OIDC/SAML (section 4) rather than speaking raw LDAP directly — the bridge tool is what keeps the two in sync, so a password change or an account disable in on-prem AD propagates to the cloud IdP your application actually talks to, typically within minutes. Direct LDAP integration (binding straight to the on-prem directory yourself) remains necessary when no such bridge exists yet, or for an internal tool the customer explicitly wants kept off any cloud-facing identity path — in which case network reachability (a VPN or private link into the customer's network, since an on-prem LDAP server is essentially never exposed to the public internet) becomes a real deployment prerequisite in its own right, not just a protocol detail.
python
import ssl
from ldap3 import Server, Connection, Tls, ALL
LDAP_HOST = "ldaps://ad.corp.example.com:636"
def authenticate_via_ldap_bind(username: str, password: str) -> list[str] | None:
"""
Authenticates a user by attempting an LDAP bind with their own supplied
credentials — the directory itself verifies the password; this function
never sees or stores it beyond the single bind attempt.
Args:
username (str): The user's directory principal (e.g., a UPN or DN).
password (str): The user's password, used only for this bind attempt.
Returns:
list[str] | None: The user's group DNs if the bind succeeded,
None if authentication failed.
"""
tls_config = Tls(validate=ssl.CERT_REQUIRED) # never disable certificate validation for LDAPS
server = Server(LDAP_HOST, use_ssl=True, tls=tls_config, get_info=ALL)
try:
conn = Connection(server, user=username, password=password, auto_bind=True)
except Exception:
return None # bind failure — invalid credentials or account disabled
conn.search(
search_base="OU=Groups,DC=corp,DC=example,DC=com",
search_filter=f"(member={username})",
attributes=["cn"],
)
groups = [entry.cn.value for entry in conn.entries]
conn.unbind()
return groupsSCIM — automated provisioning and deprovisioning as a security control, not just an SSO convenience
SSO (section 4) answers "how does an already-provisioned employee log in." It says nothing about when an employee's access to your system should first appear or, critically, disappear — that's a distinct problem, and treating SSO alone as sufficient leaves a real, common gap: an employee who's offboarded from the company still has valid group memberships and could still successfully SSO into your system if nothing separately revokes their access, and in practice, "wait for them to next try to log in and get denied" is not how offboarding urgency actually works for a departing employee who may have deliberately-hostile intent or simply retained access they should no longer have.
SCIM (System for Cross-domain Identity Management) is the standard protocol that solves this directly: the customer's IdP pushes user lifecycle events — create, update, deactivate, delete — to your application's SCIM endpoint automatically, the moment they happen in the source of truth (HR system → IdP → your application), rather than your application only ever learning about a user's status reactively, the next time they attempt to authenticate.
Employee offboarded in HR system
IdP receives the offboarding eventOkta/Azure AD
Your SCIM endpoint (/scim/v2/Users/{id})deactivates the account IMMEDIATELY, revoking active sessions/tokens — independent of whether the employee ever tries to log in again
This is the concrete answer to a question that comes up in essentially every enterprise security review: *"how quickly is access revoked when someone leaves?"* — the correct answer, with SCIM properly wired up, is "within minutes of the IdP processing the offboarding event, via an automated push, not gated behind the employee's next login attempt." Without SCIM, the honest answer is uncomfortably weaker: access technically persists until something else notices and manually revokes it, which for a resource an offboarded employee has no further reason to visit could be a long time.
```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class ScimUserPatch(BaseModel):
"""A SCIM PATCH payload's relevant fields for a user lifecycle event."""
active: bool
@app.patch("/scim/v2/Users/{scim_user_id}")
async def handle_scim_deprovision(scim_user_id: str, patch: ScimUserPatch, current_client=None):
"""
Handles a SCIM-driven user lifecycle update from the customer's IdP —
specifically, immediate deactivation on offboarding, independent of
whether the user ever attempts to authenticate again.
Args:
scim_user_id (str): The SCIM-assigned identifier for this user,
mapped to your application's internal user record.
patch (ScimUserPatch): The SCIM PATCH body; `active=False` signals
deprovisioning.
Returns:
dict: The updated user resource in SCIM's expected response shape.
"""
user = await lookup_user_by_scim_id(scim_user_id)
if user is None:
raise HTTPException(status_code=404, detail="user not found")
if not patch.active:
await deactivate_user_account(user.id)
await revoke_all_active_sessions(user.id) # kills any session that predates the deprovisioning event
logger.warning("SCIM deprovisioned user_id=%s (offboarded)", user.id)
return {"id": scim_user_id, "active": patch.active}The distinction worth being explicit about in a customer conversation: SSO is an authentication convenience (one fewer password to manage); SCIM is an authorization-lifecycle security control (access actually gets revoked, automatically, the moment it should) — a mature enterprise deployment needs both, and a security reviewer asking "how do you handle offboarding" is asking specifically about SCIM, not SSO.
Mapping IdP groups to your RBAC roles
A customer's IdP typically already has groups like "Support-Team," "Support-Managers," "IT-Admins" — the practical integration work is mapping these existing groups to your system's own RBAC roles (Part 9.4), so a customer's existing organizational structure and access-management practices (which they already maintain, add/remove employees from, etc.) directly drive your system's authorization without any duplicate administrative work on their part.
Webhooks as an inbound enterprise integration mechanism
Customer's CRM: a record is updated
│
▼ CRM sends a webhook POST to your registered endpoint
┌──────────────────────────────────────┐
│ Your webhook receiver endpoint │
│ - verifies the webhook's signature │ (exactly Part 1.2's idempotency-
│ (most enterprise systems sign key discipline, now applied to
│ webhook payloads, Part 9.5's verifying the SENDER is genuine)
│ "never trust unverified input") │
│ - processes the update (Part 3.4/3.5's │
│ incremental re-indexing, not a full │
│ corpus rebuild for one changed record) │
└──────────────────────────────────────┘This directly closes the staleness gap Part 8.2's real-world example illustrated: rather than periodically re-scanning an entire external system for changes (slow, resource-intensive, and still has a real staleness window between scans), a webhook-driven integration updates your system's knowledge the moment the source system's data actually changes.
5. Simple mental model
SSO is like using your existing company badge to enter a partner company's building instead of that partner company issuing you a completely separate, additional badge to remember and carry — the partner building's door reader (your application) trusts your home company's badge system (the IdP) because it's been configured to recognize and verify it. Webhook-based integration is like subscribing to real-time alerts from a system rather than periodically calling to ask "has anything changed?" — you get told immediately when something relevant happens, instead of discovering it only the next time you happen to check.
6. Real-world example
An enterprise customer's IT team requires that any new internal tool — including your AI assistant — integrate with their existing Okta SSO, with role mapping matching their existing "Support-L1," "Support-L2," "Support-Manager" Okta groups directly to your system's corresponding permission tiers (Part 9.4's RBAC). Simultaneously, your RAG-based knowledge assistant (Part 3.5) needs to stay current with their frequently-updated internal wiki — rather than re-crawling the entire wiki nightly (slow, and still leaves up to a full day's staleness window), the wiki platform's webhook-on-page-update feature triggers your system to re-chunk and re-embed just the specific changed page within seconds of the actual edit, directly preventing the kind of stale-answer failure mode Part 8.2's real-world example described.
7. Architecture diagram
Customer's Identity ProviderOkta/Azure AD
Customer's CRM/wiki/ticketing system
Your AI Systemauthenticates users, maps IdP groups to internal RBAC (Part 9.4)
Webhook receivertriggers incremental re-index (Part 3.4/3.5)
8. Production considerations
- Always verify webhook signatures cryptographically, AND validate a bounded delivery timestamp alongside the signature — a signature alone confirms the payload wasn't tampered with, but says nothing about whether this is the first time it's been delivered; a captured, validly-signed payload replayed later must be rejected by an expired timestamp (section 16).
- Design incremental re-indexing for webhook-triggered updates (Part 3.4's blue-green pattern is for full re-embeddings; a single-record webhook update should be a much lighter, targeted operation) — don't trigger a full corpus rebuild for one changed record.
- Handle webhook delivery failures/retries on the receiving end (Part 5.7/7.7's idempotency discipline) — the source system may retry a webhook delivery, and your receiver must handle a duplicate delivery safely.
- Map IdP groups to RBAC roles deliberately and keep the mapping documented — an undocumented or stale group-to-role mapping is a real, common source of access-control drift as a customer's organizational structure evolves.
- Pin JWT/OIDC algorithm, audience, and issuer explicitly at verification time (section 4's pitfalls subsection) — never let a verification library infer the algorithm from the token's own header, and never omit
audience/issuerchecks even though many libraries allow it. - Wire up SCIM for automated deprovisioning, not just SSO for authentication (section 4) — an enterprise security review will specifically ask how quickly access is revoked on offboarding, and "the next time they try to log in" is not an acceptable answer.
- For a customer whose identity source is on-prem Active Directory, confirm LDAPS (never plaintext LDAP) and clarify whether a hybrid cloud-IdP bridge already exists before assuming a direct LDAP integration is needed (section 4).
9. Common mistakes
- Building a webhook receiver with no signature verification, trusting any request that arrives at the endpoint — a serious, avoidable vulnerability.
- Verifying a webhook's signature but not its delivery timestamp, leaving a captured, validly-signed payload replayable indefinitely.
- Triggering a full corpus re-embed for every single webhook-driven update instead of a targeted, incremental one, wasting cost and time disproportionate to a single record's change (Part 7.10's cost discipline).
- Not handling webhook retry/duplicate-delivery scenarios, causing duplicate processing (Part 5.7's exact idempotency warning, applied to inbound webhooks).
- Building custom authentication instead of integrating with the customer's existing SSO, creating unnecessary friction and an additional identity system for their IT team to manage.
- Verifying a JWT's signature while trusting the token's own
algheader to decide how, or skipping explicitaudience/issuerchecks — either gap can make a properly-signed-but-wrong-context (or outright forged) token pass verification. - Treating SSO as sufficient for offboarding security, with no SCIM (or equivalent) deprovisioning pipeline — access lingers until something else notices.
10. Security considerations
Webhook signature verification, paired with a bounded timestamp check (section 16), is the load-bearing security control for this integration pattern — signature verification alone is functionally equivalent to an unauthenticated API endpoint accepting arbitrary input the first time, and to a replayable one thereafter (Part 9.5's API-security discussion applies directly). SSO assertion/token signature verification is equally load-bearing on the authentication side — never trust an identity claim without cryptographically verifying its source, with algorithm/audience/issuer all pinned explicitly (section 4), not inferred. SCIM-driven deprovisioning closes the authorization-lifecycle gap SSO alone leaves open. LDAP bind credentials must travel over LDAPS, never plaintext LDAP.
11. Performance considerations
Webhook-driven incremental updates are dramatically more efficient than periodic full re-scans for keeping external data current — both in terms of staleness window (near-instant vs. up-to-a-full-scan-interval) and computational cost (one record vs. the entire corpus).
12. Cost considerations
Incremental, webhook-triggered re-indexing avoids the repeated full-corpus embedding cost of periodic full re-scans (Part 3.4's re-embedding cost discussion) — a direct, concrete cost optimization alongside its staleness-reduction benefit.
13. When to use it
SSO: any enterprise deployment, essentially without exception — customers will very commonly require it as a baseline condition of adoption. Webhook-based integration: any scenario where your AI system's knowledge/data needs to track a frequently-changing external source with minimal staleness.
14. When NOT to use it
A small, single-organization internal tool with no enterprise IT department to integrate with may reasonably use simpler authentication initially — though this should be understood as a starting point likely to need SSO integration as the tool scales toward genuine enterprise deployment. For a source system that changes rarely, periodic polling may be simpler than building webhook integration, if the resulting staleness window is genuinely acceptable for the use case.
15. Alternatives and trade-offs
| Approach | Good for | Weak point |
|---|---|---|
| SSO (SAML/OIDC) | No new identity system for the customer to manage, uses their existing access controls | Real integration effort per IdP/protocol |
| Direct LDAP/AD bind | Works against legacy on-prem identity with no cloud IdP dependency | Requires network reachability into the customer's environment; no built-in SSO-style redirect flow |
| Custom authentication (anti-pattern for enterprise) | Simple for a standalone, non-enterprise tool | Unacceptable friction/risk for genuine enterprise deployment |
| SCIM-driven deprovisioning | Near-instant, automated access revocation on offboarding | Requires the customer's IdP to support SCIM and be configured to push events |
| Manual/reactive deprovisioning (anti-pattern) | No integration work | Access lingers indefinitely until someone notices and manually revokes it |
| Webhook-driven updates | Near-instant staleness window, efficient (only changed records processed) | Requires the source system to support webhooks; signature AND timestamp verification discipline required |
| Periodic polling | Simpler to implement, no dependency on source system's webhook support | Real staleness window; wasteful re-scanning of unchanged data |
16. Practical Python/code example
python
import hmac
import hashlib
import time
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
WEBHOOK_SECRET = os.environ["CRM_WEBHOOK_SECRET"] # from a secrets manager, Part 9.5
REPLAY_TOLERANCE_SECONDS = 300 # 5 minutes — matches Stripe/GitHub's typical webhook tolerance window
@app.post("/webhooks/crm-update")
async def handle_crm_webhook(request: Request):
"""
Receives and verifies a CRM webhook before processing: checks the
signature AND a bounded timestamp window, so a captured, validly-signed
payload can't simply be replayed later by an attacker who intercepted it
— signature validity alone says nothing about whether this is the FIRST
time this exact payload has been delivered.
"""
body = await request.body()
signature = request.headers.get("X-Webhook-Signature", "")
timestamp_header = request.headers.get("X-Webhook-Timestamp", "")
if not timestamp_header.isdigit():
raise HTTPException(status_code=401, detail="missing or malformed webhook timestamp")
delivery_age = abs(time.time() - int(timestamp_header))
if delivery_age > REPLAY_TOLERANCE_SECONDS:
raise HTTPException(status_code=401, detail="webhook timestamp outside tolerance window — possible replay")
# Sign over timestamp + body together, not the body alone — otherwise an
# attacker could pair a captured, validly-signed body with a freshly
# forged timestamp and slip back inside the tolerance window.
signed_payload = f"{timestamp_header}.".encode() + body
expected_signature = hmac.new(WEBHOOK_SECRET.encode(), signed_payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected_signature):
raise HTTPException(status_code=401, detail="invalid webhook signature")
payload = await request.json()
await enqueue_incremental_reindex(record_id=payload["record_id"])
return {"status": "accepted"}Two details worth being explicit about: hmac.compare_digest rather than a plain == comparison is a deliberate choice to avoid timing-attack vulnerabilities in signature comparison. And the timestamp is signed together with the body (f"{timestamp_header}.".encode() + body), exactly as Stripe and GitHub's own webhook schemes do — a timestamp check validated separately from the signature would let an attacker attach an arbitrary fresh timestamp to an old, captured, validly-signed payload and walk straight back inside the tolerance window; binding the two together in what's actually signed is what makes the replay window genuinely bounded rather than only bounded in appearance.
17. Production-quality example
An SSO group-to-RBAC-role mapping, explicitly documented and versioned per section 8's recommendation:
python
import logging
logger = logging.getLogger("sso_role_mapping")
IDP_GROUP_TO_ROLE = {
"Support-L1": "support_agent",
"Support-L2": "support_lead",
"IT-Admins": "admin",
}
def map_idp_groups_to_role(idp_groups: list[str]) -> str:
"""
Maps a customer's IdP group memberships to this system's internal RBAC role,
using the highest-privilege matching role if multiple groups match.
Args:
idp_groups (list[str]): Group names from the verified SSO assertion/token.
Returns:
str: The internal role to assign this authenticated session.
"""
matched_roles = [IDP_GROUP_TO_ROLE[g] for g in idp_groups if g in IDP_GROUP_TO_ROLE]
if not matched_roles:
logger.warning("no recognized IdP group in %s — assigning minimal default role", idp_groups)
return "no_access"
role_priority = ["admin", "support_lead", "support_agent"]
for role in role_priority:
if role in matched_roles:
return role
return matched_roles[0]18. Short exercise
A customer's IT team renames their "Support-L2" Okta group to "Senior-Support" as part of an internal reorganization, but doesn't notify your team. Using this chapter's mapping approach, describe what would happen to affected employees' access, and propose a monitoring/alerting mechanism (referencing Part 8.4) that would catch this kind of drift proactively rather than waiting for a support ticket.
19. Interview questions
- Explain the difference between SAML and OIDC, and why signature verification is non-negotiable for both.
- Why is webhook-driven integration generally preferable to periodic polling for keeping a RAG corpus current, both in terms of staleness and cost?
- What security risk does an unverified webhook receiver endpoint introduce, and how does it relate to prompt injection's core lesson from Part 9.1?
- Why does verifying a webhook's signature alone not prevent replay of a captured, validly-signed payload, and what does binding a timestamp into the signed payload actually buy you over checking the timestamp separately?
- Explain the RS256-to-HS256 JWT downgrade attack, and why pinning the algorithm explicitly at verification time (rather than reading it from the token's own header) closes it.
- Why is SCIM a distinct security control from SSO, rather than a redundant piece of the same authentication story?
- A customer says their identity system is "just on-prem AD" — what does that change about your integration approach compared to a customer already using Okta/Azure AD as a cloud IdP?
20. FDE/customer scenario
Customer's IT team: "Any new tool has to integrate with our Okta setup — no exceptions, no separate logins."
This is an extremely common, non-negotiable enterprise requirement, and the credible response demonstrates fluency with exactly this chapter's content: confirming SAML/OIDC support, describing the group-to-role mapping process concretely (using their actual named Okta groups), and setting realistic expectations about the integration effort involved — treating this as a standard, well-understood part of enterprise AI deployment rather than a surprising or unusually difficult request.
Customer's security reviewer (a second, equally common scenario): "When someone leaves the company, how fast does their access to your system actually go away?"
The weak answer describes SSO alone ("they'd fail to log in next time they tried, since their Okta account would be disabled") — which is technically true but leaves an uncomfortable gap: any session or token issued before offboarding may still be valid, and nothing proactively revokes access the moment offboarding happens. The credible answer is SCIM: the customer's IdP pushes a deprovisioning event to your SCIM endpoint the moment HR processes the offboarding, your system deactivates the account and revokes active sessions immediately (section 4/16), and this happens independent of whether the former employee ever attempts to log in again — precisely the distinction this chapter draws between SSO as an authentication convenience and SCIM as an authorization-lifecycle security control.
Key takeaways
- SSO (SAML/OIDC) integrates your AI system into a customer's existing identity infrastructure rather than adding a new one — a near-universal enterprise requirement, not an optional nicety.
- A safe JWT/OIDC verification pins the algorithm, audience, and issuer explicitly at the decode call — never inferred from the token's own header — closing algorithm-confusion (
alg: none, RS256→HS256 downgrade) and cross-application/cross-tenant token acceptance. - LDAP/Active Directory remains the authoritative identity store behind many enterprise customers' cloud IdPs, and SCIM is the distinct, security-critical protocol for automated deprovisioning — SSO alone doesn't guarantee an offboarded employee's access is actually revoked.
- Webhook-based integration keeps external data current with a near-instant staleness window and far lower cost than periodic full re-scans, directly addressing the stale-data failure mode from Part 8.2.
- Webhook signature verification, combined with a bounded timestamp check, is as load-bearing a security control as any authentication mechanism — signature verification alone still permits replay of a captured, validly-signed payload.
Things you should be able to explain
- The SAML/OIDC authentication flow and why signature verification is non-negotiable.
- Why webhook-driven updates are both faster and cheaper than periodic polling for keeping a RAG corpus current.
- The algorithm-confusion and missing audience/issuer pitfalls in JWT verification, and the explicit parameters that close each one.
- Why SCIM is a security control distinct from SSO's authentication role.
Things you should be able to build
- A signature-and-timestamp-verified webhook receiver and an IdP-group-to-RBAC-role mapping function.
- A safe OIDC ID token verification call with algorithm/audience/issuer pinned explicitly.
- An LDAP bind-authentication function and a SCIM deprovisioning endpoint.
Common mistakes
- Unverified webhook receivers trusting any incoming request, or verifying signature but not timestamp, leaving replay possible.
- Full corpus re-embeds triggered by single-record webhook updates.
- Custom authentication instead of SSO integration for enterprise deployments.
- JWT verification that trusts the token's own
algheader, or skips explicit audience/issuer checks. - Treating SSO as sufficient for offboarding security with no SCIM deprovisioning pipeline.
Recommended next chapter
02-enterprise-databases-and-pipelines.md