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 +}