diff options
| author | mo khan <mo@mokhan.ca> | 2022-03-30 17:57:15 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2022-03-30 17:57:15 -0600 |
| commit | 05cc9a044b2c4deb399265cd94ca7f1fe8eb0d2b (patch) | |
| tree | 293326c516623b3d8436c4788399c22d89759025 | |
| parent | f62315ef13ef48aafa6c130709732270986bfd5f (diff) | |
Add a tiny SAML IDP
| -rw-r--r-- | src/saml-idp/README.md | 17 | ||||
| -rw-r--r-- | src/saml-idp/main.rb | 181 |
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?("&") ? "&" : "&" + 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 |
