Commit 68f6057

mo khan <mo@mokhan.ca>
2025-02-28 22:23:06
feat: implement more of the OIDC workflow
1 parent f6f82b7
Changed files (2)
bin/idp
@@ -50,10 +50,49 @@ class IdentityProvider
     @storage = {}
   end
 
+  def call(env)
+    path = env['PATH_INFO']
+    case env['REQUEST_METHOD']
+    when 'GET'
+      case path
+      when '/.well-known/openid-configuration'
+        return openid_metadata
+      when '/.well-known/oauth-authorization-server'
+        return oauth_metadata
+      when '/.well-known/webfinger' # RFC-7033
+        return not_found
+      when "/metadata.xml"
+        return saml_metadata
+      when "/sessions/new"
+        return saml_post_back(Rack::Request.new(env))
+      when "/oauth/authorize" # RFC-6749
+        return get_authorize(Rack::Request.new(env))
+      else
+        return not_found
+      end
+    when 'POST'
+      case path
+      when "/sessions/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 not_found
+      when "/oauth/revoke" # RFC-7009
+        return not_found
+      else
+        return not_found
+      end
+    end
+    not_found
+  end
+
+  private
+
   # Download IDP Metadata
   #
   # GET /metadata.xml
-  def metadata
+  def saml_metadata
     xml = Saml::Kit::Metadata.build_xml do |builder|
       builder.contact_email = 'hi@example.com'
       builder.organization_name = "Acme, Inc"
@@ -96,17 +135,58 @@ class IdentityProvider
     })]]
   end
 
+  def get_authorize(request)
+    template = <<~ERB
+      <!doctype html>
+      <html>
+        <head><title></title></head>
+        <body>
+          <h2>Authorize?</h2>
+          <form action="/oauth/authorize" method="post">
+            <input type="hidden" name="client_id" value="<%= request.params['client_id'] %>" />
+            <input type="hidden" name="scope" value="<%= request.params['scope'] %>" />
+            <input type="hidden" name="redirect_uri" value="<%= request.params['redirect_uri'] %>" />
+            <input type="hidden" name="response_mode" value="<%= request.params['response_mode'] %>" />
+            <input type="hidden" name="response_type" value="<%= request.params['response_type'] %>" />
+            <input type="hidden" name="state" value="<%= request.params['state'] %>" />
+            <input type="hidden" name="code_challenge_method" value="<%= request.params['code_challenge_method'] %>" />
+            <input type="hidden" name="code_challenge" value="<%= request.params['code_challenge'] %>" />
+            <input type="submit" value="Submit" />
+          </form>
+        </body>
+      </html>
+    ERB
+    html = ERB.new(template, trim_mode: '-').result(binding)
+    [200, { 'Content-Type' => "text/html" }, [html]]
+  end
+
+  def post_authorize(request)
+    params = request.params.slice('client_id', 'redirect_uri', 'response_type', 'response_mode', 'state', 'code_challenge_method', 'code_challenge', 'scope')
+    case params['response_type']
+    when 'code'
+      case params['response_mode']
+      when 'fragment'
+        return [302, { 'Location' => "#{params['redirect_uri']}#code=#{SecureRandom.uuid}&state=#{params['state']}" }, []]
+      when 'query'
+        return [302, { 'Location' => "#{params['redirect_uri']}?code=#{SecureRandom.uuid}&state=#{params['state']}" }, []]
+      else
+        # TODO:: form post
+      end
+
+    when 'token'
+      return not_found
+    else
+      return not_found
+    end
+  end
+
   # 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",
-      # token_endpoint_auth_methods_supported: [],
-      # token_endpoint_auth_signing_alg_values_supported: [],
       userinfo_endpoint: "http://localhost:8282/oidc/user/",
-      # check_session_iframe: nil,
-      # end_session_endpoint: nil,
       jwks_uri: "", # RFC-7517
       registration_endpoint: nil,
       scopes_supported: ["openid", "profile", "email"],
@@ -156,45 +236,7 @@ class IdentityProvider
     })]]
   end
 
-  # auth service
-  def call(env)
-    path = env['PATH_INFO']
-    case env['REQUEST_METHOD']
-    when 'GET'
-      case path
-      when '/.well-known/openid-configuration'
-        return openid_metadata
-      when '/.well-known/oauth-authorization-server'
-        return oauth_metadata
-      when '/.well-known/webfinger' # RFC-7033
-        return not_found
-      when "/metadata.xml"
-        return metadata
-      when "/sessions/new"
-        return post_back(Rack::Request.new(env))
-      when "oauth/authorize" # RFC-6749
-        return not_found
-      else
-        return not_found
-      end
-    when 'POST'
-      case path
-      when "/sessions/new"
-        return post_back(Rack::Request.new(env))
-      when "oauth/token" # RFC-6749
-        return not_found
-      when "oauth/revoke" # RFC-7009
-        return not_found
-      else
-        return not_found
-      end
-    end
-    not_found
-  end
-
-  private
-
-  def post_back(request)
+  def saml_post_back(request)
     params = saml_params_from(request)
     saml_request = binding_for(request).deserialize(params)
     @builder = nil
bin/sp
@@ -59,15 +59,17 @@ 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 "/oauth/callback"
+        return oauth_callback(Rack::Request.new(env))
       else
-        # TODO Generate a post to the IdP
-        return post_to_idp(Rack::Request.new(env))
+        return saml_post_to_idp(Rack::Request.new(env))
       end
     when 'POST'
       case path
       when "/assertions"
-        # TODO:: Render the SAMLResponse from the IdP
-        return assertions(Rack::Request.new(env))
+        return saml_assertions(Rack::Request.new(env))
       else
         return not_found
       end
@@ -81,7 +83,16 @@ class ServiceProvider
     [404, {}, []]
   end
 
-  def post_to_idp(request)
+  def redirect_to(location)
+    [302, { 'Location' => location }, []]
+  end
+
+  def oauth_callback(request)
+    # TODO:: Exchange grant (authorization_code) for an access token
+    [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate(request.params)]]
+  end
+
+  def saml_post_to_idp(request)
     idp = Saml::Kit.registry.metadata_for('http://localhost:8282/metadata.xml')
     relay_state = Base64.strict_encode64(JSON.generate(redirect_to: '/dashboard'))
 
@@ -107,12 +118,11 @@ class ServiceProvider
         </body>
       </html>
     ERB
-    erb = ERB.new(template, trim_mode: '-')
-    html = erb.result(binding)
+    html = ERB.new(template, trim_mode: '-').result(binding)
     [200, { 'Content-Type' => "text/html" }, [html]]
   end
 
-  def assertions(request)
+  def saml_assertions(request)
     sp = Saml::Kit.registry.metadata_for('http://localhost:8283/metadata.xml')
     saml_binding = sp.assertion_consumer_service_for(binding: :http_post)
     saml_response = saml_binding.deserialize(request.params)