Commit f9f9dce
2025-02-27 17:54:10
Changed files (6)
src/idp/.gitignore
@@ -0,0 +1,1 @@
+config.yml
src/idp/cert.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJDQTEL
+MAkGA1UECAwCQUIxEDAOBgNVBAcMB0NhbGdhcnkxDzANBgNVBAoMBlhtbEtpdDEP
+MA0GA1UECwwGWG1sS2l0MQ8wDQYDVQQDDAZYbWxLaXQwHhcNMjIwMzE4MjAzMTMy
+WhcNMjIwNDE3MjAzMTMyWjBfMQswCQYDVQQGEwJDQTELMAkGA1UECAwCQUIxEDAO
+BgNVBAcMB0NhbGdhcnkxDzANBgNVBAoMBlhtbEtpdDEPMA0GA1UECwwGWG1sS2l0
+MQ8wDQYDVQQDDAZYbWxLaXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDEqKaalD1hckzdERHCgn92KaeVX6VBAKw3eTo3jiMu1P+bd63sot1ClgJOfGEV
+l1aDcflHuc229q+HMU0qzqzlvFra42Y1peZXEu01q2M4ZMG9VQI/p2u+cgXrTyBh
++NQMXUgCAwkywp1Et72GkRxkqyQCSM9vAMkwaVjek68zgFUNGSw2ZrKoKJOUf6NR
+l+VoF4Nw3ubfrS+D2F2yzymRRpN3vOrVbwUc18Zpxxhw/C2bYS1FKe4owqm6lkvy
+XKjhvUZ+ursU5qROCJck6EsOsbgoA2GoMfuOMkivkUKXVg8Cv8Z59+/1+6u8oSeG
+1ITr0eMfFf6tShTS8UO1M7yXAgMBAAGjITAfMB0GA1UdDgQWBBT3q/PhlXnHI0St
+QeVGiX2ZmlVRdzANBgkqhkiG9w0BAQsFAAOCAQEAO2tlOzw4KYo+O36xA3lOYEo5
+Swh5nYhaV1A/RBDBr9sA2wwcRLVU27xuLKu8a7fcN2pGpzrfYyQ6vmDIUfGUVdMT
+a0AkHsdrZwn7TUtKpyrc/7zkIG3a26oDpVXdFpQnjoog5gNix2f3SWHYMgGOgLUd
+DtyNh/LQpKTfU6wY50FKqpu/K8cLs0NS0yGmBmd2D1gQXcnY6Ng7K5fA+x3SdMI1
+wVupDCfX4RaWkTK1hnJt/NYsCO6TYp0ltP/Omhv/PDi8C/27wIY9uZ4DaK9vUIQv
+gFO5n+bebIefpJYc0Q8iIFNY4am0DcendxWZSBK2aCWMJUF9H5xaej9a7BBDXw==
+-----END CERTIFICATE-----
src/idp/config.example.yml
@@ -0,0 +1,3 @@
+---
+host: example.ngrok.io
+email: example@example.com
src/idp/insecure-key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAxKimmpQ9YXJM3RERwoJ/dimnlV+lQQCsN3k6N44jLtT/m3et
+7KLdQpYCTnxhFZdWg3H5R7nNtvavhzFNKs6s5bxa2uNmNaXmVxLtNatjOGTBvVUC
+P6drvnIF608gYfjUDF1IAgMJMsKdRLe9hpEcZKskAkjPbwDJMGlY3pOvM4BVDRks
+NmayqCiTlH+jUZflaBeDcN7m360vg9hdss8pkUaTd7zq1W8FHNfGaccYcPwtm2Et
+RSnuKMKpupZL8lyo4b1Gfrq7FOakTgiXJOhLDrG4KANhqDH7jjJIr5FCl1YPAr/G
+effv9furvKEnhtSE69HjHxX+rUoU0vFDtTO8lwIDAQABAoIBACuVJLcFO0UpS5eC
+fOkaepz5RkZ4V+s79u6kUx6UxX9PfQY7U7Qps9dZ31D9h5Z9X5Lp41DeAJUXvna7
+mlpuSyruv0PbOX+SMKYDb8aBIRASZE1NVZ49wEcIhf9MHeUYfAXxdk/b1GIHd0sP
+XVVBO4Wj1+sZr77t8ahk8GkDWcSTvLFhGYsgtWfag4aj1YejxBtKGsYsOMCTZbd4
+A5lYT1dCyfsAo4b3bm6ajjdFoJUeC+SFsDzzbRWGyeRiCMzEBmksEEcMYKRo8uOr
+JYgl6Wc2SBWZL80+d9s22ZHvE7FAmgWn6OQQhy+kR8w5UO52GocwnuNuDm6qVeu5
+66gc6iECgYEA7AI2OiFUneLZ3Ps5QFA4uDLkc7ny58pbLtY57jZvkPvTpvYZkYSu
+x9JPxvSqCmS3hH9HEEE2Y+Tsg/mHEphY6t7REl2InqH3sLaeHQxMfHf9+LcKCPfl
+OIr5yyaZRVB32nW14VukLYsmEQLJYLXaGVQ/sTYRn7Yf5/PKlectIOMCgYEA1VEl
+u14AqEFTTpXC0sNySaM3m5buRYCx/Ro8K1riuKuwzfJT6rOdqvWghNGeFGpYPi4x
+k+i+IfkItYbBA6YS6kROh/l9jkyNGrXWd8gsYiI8L3lpfl2PBCDs9349nl9l8DFZ
+cvkTeRcVxd2RmtNkcNtpMgzjD2ycYaSQqEUGx70CgYEAreKbTY0NKR7g4d3/OpFg
+mOZ2R4WzoHAJaqLQH+DfpnTEZnlgMUUO+Y7M1IujVPEL/YVBOIqzpjoewMXybRLu
+QG5WoC9l32r6caq7KC/Nks9dwggqTp1Gt7g9fx47Q0ScacrcbOP2PNAPBe2FrcmO
+nabjHo/1wDSRoXaPxo6DQ30CgYAU2de8VtXtnGUOO2lNvLkBJakb1kb4GDpNqTDU
+dA/RSUcA+nzlZiU1Pskv8mVnTXXOrik+cfOT0ondZIydVLBoocCjXem97RGl2Lxb
+/P8JoJsNcOq05WRDXQyMrJRNVLncHpbFvD8BCRahvqSq45rfxTKlJ8lSCqXGjZVu
+PUEKaQKBgQCtV2eiirkXM05jIext/hEIuWcnvqr2ea0STha27ZZ7xkF93142GMCX
+0I3b0VszWZj0911SOqUyMCeAKJg9PCz8kNZYZgOe17/bdXSacJJOtDGHgPeucA9Y
+0csGOGCIspHia9MkxpSuvbE/OMa3F4e96Mm50hX/4MWMmyp4ANeSTg==
+-----END RSA PRIVATE KEY-----
src/idp/main.rb
@@ -0,0 +1,204 @@
+#!/usr/bin/env ruby
+
+# Start the server by running:
+#
+# $ ruby main.rb
+
+require "bundler/inline"
+gemfile do
+ source "https://rubygems.org"
+
+ gem "rack", "~> 3.0"
+ gem "rackup", "~> 2.0"
+ gem "saml-kit", "~> 1.0"
+ gem "webrick", "~> 1.0"
+end
+require "erb"
+
+class Configuration
+ def initialize
+ @config = YAML.safe_load(read_from("config.yml"))
+ end
+
+ def [](key)
+ @config.fetch(key.to_s)
+ end
+
+ def private_key
+ @private_key ||= read_from('insecure-key.pem')
+ end
+
+ def certificate
+ @certificate ||= read_from('cert.pem')
+ end
+
+ private
+
+ def base_dir
+ @base_dir ||= Pathname.new(__FILE__).parent
+ end
+
+ def read_from(file)
+ base_dir.join(file).read
+ end
+end
+
+class User
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def name_id_for(name_id_format)
+ @attributes.fetch(:email)
+ end
+
+ def assertion_attributes_for(request)
+ {
+ custom: 'custom attribute'
+ }
+ end
+end
+
+class OnDemandRegistry < Saml::Kit::DefaultRegistry
+ def metadata_for(entity_id)
+ puts entity_id.inspect
+ super(entity_id)
+ end
+end
+
+$config = Configuration.new
+
+Saml::Kit.configure do |x|
+ x.entity_id = "https://#{$config[:host]}/metadata.xml"
+ x.registry = OnDemandRegistry.new
+ x.logger = Logger.new("/dev/stderr")
+ x.add_key_pair(
+ $config.certificate,
+ $config.private_key,
+ use: :signing
+ )
+end
+
+class IdentityProvider
+ def initialize
+ @storage = {}
+ end
+
+ # Download IDP Metadata
+ #
+ # GET /metadata.xml
+ def metadata
+ xml = Saml::Kit::Metadata.build_xml do |builder|
+ builder.embed_signature = false
+ builder.contact_email = 'hi@example.com'
+ builder.organization_name = "Acme, Inc"
+ builder.organization_url = "https://example.com"
+ builder.build_identity_provider do |x|
+ x.add_single_sign_on_service("https://#{$config[:host]}/sessions/new", binding: :http_post)
+ x.name_id_formats = [Saml::Kit::Namespaces::EMAIL_ADDRESS]
+ x.attributes << :Username
+ end
+ end
+
+ [200, { 'Content-Type' => "application/samlmetadata+xml" }, [xml]]
+ end
+
+ def call(env)
+ path = env['PATH_INFO']
+ case env['REQUEST_METHOD']
+ when 'GET'
+ case path
+ when "/metadata.xml"
+ return metadata
+ when "/sessions/new"
+ return post_back(Rack::Request.new(env))
+ else
+ return not_found
+ end
+ when 'POST'
+ case path
+ when "/sessions/new"
+ return post_back(Rack::Request.new(env))
+ else
+ return not_found
+ end
+ end
+ not_found
+ end
+
+ private
+
+ def post_back(request)
+ params = saml_params_from(request)
+ saml_request = binding_for(request).deserialize(params)
+ @builder = nil
+ url, saml_params = saml_request.response_for(
+ User.new($config),
+ binding: :http_post,
+ relay_state: params[:RelayState]
+ ) do |builder|
+ builder.embed_signature = true
+ @builder = builder
+ end
+ template = <<~ERB
+ <!doctype html>
+ <html>
+ <head><title></title></head>
+ <body>
+ <h2>SAML Request</h2>
+ <textarea readonly="readonly" disabled="disabled" cols=225 rows=6><%=- saml_request.to_xml(pretty: true) -%></textarea>
+
+ <h2>SAML Response</h2>
+ <textarea readonly="readonly" disabled="disabled" cols=225 rows=30><%=- @builder.build.to_xml(pretty: true) -%></textarea>
+ <form action="<%= url %>" method="post">
+ <%- saml_params.each do |(key, value)| -%>
+ <input type="hidden" name="<%= key %>" value="<%= value %>" />
+ <%- end -%>
+ <input type="submit" value="Submit" />
+ </form>
+ </body>
+ </html>
+ ERB
+ erb = ERB.new(template, nil, trim_mode: '-')
+ html = erb.result(binding)
+ [200, { 'Content-Type' => "text/html" }, [html]]
+ end
+
+
+ def not_found
+ [404, {}, []]
+ end
+
+ def saml_params_from(request)
+ if request.post?
+ {
+ "SAMLRequest" => request.params["SAMLRequest"],
+ "RelayState" => request.params["RelayState"],
+ }
+ else
+ query_string = request.query_string
+ on = query_string.include?("&") ? "&" : "&"
+ Hash[query_string.split(on).map { |x| x.split("=", 2) }].symbolize_keys
+ end
+ end
+
+ def binding_for(request)
+ location = "#{$config[:host]}/sessions/new"
+ if request.post?
+ Saml::Kit::Bindings::HttpPost
+ .new(location: location)
+ else
+ Saml::Kit::Bindings::HttpRedirect
+ .new(location: location)
+ end
+ end
+end
+
+if __FILE__ == $0
+ app = Rack::Builder.new do
+ use Rack::Reloader
+ run IdentityProvider.new
+ end.to_app
+
+ Rackup::Server.start(app: app, Port: 8282)
+end
src/idp/README.md
@@ -0,0 +1,21 @@
+# SAML IdP
+
+This is a tiny SAML Identity Provider for testing out interactions with
+a SAML Service Provider
+
+## Getting Started
+
+1. Copy the example coniguration
+
+ $ cp config.example.yml config.yml
+
+1. Edit the `config.yml` to match your needs.
+1. Start the server:
+
+ $ ruby -rwebrick main.rb
+
+1. Start ngrok
+
+ $ ngrok http 8282
+
+1. Use `https://<xxxx>.ngrok.io/metadata.xml` as your SAML IDP Metadata url.