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:
- Sessions Controller - SAML SSO authentication flows
- Metadata Controller - SAML metadata generation
- SAML-Kit Library - The Ruby SAML library powering this implementation
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.