Commit fc35bf1

mo khan <mo@mokhan.ca>
2025-02-27 18:39:38
Complete the first portion of a saml transaction
1 parent f9f9dce
src/idp/cert.pem
@@ -1,20 +0,0 @@
------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
@@ -1,3 +0,0 @@
----
-host: example.ngrok.io
-email: example@example.com
src/idp/insecure-key.pem
@@ -1,27 +0,0 @@
------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
@@ -8,40 +8,12 @@ require "bundler/inline"
 gemfile do
   source "https://rubygems.org"
 
+  gem "erb", "~> 4.0"
   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)
@@ -49,7 +21,7 @@ class User
   end
 
   def name_id_for(name_id_format)
-    @attributes.fetch(:email)
+    @attributes[:email]
   end
 
   def assertion_attributes_for(request)
@@ -61,22 +33,18 @@ end
 
 class OnDemandRegistry < Saml::Kit::DefaultRegistry
   def metadata_for(entity_id)
-    puts entity_id.inspect
+    found = super(entity_id)
+    return found if found
+
+    register_url(entity_id, verify_ssl: false)
     super(entity_id)
   end
 end
 
-$config = Configuration.new
-
 Saml::Kit.configure do |x|
-  x.entity_id = "https://#{$config[:host]}/metadata.xml"
+  x.entity_id = "http://localhost:8282/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
@@ -89,12 +57,11 @@ class IdentityProvider
   # 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.add_single_sign_on_service("http://localhost:8282/sessions/new", binding: :http_post)
         x.name_id_formats = [Saml::Kit::Namespaces::EMAIL_ADDRESS]
         x.attributes << :Username
       end
@@ -133,7 +100,7 @@ class IdentityProvider
     saml_request = binding_for(request).deserialize(params)
     @builder = nil
     url, saml_params = saml_request.response_for(
-      User.new($config),
+      User.new({ email: "example@example.com" }),
       binding: :http_post,
       relay_state: params[:RelayState]
     ) do |builder|
@@ -145,10 +112,10 @@ class IdentityProvider
       <html>
         <head><title></title></head>
         <body>
-          <h2>SAML Request</h2>
+          <h2>Recieved SAML Request</h2>
           <textarea readonly="readonly" disabled="disabled" cols=225 rows=6><%=- saml_request.to_xml(pretty: true) -%></textarea>
 
-          <h2>SAML Response</h2>
+          <h2>Sending SAML Response (IdP -> SP)</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)| -%>
@@ -183,7 +150,7 @@ class IdentityProvider
   end
 
   def binding_for(request)
-    location = "#{$config[:host]}/sessions/new"
+    location = "http://localhost:8282/sessions/new"
     if request.post?
       Saml::Kit::Bindings::HttpPost
         .new(location: location)
src/idp/README.md
@@ -5,17 +5,8 @@ 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
+    $ ruby main.rb
 
-1. Use `https://<xxxx>.ngrok.io/metadata.xml` as your SAML IDP Metadata url.
+1. Use `http://localhost:8282/metadata.xml` as your SAML IdP Metadata url.
src/sp/main.rb
@@ -0,0 +1,129 @@
+#!/usr/bin/env ruby
+
+# Start the server by running:
+#
+# $ ruby main.rb
+
+require "bundler/inline"
+gemfile do
+  source "https://rubygems.org"
+
+  gem "base64", "~> 0.1"
+  gem "erb", "~> 4.0"
+  gem "rack", "~> 3.0"
+  gem "rackup", "~> 2.0"
+  gem "saml-kit", "~> 1.0"
+  gem "webrick", "~> 1.0"
+end
+
+class OnDemandRegistry < Saml::Kit::DefaultRegistry
+  def metadata_for(entity_id)
+    found = super(entity_id)
+    return found if found
+
+    register_url(entity_id, verify_ssl: false)
+    super(entity_id)
+  end
+end
+
+Saml::Kit.configure do |x|
+  x.entity_id = "http://localhost:8283/metadata.xml"
+  x.registry = OnDemandRegistry.new
+  x.logger = Logger.new("/dev/stderr")
+end
+
+class ServiceProvider
+  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_service_provider do |x|
+        x.name_id_formats = [Saml::Kit::Namespaces::EMAIL_ADDRESS]
+        x.add_assertion_consumer_service("http://localhost:8283/assertions", binding: :http_post)
+      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
+      else
+        # TODO Generate a post to the IdP
+        return post_to_idp(Rack::Request.new(env))
+      end
+    when 'POST'
+      case path
+      when "/assertions"
+        # TODO:: Render the SAMLResponse from the IdP
+        return not_found
+      else
+        return not_found
+      end
+    end
+    not_found
+  end
+
+  private
+
+  def not_found
+    [404, {}, []]
+  end
+
+  def post_to_idp(request)
+    entity_id = 'http://localhost:8282/metadata.xml'
+    idp = Saml::Kit.registry.metadata_for(entity_id)
+    relay_state = Base64.strict_encode64(JSON.generate(redirect_to: '/dashboard'))
+
+    @saml_builder = nil
+    uri, saml_params = idp.login_request_for(binding: :http_post, relay_state: relay_state) do |builder|
+      @saml_builder = builder
+      # builder.issuer = params[:issuer] if params[:issuer].present?
+      # builder.assertion_consumer_service_url = callback_url
+    end
+
+    template = <<~ERB
+      <!doctype html>
+      <html>
+        <head><title></title></head>
+        <body style="background-color: pink;">
+          <h2>Sending SAML Request (SP -> IdP)</h2>
+          <textarea readonly="readonly" disabled="disabled" cols=225 rows=6><%=- @saml_builder.to_xml(pretty: true) -%></textarea>
+
+          <form action="<%= uri %>" 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
+end
+
+if __FILE__ == $0
+  app = Rack::Builder.new do
+    use Rack::Reloader
+    run ServiceProvider.new
+  end.to_app
+
+  Rackup::Server.start(app: app, Port: 8283)
+end
src/sp/README.md
@@ -0,0 +1,16 @@
+# SAML SP
+
+This is a tiny SAML Service Provider for testing out interactions with
+a SAML Identity Provider (IdP)
+
+## Getting Started
+
+1. Start the server:
+
+    $ ruby main.rb
+
+1. Start ngrok
+
+    $ ngrok http 8283
+
+1. Use `https://<xxxx>.ngrok.io/metadata.xml` as your SAML SP Metadata url.