diff options
Diffstat (limited to 'bin')
| -rwxr-xr-x | bin/idp | 284 |
1 files changed, 161 insertions, 123 deletions
@@ -25,41 +25,146 @@ $scheme = ENV.fetch("SCHEME", "http") $port = ENV.fetch("PORT", 8282).to_i $host = ENV.fetch("HOST", "localhost:#{$port}") -class JWT - attr_reader :claims +module HTTPHelpers + def default_headers + { + 'X-Powered-By' => 'IDP' + } + end - def initialize(claims) - @claims = claims + def http_not_found + [404, default_headers, []] end - def to_jwt - [ - Base64.strict_encode64(JSON.generate({alg: "RS256", typ: "JWT"})), - Base64.strict_encode64(JSON.generate(claims)), - Base64.strict_encode64(JSON.generate({})), - ].join(".") + def http_ok(headers = {}, body = nil) + [200, default_headers.merge(headers), [body]] end end -class User - def initialize(attributes) - @attributes = attributes - end +module Authn + class User + def initialize(attributes) + @attributes = attributes + end - def name_id_for(name_id_format) - if name_id_format == Saml::Kit::Namespaces::EMAIL_ADDRESS - @attributes[:email] - else - @attributes[:id] + def name_id_for(name_id_format) + if name_id_format == Saml::Kit::Namespaces::EMAIL_ADDRESS + @attributes[:email] + else + @attributes[:id] + end + end + + def assertion_attributes_for(request) + { + email: @attributes[:email], + access_token: ::Authz::JWT.new(sub: SecureRandom.uuid, iat: Time.now.to_i).to_jwt, + } end end - def assertion_attributes_for(request) - { - custom: 'custom attribute', - email: @attributes[:email], - access_token: JWT.new(sub: SecureRandom.uuid, iat: Time.now.to_i).to_jwt, - } + class SAMLController + include ::HTTPHelpers + + def initialize(scheme, host) + @saml_metadata = Saml::Kit::Metadata.build do |builder| + builder.contact_email = 'hi@example.com' + builder.organization_name = "Acme, Inc" + builder.organization_url = "#{scheme}://#{host}" + builder.build_identity_provider do |x| + x.add_single_sign_on_service("#{scheme}://#{host}/saml/new", binding: :http_post) + x.name_id_formats = [Saml::Kit::Namespaces::PERSISTENT, Saml::Kit::Namespaces::EMAIL_ADDRESS] + x.attributes << :email + x.attributes << :access_token + end + end + end + + def call(env) + path = env['PATH_INFO'] + case env['REQUEST_METHOD'] + when 'GET' + case path + when "/metadata.xml" + return http_ok( + { 'Content-Type' => "application/samlmetadata+xml" }, + saml_metadata.to_xml(pretty: true) + ) + when "/new" + # TODO:: render a login page + return saml_post_back(Rack::Request.new(env)) + end + when 'POST' + case path + when "/new" + return saml_post_back(Rack::Request.new(env)) + end + end + + http_not_found + end + + private + + attr_reader :saml_metadata + + def saml_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({ id: SecureRandom.uuid, email: "example@example.com" }), + 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>Recieved SAML Request</h2> + <textarea readonly="readonly" disabled="disabled" cols=225 rows=6><%=- saml_request.to_xml(pretty: true) -%></textarea> + + <h2>Sending SAML Response (IdP -> SP)</h2> + <textarea readonly="readonly" disabled="disabled" cols=225 rows=40><%=- @builder.build.to_xml(pretty: true) -%></textarea> + <form id="postback-form" action="<%= url %>" method="post"> + <%- saml_params.each do |(key, value)| -%> + <input type="hidden" name="<%= key %>" value="<%= value %>" /> + <%- end -%> + <input id="submit-button" type="submit" value="Submit" /> + </form> + </body> + </html> + ERB + erb = ERB.new(template, trim_mode: '-') + html = erb.result(binding) + [200, { 'Content-Type' => "text/html" }, [html]] + 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 = "#{$scheme}://#{$host}/saml/new" + if request.post? + Saml::Kit::Bindings::HttpPost.new(location: location) + else + Saml::Kit::Bindings::HttpRedirect.new(location: location) + end + end end end @@ -99,12 +204,26 @@ class Organization end end -module Authx +module Authz + class JWT + attr_reader :claims + + def initialize(claims) + @claims = claims + end + + def to_jwt + [ + Base64.strict_encode64(JSON.generate({alg: "RS256", typ: "JWT"})), + Base64.strict_encode64(JSON.generate(claims)), + Base64.strict_encode64(JSON.generate({})), + ].join(".") + end + end + module Rpc class AbilityHandler def allowed(request, env) - puts [request, env, can?(request)].inspect - { result: can?(request) } @@ -139,6 +258,8 @@ module Authx end class IdentityProvider + include ::HTTPHelpers + def call(env) path = env['PATH_INFO'] case env['REQUEST_METHOD'] @@ -149,57 +270,35 @@ class IdentityProvider when '/.well-known/oauth-authorization-server' return oauth_metadata when '/.well-known/webfinger' # RFC-7033 - return not_found - when "/saml/metadata.xml" - return saml_metadata - when "/saml/new" - # TODO:: render a login page - return saml_post_back(Rack::Request.new(env)) + return http_not_found when "/oauth/authorize" # RFC-6749 return get_authorize(Rack::Request.new(env)) else - return not_found + return http_not_found end when 'POST' case path - when "/saml/new" - return saml_post_back(Rack::Request.new(env)) when "/oauth/authorize" # RFC-6749 return post_authorize(Rack::Request.new(env)) when "/oauth/token" # RFC-6749 return [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({ - access_token: JWT.new(sub: SecureRandom.uuid, iat: Time.now.to_i).to_jwt, + access_token: ::Authz::JWT.new(sub: SecureRandom.uuid, iat: Time.now.to_i).to_jwt, token_type: "Bearer", issued_token_type: "urn:ietf:params:oauth:token-type:access_token", expires_in: 3600, refresh_token: SecureRandom.hex(32) })]] when "/oauth/revoke" # RFC-7009 - return not_found + return http_not_found else - return not_found + return http_not_found end end - not_found + http_not_found end private - def saml_metadata - xml = Saml::Kit::Metadata.build_xml do |builder| - 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("#{$scheme}://#{$host}/saml/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 - # GET /.well-known/oauth-authorization-server def oauth_metadata [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({ @@ -267,9 +366,9 @@ class IdentityProvider end when 'token' - return not_found + return http_not_found else - return not_found + return http_not_found end end @@ -328,70 +427,6 @@ class IdentityProvider op_tos_uri: "", })]] end - - def saml_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({ id: SecureRandom.uuid, email: "example@example.com" }), - 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>Recieved SAML Request</h2> - <textarea readonly="readonly" disabled="disabled" cols=225 rows=6><%=- saml_request.to_xml(pretty: true) -%></textarea> - - <h2>Sending SAML Response (IdP -> SP)</h2> - <textarea readonly="readonly" disabled="disabled" cols=225 rows=40><%=- @builder.build.to_xml(pretty: true) -%></textarea> - <form id="postback-form" action="<%= url %>" method="post"> - <%- saml_params.each do |(key, value)| -%> - <input type="hidden" name="<%= key %>" value="<%= value %>" /> - <%- end -%> - <input id="submit-button" type="submit" value="Submit" /> - </form> - </body> - </html> - ERB - erb = ERB.new(template, trim_mode: '-') - html = erb.result(binding) - [200, { 'Content-Type' => "text/html" }, [html]] - end - - def not_found - [404, { 'X-Backend-Server' => 'IDP' }, []] - 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 = "#{$scheme}://#{$host}/saml/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 @@ -400,9 +435,12 @@ if __FILE__ == $0 use Rack::Reloader map "/twirp" do # https://github.com/arthurnn/twirp-ruby/wiki/Service-Handlers - run ::Authx::Rpc::AbilityService.new(::Authx::Rpc::AbilityHandler.new) + run ::Authz::Rpc::AbilityService.new(::Authz::Rpc::AbilityHandler.new) end + map "/saml" do + run Authn::SAMLController.new($scheme, $host) + end run IdentityProvider.new end.to_app |
