Commit 20e1521
bin/rest-api → bin/api
@@ -13,6 +13,10 @@ gemfile do
gem "webrick", "~> 1.0"
end
+$scheme = ENV.fetch('SCHEME', 'http')
+$port = ENV.fetch('PORT', 8284).to_i
+$host = ENV.fetch('HOST', "localhost:#{$port}")
+
class Project
class << self
def all
@@ -35,11 +39,7 @@ class Project
end
end
-class RESTAPI
- def initialize
- @storage = {}
- end
-
+class API
def call(env)
request = Rack::Request.new(env)
path = env['PATH_INFO']
@@ -75,32 +75,40 @@ class RESTAPI
end
def json_not_found
- [404, { 'X-Backend-Server' => 'REST', 'Content-Type' => 'application/json' }, []]
+ http_response(code: 404)
end
def json_ok(body)
- [200, { 'Content-Type' => 'application/json' }, [JSON.pretty_generate(body)]]
+ http_response(code: 200, body: JSON.pretty_generate(body))
end
def json_created(body)
- [201, { 'Content-Type' => 'application/json' }, [JSON.pretty_generate(body.to_h)]]
+ http_response(code: 201, body: JSON.pretty_generate(body.to_h))
end
def json_unauthorized(permission)
- [401, { 'Content-Type' => 'application/json' }, [JSON.pretty_generate({
+ http_response(code: 401, body: JSON.pretty_generate({
error: {
code: 401,
message: "`#{permission}` is required",
}
- })]]
+ }))
+ end
+
+ def http_response(code:, headers: { 'Content-Type' => 'application/json' }, body: nil)
+ [
+ code,
+ headers.merge({ 'X-Backend-Server' => 'REST' }),
+ [body].compact
+ ]
end
end
if __FILE__ == $0
app = Rack::Builder.new do
use Rack::Reloader
- run RESTAPI.new
+ run API.new
end.to_app
- Rackup::Server.start(app: app, Port: ENV.fetch('PORT', 8284).to_i)
+ Rackup::Server.start(app: app, Port: $port)
end
bin/idp
@@ -13,18 +13,44 @@ gemfile do
gem "webrick", "~> 1.0"
end
+$scheme = ENV.fetch('SCHEME', 'http')
+$port = ENV.fetch('PORT', 8282).to_i
+$host = ENV.fetch('HOST', "localhost:#{$port}")
+
+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
+
class User
def initialize(attributes)
@attributes = attributes
end
def name_id_for(name_id_format)
- @attributes[:email]
+ if name_id_format == Saml::Kit::Namespaces::EMAIL_ADDRESS
+ @attributes[:email]
+ else
+ @attributes[:id]
+ end
end
def assertion_attributes_for(request)
{
- custom: 'custom attribute'
+ custom: 'custom attribute',
+ email: @attributes[:email],
+ access_token: JWT.new(sub: SecureRandom.uuid, iat: Time.now.to_i).to_jwt,
}
end
end
@@ -40,16 +66,12 @@ class OnDemandRegistry < Saml::Kit::DefaultRegistry
end
Saml::Kit.configure do |x|
- x.entity_id = "http://localhost:8282/metadata.xml"
+ x.entity_id = "#{$scheme}://#{$host}/metadata.xml"
x.registry = OnDemandRegistry.new
x.logger = Logger.new("/dev/stderr")
end
class IdentityProvider
- def initialize
- @storage = {}
- end
-
def call(env)
path = env['PATH_INFO']
case env['REQUEST_METHOD']
@@ -63,7 +85,8 @@ class IdentityProvider
return not_found
when "/metadata.xml"
return saml_metadata
- when "/sessions/new"
+ when "/saml/new"
+ # TODO:: render a login page
return saml_post_back(Rack::Request.new(env))
when "/oauth/authorize" # RFC-6749
return get_authorize(Rack::Request.new(env))
@@ -72,13 +95,13 @@ class IdentityProvider
end
when 'POST'
case path
- when "/sessions/new"
+ 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: to_jwt(sub: SecureRandom.uuid, iat: Time.now.to_i),
+ access_token: JWT.new(sub: SecureRandom.uuid, iat: Time.now.to_i).to_jwt,
token_type: "Bearer",
expires_in: 3600,
refresh_token: SecureRandom.hex(32)
@@ -94,14 +117,6 @@ class IdentityProvider
private
- def to_jwt(claims)
- [
- Base64.strict_encode64(JSON.generate({alg: "RS256", typ: "JWT"})),
- Base64.strict_encode64(JSON.generate(claims)),
- Base64.strict_encode64(JSON.generate({})),
- ].join(".")
- end
-
# Download IDP Metadata
#
# GET /metadata.xml
@@ -111,7 +126,7 @@ class IdentityProvider
builder.organization_name = "Acme, Inc"
builder.organization_url = "https://example.com"
builder.build_identity_provider do |x|
- x.add_single_sign_on_service("http://localhost:8282/sessions/new", binding: :http_post)
+ 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
@@ -123,9 +138,9 @@ class IdentityProvider
# GET /.well-known/oauth-authorization-server
def oauth_metadata
[200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({
- issuer: "http://localhost:8282/.well-known/oauth-authorization-server",
- authorization_endpoint: "http://localhost:8282/oauth/authorize",
- token_endpoint: "http://localhost:8282/oauth/token",
+ issuer: "#{$scheme}://#{$host}/.well-known/oauth-authorization-server",
+ authorization_endpoint: "#{$scheme}://#{$host}/oauth/authorize",
+ token_endpoint: "#{$scheme}://#{$host}/oauth/token",
jwks_uri: "", # RFC-7517
registration_endpoint: "", # RFC-7591
scopes_supported: ["openid", "profile", "email"],
@@ -138,10 +153,10 @@ class IdentityProvider
ui_locales_supported: ["en-US"],
op_policy_uri: "",
op_tos_uri: "",
- revocation_endpoint: "http://localhost:8282/oauth/revoke", # RFC-7009
+ revocation_endpoint: "#{$scheme}://#{$host}/oauth/revoke", # RFC-7009
revocation_endpoint_auth_methods_supported: ["client_secret_basic"],
revocation_endpoint_auth_signing_alg_values_supported: ["RS256"],
- introspection_endpoint: "http://localhost:8282/oauth/introspect", # RFC-7662
+ introspection_endpoint: "#{$scheme}://#{$host}/oauth/introspect", # RFC-7662
introspection_endpoint_auth_methods_supported: ["client_secret_basic"],
introspection_endpoint_auth_signing_alg_values_supported: ["RS256"],
code_challenge_methods_supported: [], # RFC-7636
@@ -196,10 +211,10 @@ class IdentityProvider
# GET /.well-known/openid-configuration
def openid_metadata
[200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({
- issuer: "http://localhost:8282/.well-known/oauth-authorization-server",
- authorization_endpoint: "http://localhost:8282/oauth/authorize",
- token_endpoint: "http://localhost:8282/oauth/token",
- userinfo_endpoint: "http://localhost:8282/oidc/user/",
+ issuer: "#{$scheme}://#{$host}/.well-known/oauth-authorization-server",
+ authorization_endpoint: "#{$scheme}://#{$host}/oauth/authorize",
+ token_endpoint: "#{$scheme}://#{$host}/oauth/token",
+ userinfo_endpoint: "#{$scheme}://#{$host}/oidc/user/",
jwks_uri: "", # RFC-7517
registration_endpoint: nil,
scopes_supported: ["openid", "profile", "email"],
@@ -254,7 +269,7 @@ class IdentityProvider
saml_request = binding_for(request).deserialize(params)
@builder = nil
url, saml_params = saml_request.response_for(
- User.new({ email: "example@example.com" }),
+ User.new({ id: SecureRandom.uuid, email: "example@example.com" }),
binding: :http_post,
relay_state: params[:RelayState]
) do |builder|
@@ -270,7 +285,7 @@ class IdentityProvider
<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=30><%=- @builder.build.to_xml(pretty: true) -%></textarea>
+ <textarea readonly="readonly" disabled="disabled" cols=225 rows=40><%=- @builder.build.to_xml(pretty: true) -%></textarea>
<form action="<%= url %>" method="post">
<%- saml_params.each do |(key, value)| -%>
<input type="hidden" name="<%= key %>" value="<%= value %>" />
@@ -303,7 +318,7 @@ class IdentityProvider
end
def binding_for(request)
- location = "http://localhost:8282/sessions/new"
+ location = "#{$scheme}://#{$host}/saml/new"
if request.post?
Saml::Kit::Bindings::HttpPost
.new(location: location)
@@ -320,5 +335,5 @@ if __FILE__ == $0
run IdentityProvider.new
end.to_app
- Rackup::Server.start(app: app, Port: ENV.fetch('PORT', 8282).to_i)
+ Rackup::Server.start(app: app, Port: $port)
end
bin/sp → bin/ui
@@ -14,6 +14,10 @@ gemfile do
gem "webrick", "~> 1.0"
end
+$scheme = ENV.fetch('SCHEME', 'http')
+$port = ENV.fetch('PORT', 8283).to_i
+$host = ENV.fetch('HOST', "localhost:#{$port}")
+
class OnDemandRegistry < Saml::Kit::DefaultRegistry
def metadata_for(entity_id)
found = super(entity_id)
@@ -25,28 +29,21 @@ class OnDemandRegistry < Saml::Kit::DefaultRegistry
end
Saml::Kit.configure do |x|
- x.entity_id = "http://localhost:8283/metadata.xml"
+ x.entity_id = "#{$scheme}://#{$host}/metadata.xml"
x.registry = OnDemandRegistry.new
x.logger = Logger.new("/dev/stderr")
end
-class ServiceProvider
- def initialize
- @storage = {}
- end
-
- # Download IDP Metadata
- #
- # GET /metadata.xml
+class UI
def metadata
xml = Saml::Kit::Metadata.build_xml do |builder|
builder.embed_signature = false
- builder.contact_email = 'hi@example.com'
+ 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("http://localhost:8283/assertions", binding: :http_post)
+ x.add_assertion_consumer_service("#{$scheme}://#{$host}/saml/assertions", binding: :http_post)
end
end
@@ -60,16 +57,19 @@ class ServiceProvider
case path
when "/metadata.xml"
return metadata
- when "/openid/new"
- return redirect_to("http://localhost:8282/oauth/authorize?client_id=service-provider&state=example&redirect_uri=http://localhost:8283/oauth/callback&response_type=code&response_mode=query&scope=openid")
+ when "/oidc/new"
+ return redirect_to("http://localhost:8282/oauth/authorize?client_id=service-provider&state=example&redirect_uri=#{$scheme}://#{$host}/oauth/callback&response_type=code&response_mode=query&scope=openid")
+ when "/saml/new"
+ return saml_post_to_idp(Rack::Request.new(env))
when "/oauth/callback"
return oauth_callback(Rack::Request.new(env))
else
- return saml_post_to_idp(Rack::Request.new(env))
+ # return saml_post_to_idp(Rack::Request.new(env))
+ return redirect_to("/saml/new")
end
when 'POST'
case path
- when "/assertions"
+ when "/saml/assertions"
return saml_assertions(Rack::Request.new(env))
else
return not_found
@@ -81,7 +81,7 @@ class ServiceProvider
private
def not_found
- [404, { 'X-Backend-Server' => 'SP' }, []]
+ [404, { 'X-Backend-Server' => 'UI' }, []]
end
def redirect_to(location)
@@ -132,7 +132,7 @@ class ServiceProvider
end
def saml_assertions(request)
- sp = Saml::Kit.registry.metadata_for('http://localhost:8283/metadata.xml')
+ sp = Saml::Kit.registry.metadata_for("#{$scheme}://#{$host}/metadata.xml")
saml_binding = sp.assertion_consumer_service_for(binding: :http_post)
saml_response = saml_binding.deserialize(request.params)
raise saml_response.errors unless saml_response.valid?
@@ -156,8 +156,8 @@ end
if __FILE__ == $0
app = Rack::Builder.new do
use Rack::Reloader
- run ServiceProvider.new
+ run UI.new
end.to_app
- Rackup::Server.start(app: app, Port: ENV.fetch('PORT', 8283).to_i)
+ Rackup::Server.start(app: app, Port: $port)
end
README.md
@@ -13,26 +13,6 @@ Below is a recording of a SAML based service provider initiated login, displayin

-## Identity Provider (SAML IdP)
-
-A minimal SAML Identity Provider for testing interactions with a SAML Service Provider.
-
-1. Start the server:
- ```sh
- ruby ./bin/idp
- ```
-2. Use `http://localhost:8282/metadata.xml` as your SAML IdP metadata URL.
-
-## Service Provider (SAML SP)
-
-A minimal SAML Service Provider for testing interactions with a SAML Identity Provider.
-
-1. Start the server:
- ```sh
- ruby ./bin/sp
- ```
-2. Use `http://localhost:8283/metadata.xml` as your SAML SP metadata URL.
-
## Experiments
### Twirp + gRPC (AuthZ)