Commit 5ad8108
Changed files (1)
bin
bin/idp
@@ -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