diff options
| author | mo khan <mo@mokhan.ca> | 2025-02-27 10:54:10 -0700 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-02-27 10:54:10 -0700 |
| commit | f9f9dce24d4099ab919d605447c9928b5734e365 (patch) | |
| tree | 316905f5d383aa271be1cb7c53dd0dfbfc7da3b9 /src | |
Add a tiny SAML IdP
Diffstat (limited to 'src')
| -rw-r--r-- | src/idp/.gitignore | 1 | ||||
| -rw-r--r-- | src/idp/README.md | 21 | ||||
| -rw-r--r-- | src/idp/cert.pem | 20 | ||||
| -rw-r--r-- | src/idp/config.example.yml | 3 | ||||
| -rw-r--r-- | src/idp/insecure-key.pem | 27 | ||||
| -rwxr-xr-x | src/idp/main.rb | 204 |
6 files changed, 276 insertions, 0 deletions
diff --git a/src/idp/.gitignore b/src/idp/.gitignore new file mode 100644 index 00000000..1d3ed4c1 --- /dev/null +++ b/src/idp/.gitignore @@ -0,0 +1 @@ +config.yml diff --git a/src/idp/README.md b/src/idp/README.md new file mode 100644 index 00000000..4452fa95 --- /dev/null +++ b/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. diff --git a/src/idp/cert.pem b/src/idp/cert.pem new file mode 100644 index 00000000..631b8a71 --- /dev/null +++ b/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----- diff --git a/src/idp/config.example.yml b/src/idp/config.example.yml new file mode 100644 index 00000000..165b0403 --- /dev/null +++ b/src/idp/config.example.yml @@ -0,0 +1,3 @@ +--- +host: example.ngrok.io +email: example@example.com diff --git a/src/idp/insecure-key.pem b/src/idp/insecure-key.pem new file mode 100644 index 00000000..d7be441f --- /dev/null +++ b/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----- diff --git a/src/idp/main.rb b/src/idp/main.rb new file mode 100755 index 00000000..1b18f54c --- /dev/null +++ b/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 |
