Commit e758496

mo khan <mo@mokhan.ca>
2025-03-26 20:46:20
feat: exchange a SAML assertion for an access token
1 parent ead9021
Changed files (2)
bin/idp
@@ -5,6 +5,7 @@ require "bundler/inline"
 gemfile do
   source "https://rubygems.org"
 
+  gem "base64", "~> 0.1"
   gem "bcrypt", "~> 3.0"
   gem "csv", "~> 3.0"
   gem "declarative_policy", "~> 1.0"
@@ -14,7 +15,7 @@ gemfile do
   gem "rack", "~> 3.0"
   gem "rack-session", "~> 2.0"
   gem "rackup", "~> 2.0"
-  gem "saml-kit", "~> 1.0"
+  gem "saml-kit", "~> 1.0", git: "github.com:xlgmokha/saml-kit", branch: "main"
   gem "twirp", "~> 1.0"
   gem "warden", "~> 1.0"
   gem "webrick", "~> 1.0"
@@ -444,7 +445,6 @@ module Authz
         @all ||= []
       end
 
-      # TODO:: Look up saml_assertion
       def find_by(params)
         case params[:grant_type]
         when 'authorization_code'
@@ -483,16 +483,11 @@ module Authz
 
       def saml_assertion_grant(encoded_saml_assertion)
         xml = Base64.decode64(encoded_saml_assertion)
-        saml_response = Saml::Kit::Document.to_saml_document(xml)
-        saml_assertion = saml_response.assertion
         # TODO:: Validate signature and prevent assertion reuse
+        saml_assertion = Saml::Kit::Document.to_saml_document(xml)
 
-        user = case saml_assertion.name_id_format
-        when Saml::Kit::Namespaces::EMAIL_ADDRESS
-          ::Authn::User.find_by_email(saml_assertion.name_id)
-        when Saml::Kit::Namespaces::PERSISTENT
+        user = ::Authn::User.find_by_email(saml_assertion.name_id) ||
           ::Authn::User.find(saml_assertion.name_id)
-        end
         new(user, saml_assertion: saml_assertion)
       end
 
@@ -544,6 +539,11 @@ module Authz
           body[:id_token] = user.create_id_token.to_jwt
         end
       end
+    rescue StandardError => error
+      {
+        error: error.message,
+        error_description: error.backtrace,
+      }
     end
   end
 
@@ -582,7 +582,7 @@ module Authz
           params = request.content_type == "application/json" ? JSON.parse(request.body.read, symbolize_names: true) : Hash[URI.decode_www_form(request.body.read)].transform_keys(&:to_sym)
           grant = AuthorizationGrant.find_by(params)
 
-          return [404, { "Content-Type" => "application/json" }, [JSON.pretty_generate(error: 404, message: "Not Found")]] if grant.nil? || grant.inactive?
+          return [404, { "Content-Type" => "application/json" }, [JSON.pretty_generate(error: 404, error_description: "Not Found")]] if grant.nil? || grant.inactive?
           return [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate(grant.exchange)]]
         when "/oauth/revoke" # RFC-7009
           # TODO:: Revoke the JWT token and make it ineligible for usage
bin/ui
@@ -123,7 +123,7 @@ end
 
 module HTTPHelpers
   def current_user?(request)
-    request.session[:id_token]
+    request.session[:access_token]
   end
 
   def not_found
@@ -393,14 +393,14 @@ class UI
     saml_response = saml_binding.deserialize(request.params)
     raise saml_response.errors unless saml_response.valid?
 
+    assertion = Base64.strict_encode64(saml_response.assertion.to_xml)
     response = oauth_client.exchange(
       "urn:ietf:params:oauth:grant-type:saml2-bearer",
-      assertion: request.params["SAMLResponse"],
+      assertion: assertion,
     )
     if response.code == "200"
       tokens = JSON.parse(response.body, symbolize_names: true)
       request.session[:access_token] = tokens[:access_token]
-      request.session[:id_token] = tokens[:id_token]
       request.session[:refresh_token] = tokens[:access_token]
 
       template = <<~ERB
@@ -410,13 +410,14 @@ class UI
             <title></title>
           </head>
           <body style="background-color: pink;">
-            <h2>Received SAML Response</h2>
-            <textarea readonly="readonly" disabled="disabled" cols=220 rows=40><%=- saml_response.to_xml(pretty: true) -%></textarea>
-            <pre id="raw-saml-response"><%= request.params["SAMLResponse"] %></pre>
-            <pre id="access-token"><%= JSON.pretty_generate(request.session[:access_token]) %></pre>
-
             <a href="/index.html">Home</a>
             <a href="/groups.html">Groups</a>
+
+            <h2>Received SAML Response</h2>
+            <textarea readonly="readonly" disabled="disabled" cols=220 rows=40><%=- saml_response.to_xml(pretty: true) -%></textarea>
+            <pre id="raw-saml-response" style="display: none;"><%= request.params["SAMLResponse"] %></pre>
+            <pre id="xml-saml-assertion" style="display: none;"><%= saml_response.assertion.to_xml(pretty: true) %></pre>
+            <pre id="access-token" style="display: none;"><%= JSON.pretty_generate(request.session[:access_token]) %></pre>
           </body>
         </html>
       ERB