~/src/www.mokhan.ca/xlgmokha [main]
cat saml-kit-1-0-27.md
saml-kit-1-0-27.md 26228 bytes | 2018-11-11 12:00
symlink: /opt/ruby/saml-kit-1-0-27.md

saml-kit 1.0.27

saml-kit has been updated to evict expired x509 certificates.

A common scenario in publishing SAML metadata is to rotate certificates. When a certificate is near expiration, it’s useful to be able to publish a new certificate before the old one expires. This allows other parties to sync up and get a copy of the new certificate before the old one expires.

E.g.

  • Week 1: Certificate A active
  • Week 2: Certificate A, B active
  • Week 3: Certificate B active

In the above example Certificate A expires in week 3. Certificate B becomes active in week 2.

By default saml-kit generates metadata using the signing certificates configured in the saml-kit configuration.

private_key = OpenSSL::PKey::RSA.new(2048)

certificate_a = OpenSSL::X509::Certificate.new
certificate_a.not_before = start_of_week_1
certificate_a.not_after = end_of_week_2
certificate_a.public_key = private_key.public_key
certificate_a.sign(private_key, OpenSSL::Digest::SHA256.new)

certificate_b = OpenSSL::X509::Certificate.new
certificate_b.not_before = start_of_week_2
certificate_b.not_after = end_of_week_3
certificate_b.public_key = private_key.public_key
certificate_b.sign(private_key, OpenSSL::Digest::SHA256.new)

configuration = Saml::Kit::Configuration
configuration.entity_id = "https://www.example.org/metadata"
configuration.add_key_pair(certificate_a.to_pem, private_key.export, use: :signing)
configuration.add_key_pair(certificate_b.to_pem, private_key.export, use: :signing)

When saml-kit chooses a key pair to use for signing a message, it chooses the oldest active key pair and evicts key pairs that are associated with an expired certificate.

This means if you read in key pairs from external configuration (e.g. environment variables), a process restart is not necessary to evict stale certificates.

Saml::Kit::Metadata.build(configuration: configuration) do |builder|
  builder.build_identity_provider do |x|
    x.add_single_sign_on_service('https://www.example.org/login', binding: :http_post)
  end
end

Week 1

In week 1, saml-kit will only supply certificate a in the metadata because certificate b is not active yet. During this time, messages signed by saml-kit will only be signed with certificate a. saml-kit will generate something like the following. Some attributes removed for brevity.

<EntityDescriptor entityID="https://www.example.org/metadata">
 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">...</Signature>
 <IDPSSODescriptor>
   <KeyDescriptor use="signing">
     <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
       <X509Data>
         <X509Certificate><!-- certificate a --><X509Certificate>
       </X509Data>
     </KeyInfo>
   </KeyDescriptor>
   <NameIDFormat>...</NameIDFormat>
   <SingleSignOnService Location="https://www.example.org/login"/>
 </IDPSSODescriptor>
</EntityDescriptor>

Week 2

In week 2, saml-kit will supply both certificate a and certificate b in the metadata because both certificates are now considered active. During this time, messages signed by saml-kit will continue to be signed with certificate a. saml-kit will generate something like the following. Some attributes removed for brevity.

<EntityDescriptor entityID="https://www.example.org/metadata">
 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">...</Signature>
 <IDPSSODescriptor>
   <KeyDescriptor use="signing">
     <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
       <X509Data>
         <X509Certificate><!-- certificate a --><X509Certificate>
       </X509Data>
     </KeyInfo>
   </KeyDescriptor>
   <KeyDescriptor use="signing">
     <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
       <X509Data>
         <X509Certificate><!-- certificate b --><X509Certificate>
       </X509Data>
     </KeyInfo>
   </KeyDescriptor>
   <NameIDFormat>...</NameIDFormat>
   <SingleSignOnService Location="https://www.example.org/login"/>
 </IDPSSODescriptor>
</EntityDescriptor>

Week 3

In week 3, saml-kit will only supply certificate b in the metadata because certificate a is now expired. During this time, messages signed by saml-kit will convert to being signed with certificate b. saml-kit will generate something like the following. Some attributes removed for brevity.

