Skip to content

Add AssertionExtension for extensible SAML assertions#245

Open
pelted wants to merge 2 commits intosaml-idp:masterfrom
kolide:feature/assertion-extension
Open

Add AssertionExtension for extensible SAML assertions#245
pelted wants to merge 2 commits intosaml-idp:masterfrom
kolide:feature/assertion-extension

Conversation

@pelted
Copy link
Contributor

@pelted pelted commented Feb 2, 2026

Summary

This PR adds an AssertionExtension hook so consumers of saml_idp can extend the SAML assertion XML at two specification-defined extension points: SubjectConfirmationData and AuthnContextDecl. When no extension is provided, behavior is unchanged and the assertion is built exactly as today.

Motivation

Some IdP deployments need to include custom XML in the assertion—for example, device trust or zero-trust context inside <AuthnContextDecl>, or extra confirmation data inside <SubjectConfirmationData>. The SAML 2.0 Core spec explicitly allows arbitrary elements/attributes at these points (Sections 2.4.1.2 and 2.7.2.2), but the gem currently has no way to inject them without forking. This feature provides a small, optional API to do that.

Extension points

Constant SAML location Use case
AssertionExtension::SUBJECT_CONFIRMATION_DATA_EXTENSION_POINT Inside <SubjectConfirmation> Custom confirmation data; extension emits <SubjectConfirmationData> (with standard attributes when using bearer) and any extra elements/attributes.
AssertionExtension::AUTHN_CONTEXT_DECL_EXTENSION_POINT Inside <AuthnContext>, after <AuthnContextClassRef> Custom authn context; extension adds <AuthnContextDecl>, <AuthnContextDeclRef>, or other AuthnContext children.

Usage

1. Define a subclass that implements #build(context). The context is a Builder block for either the SubjectConfirmation element (SubjectConfirmationData extension) or the AuthnContext element (AuthnContextDecl extension).

# Example: add device/zero-trust context via AuthnContextDecl
class MyAuthnContextExtension < SamlIdp::AssertionExtension
  def initialize
    super(SamlIdp::AssertionExtension::AUTHN_CONTEXT_DECL_EXTENSION_POINT)
  end

  def build(context)
    context.AuthnContextDecl do |builder|
      builder.AuthenticationContextDeclaration xmlns: "urn:my_org:saml:2.0:Device" do |decl|
        decl.Extension do |ext|
          ext.Device xmlns: "urn:my_org:saml:2.0:ZeroTrust", ID: "..." do |device|
            device.Trust do |trust|
              trust.Data Name: "Managed", Value: true
              trust.Data Name: "Compliant", Value: true
            end
          end
        end
      end
    end
  end
end

2. Pass the extension when encoding the authn response (e.g. in your IdP controller):

encode_authn_response(principal, assertion_extension: MyAuthnContextExtension.new)
# or
encode_authn_response(principal, assertion_extension: MySubjectConfirmationDataExtension.new)

If assertion_extension is omitted or nil, the assertion is built with the current default behavior (no custom XML).

API surface

  • New file: lib/saml_idp/assertion_extension.rb — base class with extension_point and abstract #build(context).
  • Controller: encode_authn_response(principal, opts) accepts optional opts[:assertion_extension].
  • SamlResponse / AssertionBuilder: Optional assertion_extension is threaded through and used only when present and when the extension’s extension_point matches the current build step.

All new parameters are optional and default to nil; existing callers are unaffected.

Backward compatibility

  • Fully backward compatible. If assertion_extension is not passed, the generated assertion XML is identical to the current implementation.
  • No changes to method signatures other than adding an optional keyword argument.

SAML 2.0 alignment

The two extension points correspond to:

  • SubjectConfirmationData — Core spec 2.4.1.2 allows arbitrary namespace-qualified attributes and arbitrary child elements inside <SubjectConfirmationData>.
  • AuthnContextDecl — Core spec 2.7.2.2 allows <AuthnContextDecl> (and related elements) inside <AuthnContext> alongside <AuthnContextClassRef>.

We always emit <AuthnContextClassRef> first; the extension then adds declaration content. For SubjectConfirmationData, the extension is responsible for emitting <SubjectConfirmationData> (including standard attributes like NotOnOrAfter, Recipient, InResponseTo for bearer) and any custom content, so that standard SPs still validate the assertion.

Testing

  • spec/lib/saml_idp/assertion_extension_spec.rb — base class behavior, extension point constants, and a subclass that implements #build.
  • Existing assertion_builder_spec, saml_response_spec, and controller_spec remain valid; the default (no extension) path is unchanged.

Checklist

  • New behavior is opt-in and backward compatible.
  • Extension points align with SAML 2.0 Core (SubjectConfirmationData, AuthnContext).
  • Tests added for AssertionExtension; existing specs still pass with no extension.

- Introduced `assertion_extension` attribute in `AssertionBuilder`, `SamlResponse`, and `Controller`.
- Updated initialization methods to accept `assertion_extension` as an option.
- Enhanced the `build` methods to utilize `assertion_extension` for customizing subject confirmation data and authentication context.
- Ensured backward compatibility by maintaining existing functionality when no extension is provided.
- Enhanced comments in the AssertionExtension class to clarify its purpose and usage.
- Provided details on the expected implementation of subclasses and alignment with SAML 2.0 specifications.
- Added references to external documentation for further specification analysis.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant