Service Provider integration

A Service Provider (SP) sends an AuthnRequest to an Identity Provider (IdP) and later receives and validates a Response. This guide covers both steps.

Resolving the IdP’s metadata

Before it can talk to an IdP, the SP needs that IdP’s metadata: the SSO endpoint to send the AuthnRequest to, and the signing certificate to verify the Response with. As on the IdP side there is no provider database in the binding; you resolve the IdP’s entityID to its metadata and read what you need from it:

from pygamlastan import metadata

ed = metadata.parse_entity(idp_metadata_xml)
sso = ed.single_sign_on_services()[0].location   # AuthnRequest destination
idp_signing_certs = ed.signing_certificates(role="idp")   # list[bytes] (DER)

The two sources mirror the IdP side and differ in how trust is established:

Local files (trusted as provided). Self-contained IdP metadata XML on disk, trusted as-is. A file may be a single <md:EntityDescriptor> or a whole-federation aggregate; parse_entities() indexes every entity in an aggregate, so one federation file gives you every IdP in it.

MDQ (signature-verified per entity). Fetch the IdP on demand by entityID and signature-verify it against the federation signing certificate before trust:

import urllib.parse, urllib.request
from pygamlastan import crypto

def mdq_fetch(base_url: str, entity_id: str, signer_cert: bytes) -> str | None:
    # MDQ single-entity request: {base}/entities/{url-encoded entityID}
    url = f"{base_url.rstrip('/')}/entities/{urllib.parse.quote(entity_id, safe='')}"
    req = urllib.request.Request(url, headers={"Accept": "application/samlmetadata+xml"})
    with urllib.request.urlopen(req, timeout=10) as resp:
        xml_text = resp.read().decode()
    # MANDATORY: reject metadata whose enveloped signature does not verify.
    verifier = crypto.SamlVerifier.from_cert(signer_cert)   # cert PEM/DER bytes
    if not verifier.verify_enveloped(xml_text).is_valid():
        return None
    return xml_text

Warning

The MDQ base URL is the service root, not an aggregate-file directory, and the signing certificate must match the federation environment. For SWAMID QA the base is https://mds.swamid.se/qa/ (a lookup hits .../qa/entities/<id>) and the signer is the QA cert (https://mds.swamid.se/qa/md/swamid-qa.crt); https://mds.swamid.se/qa/md/ serves only aggregate files (no /entities/ endpoint, every lookup 404) and production uses a different signer. See Identity Provider integration for the full discussion.

The signing certificate(s) you read from the resolved IdP metadata are exactly what you feed to SamlVerifier in Processing the response below.

Building an AuthnRequest

Describe the request with pygamlastan.profiles.AuthnRequestOptions, then call pygamlastan.profiles.create_authn_request():

from pygamlastan import core, profiles

options = profiles.AuthnRequestOptions(
    sp_entity_id="https://sp.example.org/sp",
    acs_url="https://sp.example.org/acs",
    destination="https://idp.example.org/sso",   # IdP SSO endpoint
    protocol_binding=core.BINDING_HTTP_POST,      # how the IdP should reply
    name_id_format=core.NAMEID_TRANSIENT,
    force_authn=True,
    authn_context_class_refs=[core.AUTHN_CONTEXT_PASSWORD],
    authn_context_comparison="exact",
)
request = profiles.create_authn_request(options)
xml = request.to_xml()

Every option maps to a field on the resulting message; for example request.issuer.value is the SP entity id and request.requested_authn_context.comparison is "exact".

Sending the request

Encode the request for the wire with the bindings. For the HTTP-Redirect binding:

from pygamlastan import bindings

redirect_url = bindings.redirect_encode(
    xml.encode(), is_request=True,
    destination="https://idp.example.org/sso",
    relay_state="opaque-state",
)
# return an HTTP 302 to redirect_url

Store request.id in your session: you will pass it as expected_request_id when the response comes back, which binds the response to this request.

Processing the response

When the IdP posts the Response back to your ACS:

from pygamlastan import xml, crypto, security, profiles

# 1. Cryptographically verify the signature with the IdP's signing cert
#    (idp_signing_certs[0] from the resolved IdP metadata above).
verifier = crypto.SamlVerifier.from_cert(idp_certificate_pem)
verified = verifier.verify_enveloped(response_xml)

# 2. Parse.
response = xml.parse_response(response_xml)

# 3. Validate and extract the identity.
result = profiles.process_response(
    response,
    security.SecurityConfig(),
    sp_entity_id="https://sp.example.org/sp",
    acs_url="https://sp.example.org/acs",
    expected_idp_entity_id="https://idp.example.org",
    expected_request_id=session["request_id"],
    verified_signed_ids=verified.signed_reference_ids(),
    replay_cache=replay_cache,   # see the validation guide
)

On success you get an pygamlastan.profiles.AuthnResult:

result.name_id                 # the subject identifier
result.name_id_format          # its format URI, if any
result.session_index           # needed later for Single Logout
result.authn_context_class_ref # how the user authenticated
result.idp_entity_id           # the issuing IdP
result.attributes              # list[core.Attribute]
result.attributes_dict()       # {name: [values]} for convenience

Why pass verified_signed_ids?

The presence of a <Signature> element proves nothing on its own. By passing the reference ids returned from a trusted SamlVerifier, you tell the profile which assertion/response ids were actually verified against the IdP’s key, so the “assertions must be signed” requirement is bound to real cryptography rather than to markup. See Signing, verification, and encryption and Validation and replay protection.