summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2022-03-30 17:57:15 -0600
committermo khan <mo@mokhan.ca>2022-03-30 17:57:15 -0600
commit05cc9a044b2c4deb399265cd94ca7f1fe8eb0d2b (patch)
tree293326c516623b3d8436c4788399c22d89759025
parentf62315ef13ef48aafa6c130709732270986bfd5f (diff)
Add a tiny SAML IDP
-rw-r--r--src/saml-idp/README.md17
-rw-r--r--src/saml-idp/main.rb181
2 files changed, 198 insertions, 0 deletions
diff --git a/src/saml-idp/README.md b/src/saml-idp/README.md
new file mode 100644
index 0000000..6af4153
--- /dev/null
+++ b/src/saml-idp/README.md
@@ -0,0 +1,17 @@
+# SAML IDP
+
+This is a tiny SAML Identity Provider for testing out interactions with
+Terraform Cloud.
+
+## Getting Started
+
+1. Edit the `idp.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/saml-idp/main.rb b/src/saml-idp/main.rb
new file mode 100644
index 0000000..e218afb
--- /dev/null
+++ b/src/saml-idp/main.rb
@@ -0,0 +1,181 @@
+#!/usr/bin/env ruby
+
+# Start the server by running:
+#
+# $ ruby -rwebrick main.rb
+
+require "bundler/inline"
+gemfile do
+ source "https://rubygems.org"
+
+ gem "rack", "~> 2.2"
+ gem "saml-kit", "~> 1.3"
+end
+require "rack"
+
+class Configuration
+ def initialize
+ @config = YAML.safe_load(IO.read("idp.yml"))
+ end
+
+ def [](key)
+ @config.fetch(key.to_s)
+ end
+end
+
+class User
+ def name_id_for(name_id_format)
+ $config[:email]
+ end
+
+ def assertion_attributes_for(request)
+ {
+ Username: $config[:email],
+ MemberOf: $config[:email]
+ }
+ end
+end
+
+class OnDemandRegistry < Saml::Kit::DefaultRegistry
+ REGEX = /\/sso\/saml\/samlconf-(?<uuid>[A-Za-z0-9]+)\/metadata/
+
+ def metadata_for(entity_id)
+ found = super(entity_id)
+ return found if found
+
+ uri = URI.parse(entity_id)
+ if uri.host.include?("terraform.io") || uri.host.include?("ngrok.io")
+ metadata = Saml::Kit::Metadata.build do |builder|
+ builder.entity_id = entity_id
+ builder.build_service_provider do |x|
+ match = uri.path.match(REGEX)
+ x.add_assertion_consumer_service("https://#{uri.host}/sso/saml/samlconf-#{match[:uuid]}/acs", binding: :http_post)
+ end
+ end
+ register(metadata)
+ else
+ register_url(entity_id, verify_ssl: Rails.env.production?)
+ end
+
+ 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[:cert], $config[:insecure_key], use: :signing)
+end
+
+class IdentityProvider
+ def initialize
+ @storage = {}
+ end
+
+ # Download IDP Metadata
+ #
+ # GET /metadata.xml
+ # Response
+ #
+ # Status: 200 OK
+ # {xml data}
+ 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]}/sso", binding: :http_post)
+ x.name_id_formats = [Saml::Kit::Namespaces::EMAIL_ADDRESS]
+ x.attributes << :MemberOf
+ x.attributes << :Username
+ end
+ end
+
+ [200, { 'Content-Type' => "application/samlmetadata+xml" }, [xml]]
+ end
+
+ def post_back(request)
+ location = "#{$config[:host]}/sso"
+ params = saml_params_from(request)
+ saml = if request.post?
+ Saml::Kit::Bindings::HttpPost
+ .new(location: location)
+ .deserialize(params)
+ else
+ Saml::Kit::Bindings::HttpRedirect
+ .new(location: location)
+ .deserialize(params)
+ end
+ url, saml_params = saml.response_for(User.new, binding: :http_post, relay_state: params[:RelayState])
+ template = <<~ERB
+ <!doctype html>
+ <html>
+ <head><title></title></head>
+ <body>
+ <form action="<%= url %>" method="post">
+ <%- saml_params.each do |(key, value)| -%>
+ <input type="hidden" name="<%= key %>" value="<%= value %>" />
+ <%- end -%>
+ </form>
+ <script>
+ document.querySelector('form').submit();
+ </script>
+ </body>
+ </html>
+ ERB
+ erb = ERB.new(template, nil, trim_mode: '-')
+ html = erb.result(binding)
+ [200, {}, [html]]
+ end
+
+ def call(env)
+ path = env['PATH_INFO']
+ case env['REQUEST_METHOD']
+ when 'GET'
+ case path
+ when "/metadata.xml"
+ return metadata
+ when "/sso"
+ return post_back(Rack::Request.new(env))
+ end
+ when 'POST'
+ return post_back(Rack::Request.new(env))
+ end
+ not_found
+ end
+
+ private
+
+ 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?("&amp;") ? "&amp;" : "&"
+ result = Hash[query_string.split(on).map { |x| x.split("=", 2) }]
+ result = result.symbolize_keys
+ result
+ end
+ end
+end
+
+if __FILE__ == $0
+ app = Rack::Builder.new do
+ use Rack::Reloader
+ run IdentityProvider.new
+ end.to_app
+
+ Rack::Server.start(app: app, Port: 8282)
+end