Files
authentik/authentik/sources/saml/processors/request.py
T
Jens L. d1fb7dde14 enterprise/providers: WS-Federation (#19583)
* init

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix metadata

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* aight

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* progress

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix timedelta

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start testing metadata

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add some more tests and schemas

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* test signature

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* attempt to fix signed xml linebreak

https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1258
https://github.com/robrichards/xmlseclibs/issues/28
https://github.com/xmlsec/python-xmlsec/issues/196
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format + gen

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update web

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* more validation

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* hmm

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add e2e test

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* qol fix in wait_for_url

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add UI

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* acs -> reply url

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* sign_out

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix some XML typing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove verification_kp as its not used

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix reply url

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add ws-fed to tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add logout test

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add SAMLSession

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* refactor

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* unrelated type fixes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add backchannel logout

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* delete import_metadata in wsfed

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* include generated realm

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Update web/src/admin/providers/wsfed/WSFederationProviderViewPage.ts

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Jens L. <jens@beryju.org>

* include wtrealm in ui

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-01-28 17:43:16 +01:00

169 lines
6.2 KiB
Python

"""SAML AuthnRequest Processor"""
from base64 import b64encode
from urllib.parse import quote_plus
import xmlsec
from django.http import HttpRequest
from lxml import etree # nosec
from lxml.etree import Element # nosec
from authentik.lib.xml import remove_xml_newlines
from authentik.providers.saml.utils import get_random_id
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode
from authentik.providers.saml.utils.time import get_time_string
from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.constants import (
DIGEST_ALGORITHM_TRANSLATION_MAP,
NS_MAP,
NS_SAML_ASSERTION,
NS_SAML_PROTOCOL,
SAML_BINDING_POST,
SIGN_ALGORITHM_TRANSFORM_MAP,
)
SESSION_KEY_REQUEST_ID = "authentik/sources/saml/request_id"
class RequestProcessor:
"""SAML AuthnRequest Processor"""
source: SAMLSource
http_request: HttpRequest
relay_state: str
request_id: str
issue_instant: str
def __init__(self, source: SAMLSource, request: HttpRequest, relay_state: str):
self.source = source
self.http_request = request
self.relay_state = relay_state
self.request_id = get_random_id()
self.http_request.session[SESSION_KEY_REQUEST_ID] = self.request_id
self.issue_instant = get_time_string()
def get_issuer(self) -> Element:
"""Get Issuer Element"""
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
issuer.text = self.source.get_issuer(self.http_request)
return issuer
def get_name_id_policy(self) -> Element:
"""Get NameID Policy Element"""
name_id_policy = Element(f"{{{NS_SAML_PROTOCOL}}}NameIDPolicy")
name_id_policy.attrib["Format"] = self.source.name_id_policy
return name_id_policy
def get_auth_n(self) -> Element:
"""Get full AuthnRequest"""
auth_n_request = Element(f"{{{NS_SAML_PROTOCOL}}}AuthnRequest", nsmap=NS_MAP)
auth_n_request.attrib["AssertionConsumerServiceURL"] = self.source.build_full_url(
self.http_request
)
auth_n_request.attrib["Destination"] = self.source.sso_url
auth_n_request.attrib["ID"] = self.request_id
auth_n_request.attrib["IssueInstant"] = self.issue_instant
auth_n_request.attrib["ProtocolBinding"] = SAML_BINDING_POST
auth_n_request.attrib["Version"] = "2.0"
# Create issuer object
auth_n_request.append(self.get_issuer())
if self.source.signing_kp:
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.source.signature_algorithm, xmlsec.constants.TransformRsaSha1
)
signature = xmlsec.template.create(
auth_n_request,
xmlsec.constants.TransformExclC14N,
sign_algorithm_transform,
ns=xmlsec.constants.DSigNs,
)
auth_n_request.append(signature)
# Create NameID Policy Object
auth_n_request.append(self.get_name_id_policy())
return auth_n_request
def build_auth_n(self) -> str:
"""Get Signed string representation of AuthN Request
(used for POST Bindings)"""
auth_n_request = self.get_auth_n()
if self.source.signing_kp:
xmlsec.tree.add_ids(auth_n_request, ["ID"])
ctx = xmlsec.SignatureContext()
key = xmlsec.Key.from_memory(
self.source.signing_kp.key_data, xmlsec.constants.KeyDataFormatPem, None
)
key.load_cert_from_memory(
self.source.signing_kp.certificate_data,
xmlsec.constants.KeyDataFormatCertPem,
)
ctx.key = key
digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get(
self.source.digest_algorithm, xmlsec.constants.TransformSha1
)
signature_node = xmlsec.tree.find_node(auth_n_request, xmlsec.constants.NodeSignature)
ref = xmlsec.template.add_reference(
signature_node,
digest_algorithm_transform,
uri="#" + auth_n_request.attrib["ID"],
)
xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped)
xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N)
key_info = xmlsec.template.ensure_key_info(signature_node)
xmlsec.template.add_x509_data(key_info)
ctx.sign(remove_xml_newlines(auth_n_request, signature_node))
return etree.tostring(auth_n_request).decode()
def build_auth_n_detached(self) -> dict[str, str]:
"""Get Dict AuthN Request for Redirect bindings, with detached
Signature. See https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf"""
auth_n_request = self.get_auth_n()
saml_request = deflate_and_base64_encode(etree.tostring(auth_n_request).decode())
response_dict = {
"SAMLRequest": saml_request,
}
if self.relay_state != "":
response_dict["RelayState"] = self.relay_state
if self.source.signing_kp:
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.source.signature_algorithm, xmlsec.constants.TransformRsaSha1
)
# Create the full querystring in the correct order to be signed
querystring = f"SAMLRequest={quote_plus(saml_request)}&"
if "RelayState" in response_dict:
querystring += f"RelayState={quote_plus(response_dict['RelayState'])}&"
querystring += f"SigAlg={quote_plus(self.source.signature_algorithm)}"
ctx = xmlsec.SignatureContext()
key = xmlsec.Key.from_memory(
self.source.signing_kp.key_data, xmlsec.constants.KeyDataFormatPem, None
)
key.load_cert_from_memory(
self.source.signing_kp.certificate_data,
xmlsec.constants.KeyDataFormatPem,
)
ctx.key = key
signature = ctx.sign_binary(querystring.encode("utf-8"), sign_algorithm_transform)
response_dict["Signature"] = b64encode(signature).decode()
response_dict["SigAlg"] = self.source.signature_algorithm
return response_dict