Commit d1562ce

mo khan <mo@mokhan.ca>
2025-03-12 18:56:33
feat: add a basic login page and dashboard
1 parent be5f395
Changed files (1)
bin
bin/idp
@@ -28,6 +28,10 @@ $port = ENV.fetch("PORT", 8282).to_i
 $host = ENV.fetch("HOST", "localhost:#{$port}")
 
 module HTTPHelpers
+  def logged_in?(request)
+    request.session[:user_id]
+  end
+
   def default_headers
     {
       'X-Powered-By' => 'IdP'
@@ -41,6 +45,10 @@ module HTTPHelpers
   def http_ok(headers = {}, body = nil)
     [200, default_headers.merge(headers), [body]]
   end
+
+  def http_redirect_to(location)
+    [302, { 'Location' => location }, []]
+  end
 end
 
 module Authn
@@ -112,6 +120,57 @@ module Authn
     end
   end
 
+  class SessionsController
+    include ::HTTPHelpers
+
+    def call(env)
+      request = Rack::Request.new(env)
+      case request.request_method
+      when Rack::GET
+        case request.path
+        when '/sessions/new'
+          return get_login(request)
+        end
+      when Rack::POST
+        case request.path
+        when '/sessions'
+          if (user = User.find_by_credentials(request.params))
+            request.session[:user_id] = user[:id]
+            return http_redirect_to('/')
+          else
+            return http_redirect_to("/sessions/new")
+          end
+        when '/sessions/delete'
+          request.session.delete(:user_id)
+          return http_redirect_to('/')
+        end
+      end
+
+      http_not_found
+    end
+
+    private
+
+    def get_login(request)
+      template = <<~ERB
+        <!DOCTYPE html>
+        <html>
+          <head><title></title></head>
+          <body>
+            <form id="login-form" action="/sessions" method="post">
+              <input type="input" placeholder="Username" id="username" name="username" value="" />
+              <input type="password" placeholder="Password" id="password" name="password" value="" />
+              <input type="submit" id="login-button" value="Login" />
+            </form>
+          </body>
+        </html>
+      ERB
+      erb = ERB.new(template, trim_mode: '-')
+      html = erb.result(binding)
+      [200, { 'Content-Type' => "text/html" }, [html]]
+    end
+  end
+
   class SAMLController
     include ::HTTPHelpers
 
@@ -198,7 +257,7 @@ module Authn
     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
+      request.session[:user_id] = user[:id]
 
       @builder = nil
       url, saml_params = saml_request.response_for(
@@ -330,22 +389,25 @@ module Authz
     include ::HTTPHelpers
 
     def call(env)
-      path = env['PATH_INFO']
-      case env['REQUEST_METHOD']
-      when 'GET'
-        case path
+      request = Rack::Request.new(env)
+
+      case request.request_method
+      when Rack::GET
+        case request.path_info
         when "/authorize" # RFC-6749
-          return get_authorize(Rack::Request.new(env))
+          if logged_in?(request)
+            return get_authorize(request)
+          else
+            http_redirect_to("/saml/")
+          end
         else
           return http_not_found
         end
-      when 'POST'
-        case path
+      when Rack::POST
+        case request.path_info
         when "/authorize" # RFC-6749
-          return post_authorize(Rack::Request.new(env))
+          return post_authorize(request)
         when "/token" # RFC-6749
-          request = Rack::Request.new(env)
-
           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",
@@ -413,10 +475,17 @@ class IdentityProvider
   include ::HTTPHelpers
 
   def call(env)
-    path = env['PATH_INFO']
-    case env['REQUEST_METHOD']
-    when 'GET'
-      case path
+    request = Rack::Request.new(env)
+
+    case request.request_method
+    when Rack::GET
+      case request.path
+      when '/'
+        if logged_in?(request)
+          return get_dashboard(request)
+        else
+          return http_redirect_to("/sessions/new")
+        end
       when '/.well-known/openid-configuration'
         return openid_metadata
       when '/.well-known/oauth-authorization-server'
@@ -432,6 +501,26 @@ class IdentityProvider
 
   private
 
+  def get_dashboard(request)
+    template = <<~ERB
+      <!DOCTYPE html>
+      <html>
+        <head><title></title></head>
+        <body>
+          <h1>Dashboard</h1>
+          <pre><%= request.session[:access_token] %></pre>
+
+          <form action="/sessions/delete" method="post">
+            <input type="submit" value="logout" />
+          </form>
+        </body>
+      </html>
+    ERB
+    erb = ERB.new(template, trim_mode: '-')
+    html = erb.result(binding)
+    [200, { 'Content-Type' => "text/html" }, [html]]
+  end
+
   # GET /.well-known/oauth-authorization-server
   def oauth_metadata
     [200, { 'Content-Type' => "application/json" }, [JSON.pretty_generate({
@@ -534,6 +623,10 @@ if __FILE__ == $0
     map "/saml" do
       run Authn::SAMLController.new($scheme, $host)
     end
+
+    map "/sessions" do
+      run Authn::SessionsController.new
+    end
     run IdentityProvider.new
   end.to_app