From c0418810d10e979db1d0eb726c37a0b648b7cf1b Mon Sep 17 00:00:00 2001
From: Julz Skupnjak <82210389+hcjulz@users.noreply.github.com>
Date: Wed, 3 Apr 2024 14:59:25 +0200
Subject: [PATCH] Add ability to the SAML test provider to create signed SAML
responses (#135)
* Update test responses
* Update test provider
* Use test provider for response tests
---
saml/response_test.go | 106 +++++++++++++++--------------
saml/test/provider.go | 155 ++++++++++++++++++++++++++++++++++++++++--
2 files changed, 202 insertions(+), 59 deletions(-)
diff --git a/saml/response_test.go b/saml/response_test.go
index 574b38b..840d886 100644
--- a/saml/response_test.go
+++ b/saml/response_test.go
@@ -9,13 +9,14 @@ import (
"testing"
"time"
- "github.com/hashicorp/cap/saml"
- "github.com/hashicorp/cap/saml/models/core"
- testprovider "github.com/hashicorp/cap/saml/test"
"github.com/jonboulle/clockwork"
saml2 "github.com/russellhaering/gosaml2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "github.com/hashicorp/cap/saml"
+ "github.com/hashicorp/cap/saml/models/core"
+ testprovider "github.com/hashicorp/cap/saml/test"
)
var testExpiredResp = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHNhbWwycDpSZXNwb25zZSBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo4MDAwL3NhbWwvYWNzIiBJRD0iXzg4NDljMmVlNTMyZmNkYjc4MWYyYTE3NzZlYWMzNzQxIiBJblJlc3BvbnNlVG89ImJjNWE1YmFhLTk0ZTAtNThhOC04NzJjLWU1MTQ5MWQyYjNlZSIgSXNzdWVJbnN0YW50PSIyMDIzLTA4LTI1VDE0OjMyOjUzLjY4MFoiIFZlcnNpb249IjIuMCIgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnhzZD0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiPjxzYW1sMjpJc3N1ZXIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHBzOi8vc2FtbHRlc3QuaWQvc2FtbC9pZHA8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNfODg0OWMyZWU1MzJmY2RiNzgxZjJhMTc3NmVhYzM3NDEiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPjxlYzpJbmNsdXNpdmVOYW1lc3BhY2VzIFByZWZpeExpc3Q9InhzZCIgeG1sbnM6ZWM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3JtPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPlJWNDg1dUtHSlptTkExbzU2Z3h4aytWWmt2eE1xdGxIWkEyaUhIOFpVMVE9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmQzTHBjNmhjU0I3YndDek1yTzN3ZlpyTmlHazVnWjhyS1JLT1FFTkRQMnErcDMrTGtEbVNCdDZ6enl4bjMzTUNTSnQrZFBIcEYxNFlNQUsvTjNQbld3U1NVcDBqNWt6T2M5S2E1TmRpYW5FME5nWW5VMHFqaEZKYlRoQVF6N2hSb3dTNEo0OWhTLzZNdVNRMFo3bkJCQ2VEZ2VENlBZUkFwS012bE90a0JHUEphTFQybVJ5L2duUStDQzZ1ZFVkSnl2U2diOW40M2x2eGRhYVpXckRLM1dnYTk4WWxrY1JITHJtUEFBTThLeFlXbmtvcGlvNllJTlU0RDVtWmpzRXNuVWtINDFXZ2N3Z21TMnh6UDNJQ25OYzNXSDlOSHJWS3A5YXQyREJ3cllESXNlczZGWGdZcStpVVdLMjE5MWpXcElDM3FWQUIwY09pbG1SWHd0RUg3Zz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlERWpDQ0FmcWdBd0lCQWdJVkFNRUNRMXRqZ2hhZm01T3hXRGg5aHdaZnh0aFdNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1CWXhGREFTCkJnTlZCQU1NQzNOaGJXeDBaWE4wTG1sa01CNFhEVEU0TURneU5ESXhNVFF3T1ZvWERUTTRNRGd5TkRJeE1UUXdPVm93RmpFVU1CSUcKQTFVRUF3d0xjMkZ0YkhSbGMzUXVhV1F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQzBaNFFYMU5GSwpzNzF1ZmJRd29Rb1c3cWtOQUpSSUFOR0E0aU0wVGhZZ2h1bDNwQytGd3JHdjM3YVR4V1hmQTFVRzluaktiYkRyZWlEQVpLbmdDZ3lqCnhqMHVKNGxBcmdrcjRBT0VqajV6WEE4MXVHSEFSZlVCY3R2UWNzWnBCSXhET3ZVVUltQWwrM05xTGdNR0YyZmt0eE1HN2tYM0dFVk4KYzFrbGJOM2RmWXNhdzVkVXJ3MjVEaGVMOW5wN0cvKzI4R3dIUHZMYjRhcHRPaU9OYkNhVnZoOVVNSEVBOUY3YzB6ZkYvY0w1Zk9wZApWYTU0d1RJMHUxMkNzRkt0NzhoNmxFR0c1alVzL3FYOWNsWm5jSk03RUZrTjNpbVBQeSswSEM4bnNwWGlIL01aVzhvMmNxV1JrcnczCk16QlpXM09qazVuUWo0MFY2TlViamI3a2ZlanpBZ01CQUFHalZ6QlZNQjBHQTFVZERnUVdCQlFUNlk5SjNUdy9oT0djOFBOVjdKRUUKNGsyWk5UQTBCZ05WSFJFRUxUQXJnZ3R6WVcxc2RHVnpkQzVwWklZY2FIUjBjSE02THk5ellXMXNkR1Z6ZEM1cFpDOXpZVzFzTDJsawpjREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBU2szZ3VLZlRrVmhFYUlWdnhFUE5SMnczdld0M2Z3bXdKQ2NjVzk4WFhMV2dOYnUzCllhTWIyUlNuN1RoNHAzaCttZnlrMmRvbjZhdTdVeXpjMUpkMzlSTnY4MFRHNWlRb3hmQ2dwaHkxRlltbWRhU2ZPOHd2RHRIVFROaUwKQXJBeE9ZdHpmWWJ6YjVRck5OSC9nUUVOOFJKYUVmL2cvMUdUdzl4LzEwM2RTTUswUlh0bCtmUnMybmJsRDFKSktTUTNBZGh4Sy93ZQpQM2FVUHRMeFZWSjl3TU9RT2ZjeTAybCtoSE1iNnVBanNQT3BPVktxaTNNOFhtY1VaT3B4NHN3dGdHZGVvU3BlUnlydE12UndkY2NpCk5CcDlVWm9tZTQ0cVpBWUgxaXFycG1tanNmSTlwSkl0c2dXdTNrWFBqaFNmajFBSkdSMWw5Skd2SnJIa2kxaUhUQT09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWwycDpTdGF0dXM+PHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM+PHNhbWwyOkFzc2VydGlvbiBJRD0iXzM1ZWE5MGI3MTFkNmYzODUzNDVmMGRiZGQ3ZDBlZDViIiBJc3N1ZUluc3RhbnQ9IjIwMjMtMDgtMjVUMTQ6MzI6NTMuNjgwWiIgVmVyc2lvbj0iMi4wIiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOklzc3Vlcj5odHRwczovL3NhbWx0ZXN0LmlkL3NhbWwvaWRwPC9zYW1sMjpJc3N1ZXI+PHNhbWwyOlN1YmplY3Q+PHNhbWwyOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyIgTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcCIgU1BOYW1lUXVhbGlmaWVyPSJodHRwOi8vc2FtbC5qdWx6L2V4YW1wbGUiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5tc21pdGhAc2FtbHRlc3QuaWQ8L3NhbWwyOk5hbWVJRD48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBBZGRyZXNzPSIxMDQuMjguMzkuMzQiIEluUmVzcG9uc2VUbz0iYmM1YTViYWEtOTRlMC01OGE4LTg3MmMtZTUxNDkxZDJiM2VlIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjVUMTQ6Mzc6NTMuNjkzWiIgUmVjaXBpZW50PSJodHRwOi8vbG9jYWxob3N0OjgwMDAvc2FtbC9hY3MiLz48L3NhbWwyOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sMjpTdWJqZWN0PjxzYW1sMjpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAyMy0wOC0yNVQxNDozMjo1My42ODBaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjVUMTQ6Mzc6NTMuNjgwWiI+PHNhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWwyOkF1ZGllbmNlPmh0dHA6Ly9zYW1sLmp1bHovZXhhbXBsZTwvc2FtbDI6QXVkaWVuY2U+PC9zYW1sMjpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDI6Q29uZGl0aW9ucz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDIzLTA4LTI1VDE0OjMxOjU2LjA2NFoiIFNlc3Npb25JbmRleD0iX2Y3MmE2M2VlMzc4MmI0N2M4OWY2MGU4MWFkZGUwYWIwIj48c2FtbDI6U3ViamVjdExvY2FsaXR5IEFkZHJlc3M9IjEwNC4yOC4zOS4zNCIvPjxzYW1sMjpBdXRobkNvbnRleHQ+PHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWwyOkF1dGhuQ29udGV4dD48L3NhbWwyOkF1dGhuU3RhdGVtZW50PjxzYW1sMjpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWwyOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9ImVkdVBlcnNvbkVudGl0bGVtZW50IiBOYW1lPSJ1cm46b2lkOjEuMy42LjEuNC4xLjU5MjMuMS4xLjEuNyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZT5BbWJhc3NhZG9yPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48c2FtbDI6QXR0cmlidXRlVmFsdWU+Tm9uZTwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDphdHRyaWJ1dGU6c3ViamVjdC1pZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHNkOnN0cmluZyI+bXNtaXRoQHNhbWx0ZXN0LmlkPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0idWlkIiBOYW1lPSJ1cm46b2lkOjAuOS4yMzQyLjE5MjAwMzAwLjEwMC4xLjEiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWU+bW9ydHk8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjxzYW1sMjpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJ0ZWxlcGhvbmVOdW1iZXIiIE5hbWU9InVybjpvaWQ6Mi41LjQuMjAiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWU+KzEtNTU1LTU1NS01NTA1PC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0icm9sZSIgTmFtZT0iaHR0cHM6Ly9zYW1sdGVzdC5pZC9hdHRyaWJ1dGVzL3JvbGUiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzZDpzdHJpbmciPmphbml0b3JAc2FtbHRlc3QuaWQ8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjxzYW1sMjpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJtYWlsIiBOYW1lPSJ1cm46b2lkOjAuOS4yMzQyLjE5MjAwMzAwLjEwMC4xLjMiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWU+bXNtaXRoQHNhbWx0ZXN0LmlkPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0ic24iIE5hbWU9InVybjpvaWQ6Mi41LjQuNCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1cmkiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZT5TbWl0aDwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9ImRpc3BsYXlOYW1lIiBOYW1lPSJ1cm46b2lkOjIuMTYuODQwLjEuMTEzNzMwLjMuMS4yNDEiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWU+TW9ydHkgU21pdGg8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjxzYW1sMjpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJnaXZlbk5hbWUiIE5hbWU9InVybjpvaWQ6Mi41LjQuNDIiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dXJpIj48c2FtbDI6QXR0cmlidXRlVmFsdWU+TW9ydGltZXI8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjwvc2FtbDI6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDI6QXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg==`
@@ -24,12 +25,16 @@ var testExpiredResp = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHNhb
func TestServiceProvider_ParseResponse(t *testing.T) {
t.Parallel()
const (
- testRequestId = "bc5a5baa-94e0-58a8-872c-e51491d2b3ee"
- testEntityID = "http://saml.julz/example"
- testAcs = "http://localhost:8000/saml/acs"
- metadataURL = "https://samltest.id/saml/idp"
+ testRequestId = "test-request-id"
+ testEntityID = "http://hashicorp-cap.test"
+ testAcs = "http://hashicorp-cap.test/saml/acs"
)
+ tp := testprovider.StartTestProvider(t)
+ defer tp.Close()
+
+ metadataURL := fmt.Sprintf("%s/saml/metadata", tp.ServerURL())
+
testCfg, err := saml.NewConfig(testEntityID, testAcs, metadataURL)
require.NoError(t, err)
testSp, err := saml.NewServiceProvider(testCfg)
@@ -38,7 +43,7 @@ func TestServiceProvider_ParseResponse(t *testing.T) {
fakeTime, err := time.Parse("2006-01-02 15:04:05", "2023-08-25 14:33:53")
require.NoError(t, err)
- testCfgWithBadMetadata, err := saml.NewConfig(testEntityID, testAcs, "https://samltest.id/saml/idp-invalid")
+ testCfgWithBadMetadata, err := saml.NewConfig(testEntityID, testAcs, fmt.Sprintf("%s/saml/invalid", tp.ServerURL()))
require.NoError(t, err)
testSpWithInvalidMetadataURL, err := saml.NewServiceProvider(testCfgWithBadMetadata)
require.NoError(t, err)
@@ -55,12 +60,10 @@ func TestServiceProvider_ParseResponse(t *testing.T) {
wantErrAs error
}{
{
- name: "success",
- sp: testSp,
- samlResp: testExpiredResp,
- opts: []saml.Option{
- saml.WithClock(clockwork.NewFakeClockAt(fakeTime)),
- },
+ name: "success",
+ sp: testSp,
+ samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t, testprovider.WithResponseSigned()))),
+ opts: []saml.Option{},
requestID: testRequestId,
},
{
@@ -139,19 +142,19 @@ func TestServiceProvider_ParseResponse(t *testing.T) {
wantErrContains: "unable to validate encoded response: illegal base64 data",
},
{
- name: "err-in-response-to",
- sp: testSp,
- samlResp: testExpiredResp,
- opts: []saml.Option{
- saml.WithClock(clockwork.NewFakeClockAt(fakeTime)),
- },
+ name: "err-in-response-to",
+ sp: testSp,
+ samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t, testprovider.WithResponseSigned()))),
requestID: "invalid-request-id",
wantErrContains: "doesn't match the expected requestID (invalid-request-id)",
},
{
- name: "expired",
- sp: testSp,
- samlResp: testExpiredResp,
+ name: "expired",
+ sp: testSp,
+ samlResp: base64.StdEncoding.EncodeToString([]byte(tp.SamlResponse(t,
+ testprovider.WithResponseSigned(),
+ testprovider.WithResponseExpired(),
+ ))),
requestID: "request-id",
wantErrAs: &saml2.ErrInvalidValue{},
wantErrContains: "unable to validate encoded response: Expired NotOnOrAfter value",
@@ -191,13 +194,13 @@ func TestServiceProvider_ParseResponse(t *testing.T) {
}
return
}
- require.NoError(err)
+ require.NoError(err, tc.name)
assert.Equal(testRequestId, got.InResponseTo)
- assert.Equal("http://localhost:8000/saml/acs", got.Destination)
+ assert.Equal("http://hashicorp-cap.test/saml/acs", got.Destination)
assert.Equal("urn:oasis:names:tc:SAML:2.0:status:Success", got.Status.StatusCode.Value)
- assert.Equal(metadataURL, got.Issuer())
- assert.Equal("msmith@samltest.id", got.Assertions()[0].Subject.NameID.Value)
- assert.Equal("_35ea90b711d6f385345f0dbdd7d0ed5b", got.Assertions()[0].ID)
+ assert.Equal("http://test.idp", got.Issuer())
+ assert.Equal("name-id", got.Assertions()[0].Subject.NameID.Value)
+ assert.Equal("assertion-id", got.Assertions()[0].ID)
})
}
}
@@ -213,7 +216,7 @@ func TestServiceProvider_ParseResponseCustomACS(t *testing.T) {
defer tp.Close()
cfg, err := saml.NewConfig(
- "http://test.me/entity",
+ "http://hashicorp-cap.test",
"http://test.me/saml/acs",
fmt.Sprintf("%s/saml/metadata", tp.ServerURL()),
)
@@ -288,7 +291,7 @@ const responseUnsigned = `
- http://test.me/entity
+ http://hashicorp-cap.test
@@ -313,11 +316,11 @@ const responseUnsigned = `
const testRespNoAssertions = `
-
https://samltest.id/saml/idp
+ xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://test.idp
@@ -363,11 +366,11 @@ NBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==
-
https://samltest.id/saml/idp
+ xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://test.idp
@@ -410,13 +413,13 @@ NBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==
- https://samltest.id/saml/idp
+ http://test.idp
msmith@samltest.id
-
+
@@ -470,11 +473,11 @@ NBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==
-
https://samltest.id/saml/idp
+ xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://test.idp
@@ -517,19 +520,19 @@ NBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==
- https://samltest.id/saml/idp
+ http://test.idp
msmith@samltest.id
-
+
- http://saml.julz/example
+ http://hashicorp-cap.test
@@ -578,11 +581,11 @@ NBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==
-
https://samltest.id/saml/idp
+ xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://test.idp
@@ -625,11 +628,11 @@ NBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==
- https://samltest.id/saml/idp
+ http://test.idp
- http://saml.julz/example
+ http://hashicorp-cap.test
@@ -678,11 +681,11 @@ NBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==
-
https://samltest.id/saml/idp
+ xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://test.idp
@@ -725,19 +728,18 @@ NBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu3kXPjhSfj1AJGR1l9JGvJrHki1iHTA==
- https://samltest.id/saml/idp
+ http://test.idp
- msmith@samltest.id
-
+
- .
- http://saml.julz/example
+ http://hashicorp-cap.test
diff --git a/saml/test/provider.go b/saml/test/provider.go
index b4b9d7f..025577d 100644
--- a/saml/test/provider.go
+++ b/saml/test/provider.go
@@ -19,6 +19,9 @@ import (
"testing"
"time"
+ "github.com/beevik/etree"
+ "github.com/russellhaering/gosaml2/types"
+ dsig "github.com/russellhaering/goxmldsig"
"github.com/stretchr/testify/require"
"github.com/hashicorp/cap/saml/models/core"
@@ -46,8 +49,8 @@ const meta = `
`
// From https://www.samltool.com/generic_sso_res.php
-const responseSigned = `
-
+const ResponseSigned = `
+
http://idp.example.com/metadata.php
@@ -118,8 +121,9 @@ func (s *SAMLResponsePostData) PostRequest(t *testing.T) *http.Request {
// TestProvider is an identity provider that can be used for testing
// SAML federeation and authentication flows.
type TestProvider struct {
- t *testing.T
- server *httptest.Server
+ t *testing.T
+ server *httptest.Server
+ keystore dsig.X509KeyStore
metadata *metadata.EntityDescriptorIDPSSO
recorder *httptest.ResponseRecorder
@@ -199,9 +203,18 @@ func StartTestProvider(t *testing.T) *TestProvider {
err := xml.Unmarshal([]byte(meta), &m)
r.NoError(err)
+ keystore := dsig.RandomKeyStoreForTest()
+ _, cert, err := keystore.GetKeyPair()
+ r.NoError(err)
+
+ b64Cert := base64.StdEncoding.EncodeToString(cert)
+
+ m.IDPSSODescriptor[0].RoleDescriptor.KeyDescriptor[0].KeyInfo.X509Data.X509Certificates[0].Data = b64Cert
+
provider := &TestProvider{
t: t,
metadata: &m,
+ keystore: keystore,
}
provider.defaults()
@@ -272,14 +285,13 @@ func (p *TestProvider) loginHandlerPost(w http.ResponseWriter, req *http.Request
relayState := req.FormValue("RelayState")
r.Equal(p.expectedRelayState, relayState, "relay state doesn't match")
- http.Error(w, "not implemented", http.StatusNotImplemented)
samlReq := p.parseRequestPost(rawReq)
p.validateRequest(samlReq)
samlResponseData := &SAMLResponsePostData{
- SAMLResponse: responseSigned,
+ SAMLResponse: ResponseSigned,
RelayState: relayState,
Destination: samlReq.AssertionConsumerServiceURL,
}
@@ -312,7 +324,7 @@ func (p *TestProvider) loginHandlerRedirect(w http.ResponseWriter, req *http.Req
p.validateRequest(samlReq)
samlResponseData := &SAMLResponsePostData{
- SAMLResponse: responseSigned,
+ SAMLResponse: ResponseSigned,
RelayState: relayState,
Destination: samlReq.AssertionConsumerServiceURL,
}
@@ -417,3 +429,132 @@ func (p *TestProvider) parseRequestPost(request string) *core.AuthnRequest {
return &req
}
+
+type responseOptions struct {
+ sign bool
+ expired bool
+}
+
+type ResponseOption func(*responseOptions)
+
+func getResponseOptions(opts ...ResponseOption) *responseOptions {
+ defaults := defaultResponseOptions()
+ for _, o := range opts {
+ o(defaults)
+ }
+
+ return defaults
+}
+
+func defaultResponseOptions() *responseOptions {
+ return &responseOptions{}
+}
+
+func WithResponseSigned() ResponseOption {
+ return func(o *responseOptions) {
+ o.sign = true
+ }
+}
+
+func WithResponseExpired() ResponseOption {
+ return func(o *responseOptions) {
+ o.expired = true
+ }
+}
+
+func (p *TestProvider) SamlResponse(t *testing.T, opts ...ResponseOption) string {
+ r := require.New(t)
+
+ opt := getResponseOptions(opts...)
+
+ notOnOrAfter := "2200-01-18T06:21:48Z"
+
+ if opt.expired {
+ notOnOrAfter = "2001-01-18T06:21:48Z"
+ }
+
+ response := &core.Response{
+ Response: types.Response{
+ Destination: "http://hashicorp-cap.test/saml/acs",
+ ID: "test-resp-id",
+ InResponseTo: "test-request-id",
+ IssueInstant: time.Now(),
+ Version: "2.0",
+ Issuer: &types.Issuer{
+ Value: "http://test.idp",
+ },
+ Status: &types.Status{
+ StatusCode: &types.StatusCode{
+ Value: string(core.StatusCodeSuccess),
+ },
+ },
+ Assertions: []types.Assertion{
+ {
+ ID: "assertion-id",
+ Issuer: &types.Issuer{
+ Value: "http://test.idp",
+ },
+ Subject: &types.Subject{
+ NameID: &types.NameID{
+ Value: "name-id",
+ },
+ SubjectConfirmation: &types.SubjectConfirmation{
+ Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
+ SubjectConfirmationData: &types.SubjectConfirmationData{
+ InResponseTo: "test-request-id",
+ Recipient: "http://hashicorp-cap.test/saml/acs",
+ NotOnOrAfter: notOnOrAfter,
+ },
+ },
+ },
+ Conditions: &types.Conditions{
+ NotBefore: "2001-01-18T06:21:48Z",
+ NotOnOrAfter: notOnOrAfter,
+ AudienceRestrictions: []types.AudienceRestriction{
+ {
+ Audiences: []types.Audience{
+ {Value: "http://hashicorp-cap.test"},
+ },
+ },
+ },
+ },
+ AttributeStatement: &types.AttributeStatement{
+ Attributes: []types.Attribute{
+ {
+ Name: "mail",
+ NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ Values: []types.AttributeValue{
+ {
+ Type: "xs:string",
+ Value: "user@hashicorp-cap.test",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ resp, err := xml.Marshal(response)
+ r.NoError(err)
+
+ doc := etree.NewDocument()
+ err = doc.ReadFromBytes(resp)
+ r.NoError(err)
+
+ if opt.sign {
+ signCtx := dsig.NewDefaultSigningContext(p.keystore)
+
+ signed, err := signCtx.SignEnveloped(doc.Root())
+ r.NoError(err)
+
+ doc.SetRoot(signed)
+ }
+
+ result, err := doc.WriteToString()
+ r.NoError(err)
+
+ return result
+}