From 0704a779e5a20611bb8ee685a0dcdc1bebe74ba9 Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 25 Mar 2025 09:51:32 -0600 Subject: feat: exchange saml assertion for an access token --- bin/idp | 33 ++++++++++++++++++++++++++++----- bin/ui | 1 + test/e2e_test.go | 15 +++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/bin/idp b/bin/idp index e93a14eb..9c2ff1df 100755 --- a/bin/idp +++ b/bin/idp @@ -99,8 +99,20 @@ module Authn end end - def find_by_username(username) + def find_by all.find do |user| + yield user + end + end + + def find_by_email(email) + find_by do |user| + user[:email] == email + end + end + + def find_by_username(username) + find_by do |user| user[:username] == username end end @@ -443,7 +455,7 @@ module Authz client_credentials_grant(params) when 'password' password_grant(params[:username], params[:password]) - when 'urn:ietf:params:oauth:grant-type:saml2-bearer' # RFC7522 + when "urn:ietf:params:oauth:grant-type:saml2-bearer" # RFC-7522 saml_assertion_grant(params[:assertion]) when 'urn:ietf:params:oauth:grant-type:jwt-bearer' # RFC7523 jwt_bearer_grant(params) @@ -469,8 +481,19 @@ module Authz raise NotImplementedError end - def saml_assertion_grant(saml_assertion) - raise NotImplementedError + 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 + + 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 + ::Authn::User.find(saml_assertion.name_id) + end + new(user, saml_assertion: saml_assertion) end def jwt_bearer_grant(params) @@ -517,7 +540,7 @@ module Authz expires_in: 3600, refresh_token: SecureRandom.hex(32) }.tap do |body| - if params['scope'].include?("openid") + if params["scope"]&.include?("openid") body[:id_token] = user.create_id_token.to_jwt end end diff --git a/bin/ui b/bin/ui index 894f9a6e..8dab9df1 100755 --- a/bin/ui +++ b/bin/ui @@ -388,6 +388,7 @@ class UI

Received SAML Response

+
<%= request.params["SAMLResponse"] %>
ERB diff --git a/test/e2e_test.go b/test/e2e_test.go index 40ed9439..19c09af7 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "encoding/base64" "net/http" "net/url" "strings" @@ -58,6 +59,20 @@ func TestAuthx(t *testing.T) { assert.Equal(t, "http://ui.example.com:8080/saml/assertions", action) assert.NoError(t, page.Locator("#submit-button").Click()) assert.Contains(t, x.Must(page.Content()), "Received SAML Response") + + t.Run("exchange SAML assertion for access token", func(t *testing.T) { + samlAssertion := x.Must(page.Locator("#saml-response").TextContent()) + io := bytes.NewBuffer(nil) + assert.NoError(t, serde.ToJSON(io, map[string]string{ + "assertion": samlAssertion, + "grant_type": "urn:ietf:params:oauth:grant-type:saml2-bearer", + })) + request := x.Must(http.NewRequestWithContext(t.Context(), "POST", "http://idp.example.com:8080/oauth/token", io)) + request.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("client_id:client_secret"))) + request.Header.Add("Content-Type", "application/json ") + response := x.Must(client.Do(request)) + require.Equal(t, http.StatusOK, response.StatusCode) + }) }) }) -- cgit v1.2.3