Commit 44a0ee1

mo khan <mo@mokhan.ca>
2025-03-12 17:52:07
feat: render a simple login page
1 parent a06598e
Changed files (3)
bin/api
@@ -101,7 +101,6 @@ class API
       permission: permission,
       resource: ::GlobalID.create(resource, app: "example").to_s
     )
-    puts response.inspect
     response.error.nil? && response.data.result
   end
 
bin/idp
@@ -10,6 +10,7 @@ gemfile do
   gem "globalid", "~> 1.0"
   gem "google-protobuf", "~> 3.0"
   gem "rack", "~> 3.0"
+  gem "rack-session", "~> 2.0"
   gem "rackup", "~> 2.0"
   gem "saml-kit", "~> 1.0"
   gem "twirp", "~> 1.0"
@@ -28,7 +29,7 @@ $host = ENV.fetch("HOST", "localhost:#{$port}")
 module HTTPHelpers
   def default_headers
     {
-      'X-Powered-By' => 'IDP'
+      'X-Powered-By' => 'IdP'
     }
   end
 
@@ -43,6 +44,18 @@ end
 
 module Authn
   class User
+    class << self
+      def find_by_username(username)
+        User.new(id: SecureRandom.uuid, username: username, email: "#{username}@example.com")
+      end
+
+      def find_by_credentials(params = {})
+        user = find_by_username(params["username"])
+        return user if user.valid_password?(params["password"])
+
+      end
+    end
+
     def initialize(attributes)
       @attributes = attributes
     end
@@ -55,12 +68,20 @@ module Authn
       end
     end
 
+    def create_access_token
+      ::Authz::JWT.new(sub: @attributes[:id], iat: Time.now.to_i)
+    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
+
+    def valid_password?(entered_password)
+      # TODO:: BCrypt hash secure compare
+      true
+    end
   end
 
   class OnDemandRegistry < Saml::Kit::DefaultRegistry
@@ -91,7 +112,6 @@ module Authn
           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
@@ -106,14 +126,18 @@ module Authn
             { '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))
+          return login_page(Rack::Request.new(env))
+        when "/login"
+          request = Rack::Request.new(env)
+          if (user = User.find_by_credentials(request.params))
+            return saml_post_back(request, user)
+          else
+            return login_page(request)
+          end
         end
       end
 
@@ -124,20 +148,48 @@ module Authn
 
     attr_reader :saml_metadata
 
-    def saml_post_back(request)
+    def login_page(request)
+      saml_params = saml_params_from(request)
+      saml_request = binding_for(request).deserialize(saml_params)
+      raise saml_request.errors unless saml_request.valid?
+
+      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>
+
+            <form action="/saml/login" method="post">
+              <input type="input" placeholder="Username" name="username" value="" />
+              <input type="password" placeholder="Password" name="password" value="" />
+              <%- saml_params.each do |(key, value)| -%>
+                <input type="hidden" name="<%= key %>" value="<%= value %>" />
+              <%- end -%>
+              <input type="submit" value="Login" />
+            </form>
+          </body>
+        </html>
+      ERB
+      erb = ERB.new(template, trim_mode: '-')
+      html = erb.result(binding)
+      [200, { 'Content-Type' => "text/html" }, [html]]
+    end
+
+    def saml_post_back(request, user)
       params = saml_params_from(request)
       saml_request = binding_for(request).deserialize(params)
+      request.session[:access_token] = user.create_access_token
+
       @builder = nil
       url, saml_params = saml_request.response_for(
-        User.new({ id: SecureRandom.uuid, email: "example@example.com" }),
+        user,
         binding: :http_post,
         relay_state: params[:RelayState]
-      ) do |builder|
-        builder.embed_signature = true
-        @builder = builder
-      end
+      ) { |builder| @builder = builder }
       template = <<~ERB
-        <!doctype html>
+        <!DOCTYPE html>
         <html>
           <head><title></title></head>
           <body>
@@ -274,6 +326,9 @@ module Authz
         when "/authorize" # RFC-6749
           return post_authorize(Rack::Request.new(env))
         when "/token" # RFC-6749
+          request = Rack::Request.new(env)
+          request.body
+
           return [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({
             access_token: ::Authz::JWT.new(sub: SecureRandom.uuid, iat: Time.now.to_i).to_jwt,
             token_type: "Bearer",
@@ -292,7 +347,7 @@ module Authz
 
     def get_authorize(request)
       template = <<~ERB
-        <!doctype html>
+        <!DOCTYPE html>
         <html>
           <head><title></title></head>
           <body>
@@ -449,6 +504,8 @@ if __FILE__ == $0
   app = Rack::Builder.new do
     use Rack::CommonLogger
     use Rack::Reloader
+    use Rack::Session::Cookie, { domain: $host.split(":", 2)[0], path: "/", secret: SecureRandom.hex(64) }
+
     map "/twirp" do
       # https://github.com/arthurnn/twirp-ruby/wiki/Service-Handlers
       run ::Authx::Rpc::AbilityService.new(::Authz::Rpc::Ability.new)
bin/ui
@@ -9,6 +9,7 @@ gemfile do
   gem "erb", "~> 4.0"
   gem "net-hippie", "~> 1.0"
   gem "rack", "~> 3.0"
+  gem "rack-session", "~> 2.0"
   gem "rackup", "~> 2.0"
   gem "saml-kit", "~> 1.0"
   gem "webrick", "~> 1.0"
@@ -137,6 +138,7 @@ class UI
     saml_response = saml_binding.deserialize(request.params)
     raise saml_response.errors unless saml_response.valid?
 
+    request.session[:access_token] = saml_response.attributes[:access_token]
     template = <<~ERB
       <!doctype html>
       <html>
@@ -157,6 +159,8 @@ if __FILE__ == $0
   app = Rack::Builder.new do
     use Rack::CommonLogger
     use Rack::Reloader
+    use Rack::Session::Cookie, { domain: $host.split(":", 2)[0], path: "/", secret: SecureRandom.hex(64) }
+
     run UI.new
   end.to_app