<EntityDescriptor entityID="https://www.example.org/metadata">
 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">...</Signature>
 <IDPSSODescriptor>
   <KeyDescriptor use="signing">
     <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
       <X509Data>
         <X509Certificate><!-- certificate b --><X509Certificate>
       </X509Data>
     </KeyInfo>
   </KeyDescriptor>
   <NameIDFormat>...</NameIDFormat>
   <SingleSignOnService Location="https://www.example.org/login"/>
 </IDPSSODescriptor>
</EntityDescriptor>

Real-World SAML Implementation

Here’s how I use saml-kit in a production Rails application for SAML SSO authentication (source):

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  skip_before_action :authenticate_user!, only: [:new, :create, :destroy]

  def new
    @saml_request = Saml::Kit::AuthenticationRequest.deserialize(
      request.params[:SAMLRequest], 
      binding: :http_redirect
    )
  end

  def create
    if user_params[:email].present? && user_params[:password].present?
      user = User.find_by(email: user_params[:email])&.authenticate(user_params[:password])
      
      if user
        session[:user_id] = user.id
        
        if saml_params[:saml_request].present?
          # Handle SAML SSO flow
          saml_request = Saml::Kit::AuthenticationRequest.deserialize(
            saml_params[:saml_request]
          )
          
          response = Saml::Kit::Response.build(saml_request) do |builder|
            builder.embed_user(user)
          end
          
          redirect_to response.redirect_url_for(saml_request.issuer)
        else
          # Regular session login
          redirect_to root_path
        end
      else
        flash[:error] = 'Invalid credentials'
        redirect_to new_session_path
      end
    end
  end

  def destroy
    if saml_params[:saml_request].present?
      # Handle SAML SLO (Single Logout)
      saml_request = Saml::Kit::LogoutRequest.deserialize(
        saml_params[:saml_request]
      )
      
      session.delete(:user_id)
      
      response = Saml::Kit::LogoutResponse.build(saml_request) do |builder|
        builder.embed_status(Saml::Kit::Namespaces::SAML_SUCCESS)
      end
      
      redirect_to response.redirect_url_for(saml_request.issuer)
    else
      # Regular session logout
      session.delete(:user_id)
      redirect_to root_path
    end
  end

  private

  def user_params
    params.permit(:email, :password)
  end

  def saml_params
    params.permit(:saml_request, :relay_state)
  end
end

SAML Metadata Endpoint

The application also provides SAML metadata for service providers to consume:

# app/controllers/metadata_controller.rb
class MetadataController < ApplicationController
  def show
    render xml: Saml::Kit::Metadata.build do |builder|
      builder.build_identity_provider do |idp|
        idp.add_single_sign_on_service(
          sessions_url, 
          binding: :http_post
        )
        idp.add_single_logout_service(
          session_url(''), 
          binding: :http_post
        )
      end
    end
  end
end

This demonstrates saml-kit’s ability to handle complete SAML workflows including authentication requests, responses, and logout scenarios while maintaining certificate rotation capabilities.

Complete SAML Implementation

The above examples are from a working SAML Identity Provider implementation:

GitHub Repository: https://github.com/xlgmokha/proof

Key SAML implementation files:

Run the SAML Identity Provider:

git clone https://github.com/xlgmokha/proof.git
cd proof
bundle install
rails db:setup
rails server

# SAML endpoints available at:
# GET  /metadata           - SAML IdP metadata
# GET  /sessions/new       - SSO login (handles SAML requests)
# POST /sessions           - SSO authentication 
# DELETE /sessions/:id     - SSO logout (handles SAML logout)

Test SAML SSO flow:

# Get IdP metadata
curl -X GET "http://localhost:3000/metadata"

# Initiate SSO (in a real scenario, this comes from a Service Provider)
curl -X GET "http://localhost:3000/sessions/new?SAMLRequest=encoded_saml_request"

# Complete authentication
curl -X POST "http://localhost:3000/sessions" \
  -d "email=user@example.com&password=password&saml_request=encoded_request"

SAML-Kit Library: The saml-kit gem that powers this implementation is available on RubyGems and provides a comprehensive Ruby toolkit for SAML 2.0 implementations.

This gives you a complete reference for implementing SAML Identity Provider functionality with automatic certificate rotation and standards-compliant SAML 2.0 support.