#!/usr/bin/env ruby require "bundler/inline" gemfile do source "https://rubygems.org" gem "base64", "~> 0.1" gem "erb", "~> 4.0" gem "net-hippie", "~> 1.0" gem "rack", "~> 3.0" gem "rackup", "~> 2.0" gem "saml-kit", "~> 1.0" gem "webrick", "~> 1.0" end $scheme = ENV.fetch("SCHEME", "http") $port = ENV.fetch("PORT", 8283).to_i $host = ENV.fetch("HOST", "localhost:#{$port}") $idp_host = ENV.fetch("IDP_HOST", "localhost:8282") 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 = "#{$scheme}://#{$host}/saml/metadata.xml" x.registry = OnDemandRegistry.new x.logger = Logger.new("/dev/stderr") end class UI def metadata xml = Saml::Kit::Metadata.build_xml do |builder| builder.embed_signature = false builder.contact_email = 'ui@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("#{$scheme}://#{$host}/saml/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 "/oauth/callback" return oauth_callback(Rack::Request.new(env)) when "/oidc/new" return redirect_to("http://#{$idp_host}/oauth/authorize?client_id=service-provider&state=example&redirect_uri=#{$scheme}://#{$host}/oauth/callback&response_type=code&response_mode=query&scope=openid") when "/saml/metadata.xml" return metadata when "/saml/new" return saml_post_to_idp(Rack::Request.new(env)) else # return saml_post_to_idp(Rack::Request.new(env)) return redirect_to("/saml/new") end when 'POST' case path when "/saml/assertions" return saml_assertions(Rack::Request.new(env)) else return not_found end end not_found end private def not_found [404, { 'X-Backend-Server' => 'UI' }, []] end def redirect_to(location) [302, { 'Location' => location }, []] end def oauth_callback(request) response = Net::Hippie.default_client.post( "http://#{$idp_host}/oauth/token", headers: { 'Authorization' => Net::Hippie.basic_auth('client_id', 'secret') }, body: { grant_type: "authorization_code", code: request.params['code'], code_verifier: "not_implemented" } ) [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate(request.params.merge(JSON.parse(response.body)))]] end def saml_post_to_idp(request) idp = Saml::Kit.registry.metadata_for("http://#{$idp_host}/saml/metadata.xml") 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 end template = <<~ERB