Validation and replay protection¶
When you call pygamlastan.profiles.process_response(), gamlastan runs a
full Web Browser SSO validation suite (destination, audience, conditions,
subject confirmation, signatures, replay, and more). The pygamlastan.security
module exposes the configuration, a structured result, and the replay cache.
Configuration¶
pygamlastan.security.SecurityConfig controls the checks. Use the
production defaults, or a preset:
from pygamlastan import security
cfg = security.SecurityConfig() # production-safe defaults
cfg.clock_skew_seconds = 120 # tunable properties
cfg.require_signed_assertions = True
strict = security.SecurityConfig.strict() # all checks, incl. optional ones
loose = security.SecurityConfig.permissive() # TESTS ONLY, not for production
Warning
permissive() relaxes signature and other requirements and must never be
used in production. It exists so examples and tests can run without real
signatures.
Inspecting the result¶
pygamlastan.security.validate_response() returns a structured
ValidationResult instead of raising, so you can
inspect every check. (process_response raises
SamlProfileError on the first failure; use
validate_response when you want the detail.)
result = security.validate_response(
response, cfg,
received_url="https://sp.example.org/acs",
expected_idp_entity_id="https://idp.example.org",
sp_entity_id="https://sp.example.org/sp",
acs_url="https://sp.example.org/acs",
expected_request_id="_req1",
replay_cache=security.InMemoryReplayCache(),
)
if not result.is_valid():
for check in result.failures():
print(check.check_number, check.check_name, check.detail)
Replay protection¶
A replay cache rejects an assertion id that has already been seen. Use the built-in in-memory cache for a single process:
cache = security.InMemoryReplayCache()
cache.check_and_insert("id-1", expiry) # True the first time, False on replay
Custom backends (Redis, a database, …)¶
A single-process in-memory cache is not enough for a multi-worker deployment (each worker would have its own state). Pass any object implementing the replay cache protocol and gamlastan calls into it:
class RedisReplayCache:
def __init__(self, client):
self.client = client
def check_and_insert(self, id: str, expiry) -> bool:
# Atomically set the key only if absent; return True when newly set.
ttl = max(1, int((expiry - now()).total_seconds()))
return bool(self.client.set(f"saml:{id}", "1", nx=True, ex=ttl))
def cleanup(self) -> None:
pass # Redis expiry handles eviction
result = profiles.process_response(..., replay_cache=RedisReplayCache(redis_client))
The object needs two methods: check_and_insert(id, expiry) -> bool (return
True when the id is new, False on a replay) and cleanup(). The
adapter fails closed: if your method raises, the id is treated as a replay.