From 8b220be083178f0d0224bea958225e25303d4be8 Mon Sep 17 00:00:00 2001 From: p53 Date: Tue, 3 Oct 2023 23:41:01 +0200 Subject: [PATCH] Add e2e tests for no-proxy with UMA (#347) * Add e2e tests for no-proxy with UMA * Add post-login-path tests, session-check tests --- e2e/e2e_test.go | 323 ++++------------------ e2e/e2e_uma_test.go | 428 +++++++++++++++++++++++++++++ pkg/apperrors/apperrors.go | 59 ++-- pkg/keycloak/config/config.go | 3 + pkg/keycloak/config/config_test.go | 55 ++++ pkg/keycloak/proxy/middleware.go | 18 +- 6 files changed, 579 insertions(+), 307 deletions(-) create mode 100644 e2e/e2e_uma_test.go diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index f7f3ea9c..eb715615 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -35,11 +35,14 @@ const ( idpURI = "http://localhost:8081" testUser = "myuser" testPass = "baba1234" + testPath = "/test" umaAllowedPath = "/pets" umaForbiddenPath = "/pets/1" umaNonExistentPath = "/cat" umaMethodAllowedPath = "/horse" umaFwdMethodAllowedPath = "/turtle" + postLoginRedirectPath = "/post/login/path" + pkceCookieName = "TESTPKCECOOKIE" ) var idpRealmURI = fmt.Sprintf("%s/realms/%s", idpURI, testRealm) @@ -145,7 +148,7 @@ var _ = Describe("NoRedirects Simple login/logout", func() { }) }) -var _ = Describe("Code Flow Simple login/logout", func() { +var _ = Describe("Code Flow login/logout", func() { var portNum string var proxyAddress string @@ -167,6 +170,7 @@ var _ = Describe("Code Flow Simple login/logout", func() { "--skip-access-token-issuer-check=true", "--openid-provider-retry-count=30", "--secure-cookie=false", + "--post-login-redirect-path=" + postLoginRedirectPath, } osArgs = append(osArgs, proxyArgs...) @@ -178,6 +182,9 @@ var _ = Describe("Code Flow Simple login/logout", func() { rClient := resty.New() resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK) Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) + body := resp.Body() + Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue()) + resp, err = rClient.R().Get(proxyAddress + "/oauth/logout") Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode()).To(Equal(http.StatusOK)) @@ -191,7 +198,6 @@ var _ = Describe("Code Flow Simple login/logout", func() { var _ = Describe("Code Flow PKCE login/logout", func() { var portNum string var proxyAddress string - var pkceCookieName = "TESTPKCECOOKIE" BeforeEach(func() { server := httptest.NewServer(&testsuite.FakeUpstreamService{}) @@ -237,330 +243,97 @@ var _ = Describe("Code Flow PKCE login/logout", func() { }) }) -var _ = Describe("UMA Code Flow authorization", func() { +var _ = Describe("Code Flow login/logout with session check", func() { var portNum string - var proxyAddress string - var umaCookieName = "TESTUMACOOKIE" + var proxyAddressFirst string + var proxyAddressSec string BeforeEach(func() { server := httptest.NewServer(&testsuite.FakeUpstreamService{}) portNum = generateRandomPort() - proxyAddress = "http://localhost:" + portNum + proxyAddressFirst = "http://127.0.0.1:" + portNum + osArgs := []string{os.Args[0]} proxyArgs := []string{ "--discovery-url=" + idpRealmURI, "--openid-provider-timeout=120s", "--listen=" + "0.0.0.0:" + portNum, - "--client-id=" + umaTestClient, - "--client-secret=" + umaTestClientSecret, + "--client-id=" + testClient, + "--client-secret=" + testClientSecret, "--upstream-url=" + server.URL, "--no-redirects=false", - "--enable-uma=true", - "--cookie-uma-name=" + umaCookieName, "--skip-access-token-clientid-check=true", "--skip-access-token-issuer-check=true", "--openid-provider-retry-count=30", "--secure-cookie=false", + "--enable-idp-session-check=true", + "--enable-logout-redirect=true", + "--post-logout-redirect-uri=http://google.com", } osArgs = append(osArgs, proxyArgs...) startAndWait(portNum, osArgs) - }) - When("Accessing resource, where user is allowed to access", func() { - It("should login with user/password and logout successfully", func(ctx context.Context) { - var err error - rClient := resty.New() - resp := codeFlowLogin(rClient, proxyAddress+umaAllowedPath, http.StatusOK) - Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) - - body := resp.Body() - Expect(strings.Contains(string(body), umaCookieName)).To(BeTrue()) - - By("Accessing not allowed path") - resp, err = rClient.R().Get(proxyAddress + umaForbiddenPath) - body = resp.Body() - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) - Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse()) - - resp, err = rClient.R().Get(proxyAddress + "/oauth/logout") - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - - rClient.SetRedirectPolicy(resty.NoRedirectPolicy()) - resp, _ = rClient.R().Get(proxyAddress + umaAllowedPath) - Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther)) - }) - }) - - When("Accessing resource, which does not exist", func() { - It("should be forbidden without permission ticket", func(ctx context.Context) { - rClient := resty.New() - resp := codeFlowLogin(rClient, proxyAddress+umaNonExistentPath, http.StatusForbidden) - - body := resp.Body() - Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse()) - }) - }) - - When("Accessing resource, which exists but user is not allowed and then allowed resource", func() { - It("should be forbidden and then allowed", func(ctx context.Context) { - var err error - rClient := resty.New() - resp := codeFlowLogin(rClient, proxyAddress+umaForbiddenPath, http.StatusForbidden) - - body := resp.Body() - Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse()) - - By("Accessing allowed resource") - resp, err = rClient.R().Get(proxyAddress + umaAllowedPath) - body = resp.Body() - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse()) - - By("Accessing allowed resource one more time, checking uma cookie set") - resp, err = rClient.R().Get(proxyAddress + umaAllowedPath) - body = resp.Body() - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - Expect(strings.Contains(string(body), umaCookieName)).To(BeTrue()) - }) - }) -}) - -var _ = Describe("UMA Code Flow authorization with method scope", func() { - var portNum string - var proxyAddress string - var umaCookieName = "TESTUMACOOKIE" - - BeforeEach(func() { - server := httptest.NewServer(&testsuite.FakeUpstreamService{}) portNum = generateRandomPort() - proxyAddress = "http://localhost:" + portNum - osArgs := []string{os.Args[0]} - proxyArgs := []string{ + proxyAddressSec = "http://localhost:" + portNum + osArgs = []string{os.Args[0]} + proxyArgs = []string{ "--discovery-url=" + idpRealmURI, "--openid-provider-timeout=120s", "--listen=" + "0.0.0.0:" + portNum, - "--client-id=" + umaTestClient, - "--client-secret=" + umaTestClientSecret, + "--client-id=" + pkceTestClient, + "--client-secret=" + pkceTestClientSecret, "--upstream-url=" + server.URL, "--no-redirects=false", - "--enable-uma=true", - "--enable-uma-method-scope=true", - "--cookie-uma-name=" + umaCookieName, "--skip-access-token-clientid-check=true", "--skip-access-token-issuer-check=true", "--openid-provider-retry-count=30", "--secure-cookie=false", - "--verbose=true", - "--enable-logging=true", + "--enable-pkce=true", + "--cookie-pkce-name=" + pkceCookieName, + "--enable-idp-session-check=true", + "--enable-logout-redirect=true", + "--post-logout-redirect-uri=http://google.com", } osArgs = append(osArgs, proxyArgs...) startAndWait(portNum, osArgs) }) - When("Accessing resource, where user is allowed to access and then not allowed resource", func() { - It("should login with user/password, don't access forbidden resource and logout successfully", func(ctx context.Context) { + When("Login user with one browser client on two clients/app and logout on one of them", func() { + It("should logout on both successfully", func(ctx context.Context) { var err error rClient := resty.New() - resp := codeFlowLogin(rClient, proxyAddress+umaMethodAllowedPath, http.StatusOK) + resp := codeFlowLogin(rClient, proxyAddressFirst, http.StatusOK) + Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) + resp = codeFlowLogin(rClient, proxyAddressSec, http.StatusOK) Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) - body := resp.Body() - Expect(strings.Contains(string(body), umaCookieName)).To(BeTrue()) - - By("Accessing not allowed method") - resp, err = rClient.R().Post(proxyAddress + umaMethodAllowedPath) - body = resp.Body() - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) - Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse()) - - resp, err = rClient.R().Get(proxyAddress + "/oauth/logout") - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - - rClient.SetRedirectPolicy(resty.NoRedirectPolicy()) - resp, _ = rClient.R().Get(proxyAddress + umaAllowedPath) - Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther)) - }) - }) -}) - -var _ = Describe("UMA no-redirects authorization with forwarding client credentials grant", func() { - var portNum string - var proxyAddress string - var fwdPortNum string - var fwdProxyAddress string - - BeforeEach(func() { - server := httptest.NewServer(&testsuite.FakeUpstreamService{}) - portNum = generateRandomPort() - fwdPortNum = generateRandomPort() - proxyAddress = "http://localhost:" + portNum - fwdProxyAddress = "http://localhost:" + fwdPortNum - osArgs := []string{os.Args[0]} - fwdOsArgs := []string{os.Args[0]} - proxyArgs := []string{ - "--discovery-url=" + idpRealmURI, - "--openid-provider-timeout=120s", - "--listen=" + "0.0.0.0:" + portNum, - "--client-id=" + umaTestClient, - "--client-secret=" + umaTestClientSecret, - "--upstream-url=" + server.URL, - "--no-redirects=true", - "--enable-uma=true", - "--enable-uma-method-scope=true", - "--skip-access-token-clientid-check=true", - "--skip-access-token-issuer-check=true", - "--openid-provider-retry-count=30", - } - - fwdProxyArgs := []string{ - "--discovery-url=" + idpRealmURI, - "--openid-provider-timeout=120s", - "--listen=" + "0.0.0.0:" + fwdPortNum, - "--client-id=" + testClient, - "--client-secret=" + testClientSecret, - "--enable-uma=true", - "--enable-uma-method-scope=true", - "--enable-forwarding=true", - "--enable-authorization-header=true", - "--forwarding-grant-type=client_credentials", - "--skip-access-token-clientid-check=true", - "--skip-access-token-issuer-check=true", - "--openid-provider-retry-count=30", - } - - osArgs = append(osArgs, proxyArgs...) - startAndWait(portNum, osArgs) - fwdOsArgs = append(fwdOsArgs, fwdProxyArgs...) - startAndWait(fwdPortNum, fwdOsArgs) - }) - - When("Accessing resource, where user is allowed to access and then not allowed resource", func() { - It("should login with client secret, don't access forbidden resource", func(ctx context.Context) { - rClient := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()) - rClient.SetProxy(fwdProxyAddress) - resp, err := rClient.R().Get(proxyAddress + umaFwdMethodAllowedPath) + resp, err = rClient.R().Get(proxyAddressFirst + testPath) Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - body := resp.Body() - Expect(strings.Contains(string(body), umaFwdMethodAllowedPath)).To(BeTrue()) - - By("Accessing resource without access for client id") - resp, err = rClient.R().Get(proxyAddress + umaAllowedPath) - body = resp.Body() - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) - Expect(strings.Contains(string(body), umaAllowedPath)).To(BeFalse()) - - By("Accessing not allowed method") - resp, err = rClient.R().Post(proxyAddress + umaFwdMethodAllowedPath) - body = resp.Body() - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) - Expect(strings.Contains(string(body), umaFwdMethodAllowedPath)).To(BeFalse()) - }) - }) -}) - -var _ = Describe("UMA no-redirects authorization with forwarding direct access grant", func() { - var portNum string - var proxyAddress string - var fwdPortNum string - var fwdProxyAddress string - - BeforeEach(func() { - server := httptest.NewServer(&testsuite.FakeUpstreamService{}) - portNum = generateRandomPort() - fwdPortNum = generateRandomPort() - proxyAddress = "http://localhost:" + portNum - fwdProxyAddress = "http://localhost:" + fwdPortNum - osArgs := []string{os.Args[0]} - fwdOsArgs := []string{os.Args[0]} - proxyArgs := []string{ - "--discovery-url=" + idpRealmURI, - "--openid-provider-timeout=120s", - "--listen=" + "0.0.0.0:" + portNum, - "--client-id=" + umaTestClient, - "--client-secret=" + umaTestClientSecret, - "--upstream-url=" + server.URL, - "--no-redirects=true", - "--enable-uma=true", - "--enable-uma-method-scope=true", - "--skip-access-token-clientid-check=true", - "--skip-access-token-issuer-check=true", - "--openid-provider-retry-count=30", - } - - fwdProxyArgs := []string{ - "--discovery-url=" + idpRealmURI, - "--openid-provider-timeout=120s", - "--listen=" + "0.0.0.0:" + fwdPortNum, - "--client-id=" + testClient, - "--client-secret=" + testClientSecret, - "--forwarding-username=" + testUser, - "--forwarding-password=" + testPass, - "--enable-uma=true", - "--enable-uma-method-scope=true", - "--enable-forwarding=true", - "--enable-authorization-header=true", - "--skip-access-token-clientid-check=true", - "--skip-access-token-issuer-check=true", - "--openid-provider-retry-count=30", - } - - osArgs = append(osArgs, proxyArgs...) - startAndWait(portNum, osArgs) - fwdOsArgs = append(fwdOsArgs, fwdProxyArgs...) - startAndWait(fwdPortNum, fwdOsArgs) - }) + Expect(strings.Contains(string(body), testPath)).To(BeTrue()) - When("Accessing resource, where user is allowed to access and then not allowed resource", func() { - It("should login with user/password, don't access forbidden resource", func(ctx context.Context) { - rClient := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()) - rClient.SetProxy(fwdProxyAddress) - resp, err := rClient.R().Get(proxyAddress + umaMethodAllowedPath) + resp, err = rClient.R().Get(proxyAddressSec + testPath) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + body = resp.Body() + Expect(strings.Contains(string(body), testPath)).To(BeTrue()) - body := resp.Body() - Expect(strings.Contains(string(body), umaMethodAllowedPath)).To(BeTrue()) - Expect(resp.Header().Get(constant.UMAHeader)).NotTo(BeEmpty()) - - By("Repeating access to allowed resource, we verify that uma was saved and reused") - resp, err = rClient.R().Get(proxyAddress + umaMethodAllowedPath) + By("Logout user on first client") + resp, err = rClient.R().Get(proxyAddressFirst + "/oauth/logout") Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode()).To(Equal(http.StatusOK)) - body = resp.Body() - GinkgoLogr.Info(string(body)) - Expect(strings.Contains(string(body), umaMethodAllowedPath)).To(BeTrue()) - Expect(resp.Header().Get(constant.UMAHeader)).To(BeEmpty()) - // as first request should return uma token in header, it should be - // saved in forwarding rpt structure and sent also in this request - // so we should see it in response body - Expect(strings.Contains(string(body), constant.UMAHeader)).To(BeTrue()) - - By("Accessing resource without access for user") - resp, err = rClient.R().Get(proxyAddress + umaForbiddenPath) - body = resp.Body() - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) - Expect(strings.Contains(string(body), umaForbiddenPath)).To(BeFalse()) + By("Verify logged out on second client") + rClient.SetRedirectPolicy(resty.NoRedirectPolicy()) + resp, _ = rClient.R().Get(proxyAddressSec) + Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther)) - By("Accessing not allowed method") - resp, err = rClient.R().Post(proxyAddress + umaMethodAllowedPath) - body = resp.Body() - Expect(err).NotTo(HaveOccurred()) - Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) - Expect(strings.Contains(string(body), umaMethodAllowedPath)).To(BeFalse()) + By("Verify logged out on first client") + rClient.SetRedirectPolicy(resty.NoRedirectPolicy()) + resp, _ = rClient.R().Get(proxyAddressFirst) + Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther)) }) }) }) diff --git a/e2e/e2e_uma_test.go b/e2e/e2e_uma_test.go new file mode 100644 index 00000000..7707b618 --- /dev/null +++ b/e2e/e2e_uma_test.go @@ -0,0 +1,428 @@ +package e2e_test + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "strings" + + "github.com/gogatekeeper/gatekeeper/pkg/constant" + "github.com/gogatekeeper/gatekeeper/pkg/testsuite" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + resty "github.com/go-resty/resty/v2" +) + +var _ = Describe("UMA Code Flow authorization", func() { + var portNum string + var proxyAddress string + var umaCookieName = "TESTUMACOOKIE" + + BeforeEach(func() { + server := httptest.NewServer(&testsuite.FakeUpstreamService{}) + portNum = generateRandomPort() + proxyAddress = "http://localhost:" + portNum + osArgs := []string{os.Args[0]} + proxyArgs := []string{ + "--discovery-url=" + idpRealmURI, + "--openid-provider-timeout=120s", + "--listen=" + "0.0.0.0:" + portNum, + "--client-id=" + umaTestClient, + "--client-secret=" + umaTestClientSecret, + "--upstream-url=" + server.URL, + "--no-redirects=false", + "--enable-uma=true", + "--cookie-uma-name=" + umaCookieName, + "--skip-access-token-clientid-check=true", + "--skip-access-token-issuer-check=true", + "--openid-provider-retry-count=30", + "--secure-cookie=false", + } + + osArgs = append(osArgs, proxyArgs...) + startAndWait(portNum, osArgs) + }) + + When("Accessing resource, where user is allowed to access", func() { + It("should login with user/password and logout successfully", func(ctx context.Context) { + var err error + rClient := resty.New() + resp := codeFlowLogin(rClient, proxyAddress+umaAllowedPath, http.StatusOK) + Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) + + body := resp.Body() + Expect(strings.Contains(string(body), umaCookieName)).To(BeTrue()) + + By("Accessing not allowed path") + resp, err = rClient.R().Get(proxyAddress + umaForbiddenPath) + body = resp.Body() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) + Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse()) + + resp, err = rClient.R().Get(proxyAddress + "/oauth/logout") + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + + rClient.SetRedirectPolicy(resty.NoRedirectPolicy()) + resp, _ = rClient.R().Get(proxyAddress + umaAllowedPath) + Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther)) + }) + }) + + When("Accessing resource, which does not exist", func() { + It("should be forbidden without permission ticket", func(ctx context.Context) { + rClient := resty.New() + resp := codeFlowLogin(rClient, proxyAddress+umaNonExistentPath, http.StatusForbidden) + + body := resp.Body() + Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse()) + }) + }) + + When("Accessing resource, which exists but user is not allowed and then allowed resource", func() { + It("should be forbidden and then allowed", func(ctx context.Context) { + var err error + rClient := resty.New() + resp := codeFlowLogin(rClient, proxyAddress+umaForbiddenPath, http.StatusForbidden) + + body := resp.Body() + Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse()) + + By("Accessing allowed resource") + resp, err = rClient.R().Get(proxyAddress + umaAllowedPath) + body = resp.Body() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse()) + + By("Accessing allowed resource one more time, checking uma cookie set") + resp, err = rClient.R().Get(proxyAddress + umaAllowedPath) + body = resp.Body() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + Expect(strings.Contains(string(body), umaCookieName)).To(BeTrue()) + }) + }) +}) + +var _ = Describe("UMA Code Flow authorization with method scope", func() { + var portNum string + var proxyAddress string + var umaCookieName = "TESTUMACOOKIE" + + BeforeEach(func() { + server := httptest.NewServer(&testsuite.FakeUpstreamService{}) + portNum = generateRandomPort() + proxyAddress = "http://localhost:" + portNum + osArgs := []string{os.Args[0]} + proxyArgs := []string{ + "--discovery-url=" + idpRealmURI, + "--openid-provider-timeout=120s", + "--listen=" + "0.0.0.0:" + portNum, + "--client-id=" + umaTestClient, + "--client-secret=" + umaTestClientSecret, + "--upstream-url=" + server.URL, + "--no-redirects=false", + "--enable-uma=true", + "--enable-uma-method-scope=true", + "--cookie-uma-name=" + umaCookieName, + "--skip-access-token-clientid-check=true", + "--skip-access-token-issuer-check=true", + "--openid-provider-retry-count=30", + "--secure-cookie=false", + "--verbose=true", + "--enable-logging=true", + } + + osArgs = append(osArgs, proxyArgs...) + startAndWait(portNum, osArgs) + }) + + When("Accessing resource, where user is allowed to access and then not allowed resource", func() { + It("should login with user/password, don't access forbidden resource and logout successfully", func(ctx context.Context) { + var err error + rClient := resty.New() + resp := codeFlowLogin(rClient, proxyAddress+umaMethodAllowedPath, http.StatusOK) + Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true")) + + body := resp.Body() + Expect(strings.Contains(string(body), umaCookieName)).To(BeTrue()) + + By("Accessing not allowed method") + resp, err = rClient.R().Post(proxyAddress + umaMethodAllowedPath) + body = resp.Body() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) + Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse()) + + resp, err = rClient.R().Get(proxyAddress + "/oauth/logout") + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + + rClient.SetRedirectPolicy(resty.NoRedirectPolicy()) + resp, _ = rClient.R().Get(proxyAddress + umaAllowedPath) + Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther)) + }) + }) +}) + +var _ = Describe("UMA no-redirects authorization with forwarding client credentials grant", func() { + var portNum string + var proxyAddress string + var fwdPortNum string + var fwdProxyAddress string + + BeforeEach(func() { + server := httptest.NewServer(&testsuite.FakeUpstreamService{}) + portNum = generateRandomPort() + fwdPortNum = generateRandomPort() + proxyAddress = "http://localhost:" + portNum + fwdProxyAddress = "http://localhost:" + fwdPortNum + osArgs := []string{os.Args[0]} + fwdOsArgs := []string{os.Args[0]} + proxyArgs := []string{ + "--discovery-url=" + idpRealmURI, + "--openid-provider-timeout=120s", + "--listen=" + "0.0.0.0:" + portNum, + "--client-id=" + umaTestClient, + "--client-secret=" + umaTestClientSecret, + "--upstream-url=" + server.URL, + "--no-redirects=true", + "--enable-uma=true", + "--enable-uma-method-scope=true", + "--skip-access-token-clientid-check=true", + "--skip-access-token-issuer-check=true", + "--openid-provider-retry-count=30", + } + + fwdProxyArgs := []string{ + "--discovery-url=" + idpRealmURI, + "--openid-provider-timeout=120s", + "--listen=" + "0.0.0.0:" + fwdPortNum, + "--client-id=" + testClient, + "--client-secret=" + testClientSecret, + "--enable-uma=true", + "--enable-uma-method-scope=true", + "--enable-forwarding=true", + "--enable-authorization-header=true", + "--forwarding-grant-type=client_credentials", + "--skip-access-token-clientid-check=true", + "--skip-access-token-issuer-check=true", + "--openid-provider-retry-count=30", + } + + osArgs = append(osArgs, proxyArgs...) + startAndWait(portNum, osArgs) + fwdOsArgs = append(fwdOsArgs, fwdProxyArgs...) + startAndWait(fwdPortNum, fwdOsArgs) + }) + + When("Accessing resource, where user is allowed to access and then not allowed resource", func() { + It("should login with client secret, don't access forbidden resource", func(ctx context.Context) { + rClient := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()) + rClient.SetProxy(fwdProxyAddress) + resp, err := rClient.R().Get(proxyAddress + umaFwdMethodAllowedPath) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + + body := resp.Body() + Expect(strings.Contains(string(body), umaFwdMethodAllowedPath)).To(BeTrue()) + + By("Accessing resource without access for client id") + resp, err = rClient.R().Get(proxyAddress + umaAllowedPath) + body = resp.Body() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) + Expect(strings.Contains(string(body), umaAllowedPath)).To(BeFalse()) + + By("Accessing not allowed method") + resp, err = rClient.R().Post(proxyAddress + umaFwdMethodAllowedPath) + body = resp.Body() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) + Expect(strings.Contains(string(body), umaFwdMethodAllowedPath)).To(BeFalse()) + }) + }) +}) + +var _ = Describe("UMA no-redirects authorization with forwarding direct access grant", func() { + var portNum string + var proxyAddress string + var fwdPortNum string + var fwdProxyAddress string + + BeforeEach(func() { + server := httptest.NewServer(&testsuite.FakeUpstreamService{}) + portNum = generateRandomPort() + fwdPortNum = generateRandomPort() + proxyAddress = "http://localhost:" + portNum + fwdProxyAddress = "http://localhost:" + fwdPortNum + osArgs := []string{os.Args[0]} + fwdOsArgs := []string{os.Args[0]} + proxyArgs := []string{ + "--discovery-url=" + idpRealmURI, + "--openid-provider-timeout=120s", + "--listen=" + "0.0.0.0:" + portNum, + "--client-id=" + umaTestClient, + "--client-secret=" + umaTestClientSecret, + "--upstream-url=" + server.URL, + "--no-redirects=true", + "--enable-uma=true", + "--enable-uma-method-scope=true", + "--skip-access-token-clientid-check=true", + "--skip-access-token-issuer-check=true", + "--openid-provider-retry-count=30", + } + + fwdProxyArgs := []string{ + "--discovery-url=" + idpRealmURI, + "--openid-provider-timeout=120s", + "--listen=" + "0.0.0.0:" + fwdPortNum, + "--client-id=" + testClient, + "--client-secret=" + testClientSecret, + "--forwarding-username=" + testUser, + "--forwarding-password=" + testPass, + "--enable-uma=true", + "--enable-uma-method-scope=true", + "--enable-forwarding=true", + "--enable-authorization-header=true", + "--skip-access-token-clientid-check=true", + "--skip-access-token-issuer-check=true", + "--openid-provider-retry-count=30", + } + + osArgs = append(osArgs, proxyArgs...) + startAndWait(portNum, osArgs) + fwdOsArgs = append(fwdOsArgs, fwdProxyArgs...) + startAndWait(fwdPortNum, fwdOsArgs) + }) + + When("Accessing resource, where user is allowed to access and then not allowed resource", func() { + It("should login with user/password, don't access forbidden resource", func(ctx context.Context) { + rClient := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy()) + rClient.SetProxy(fwdProxyAddress) + resp, err := rClient.R().Get(proxyAddress + umaMethodAllowedPath) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + + body := resp.Body() + Expect(strings.Contains(string(body), umaMethodAllowedPath)).To(BeTrue()) + Expect(resp.Header().Get(constant.UMAHeader)).NotTo(BeEmpty()) + + By("Repeating access to allowed resource, we verify that uma was saved and reused") + resp, err = rClient.R().Get(proxyAddress + umaMethodAllowedPath) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + + body = resp.Body() + GinkgoLogr.Info(string(body)) + Expect(strings.Contains(string(body), umaMethodAllowedPath)).To(BeTrue()) + Expect(resp.Header().Get(constant.UMAHeader)).To(BeEmpty()) + // as first request should return uma token in header, it should be + // saved in forwarding rpt structure and sent also in this request + // so we should see it in response body + Expect(strings.Contains(string(body), constant.UMAHeader)).To(BeTrue()) + + By("Accessing resource without access for user") + resp, err = rClient.R().Get(proxyAddress + umaForbiddenPath) + body = resp.Body() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) + Expect(strings.Contains(string(body), umaForbiddenPath)).To(BeFalse()) + + By("Accessing not allowed method") + resp, err = rClient.R().Post(proxyAddress + umaMethodAllowedPath) + body = resp.Body() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusForbidden)) + Expect(strings.Contains(string(body), umaMethodAllowedPath)).To(BeFalse()) + }) + }) +}) + +var _ = Describe("UMA Code Flow, NOPROXY authorization with method scope", func() { + var portNum string + var proxyAddress string + var umaCookieName = "TESTUMACOOKIE" + // server := httptest.NewServer(&testsuite.FakeUpstreamService{}) + + BeforeEach(func() { + portNum = generateRandomPort() + proxyAddress = "http://localhost:" + portNum + osArgs := []string{os.Args[0]} + proxyArgs := []string{ + "--discovery-url=" + idpRealmURI, + "--openid-provider-timeout=120s", + "--listen=" + "0.0.0.0:" + portNum, + "--client-id=" + umaTestClient, + "--client-secret=" + umaTestClientSecret, + "--no-redirects=false", + "--enable-uma=true", + "--enable-uma-method-scope=true", + "--no-proxy=true", + "--cookie-uma-name=" + umaCookieName, + "--skip-access-token-clientid-check=true", + "--skip-access-token-issuer-check=true", + "--openid-provider-retry-count=30", + "--secure-cookie=false", + "--verbose=true", + "--enable-logging=true", + } + + osArgs = append(osArgs, proxyArgs...) + startAndWait(portNum, osArgs) + }) + + When("Accessing allowed resource", func() { + It("should be allowed and logout successfully", func(ctx context.Context) { + var err error + rClient := resty.New() + rClient.SetHeaders(map[string]string{ + "X-Forwarded-Proto": "http", + "X-Forwarded-Host": strings.Split(proxyAddress, "//")[1], + "X-Forwarded-URI": umaMethodAllowedPath, + "X-Forwarded-Method": "GET", + }) + resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK) + Expect(resp.Header().Get(constant.AuthorizationHeader)).ToNot(BeEmpty()) + + resp, err = rClient.R().Get(proxyAddress + "/oauth/logout") + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode()).To(Equal(http.StatusOK)) + + rClient.SetRedirectPolicy(resty.NoRedirectPolicy()) + resp, _ = rClient.R().Get(proxyAddress + umaAllowedPath) + Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther)) + }) + }) + + When("Accessing not allowed resource", func() { + It("should be forbidden", func(ctx context.Context) { + rClient := resty.New() + rClient.SetHeaders(map[string]string{ + "X-Forwarded-Proto": "http", + "X-Forwarded-Host": strings.Split(proxyAddress, "//")[1], + "X-Forwarded-URI": umaMethodAllowedPath, + "X-Forwarded-Method": "POST", + }) + resp := codeFlowLogin(rClient, proxyAddress, http.StatusForbidden) + Expect(resp.Header().Get(constant.AuthorizationHeader)).To(BeEmpty()) + }) + }) + + When("Accessing resource without X-Forwarded headers", func() { + It("should be forbidden", func(ctx context.Context) { + rClient := resty.New() + rClient.SetHeaders(map[string]string{ + "X-Forwarded-Proto": "http", + "X-Forwarded-Host": strings.Split(proxyAddress, "//")[1], + "X-Forwarded-URI": umaMethodAllowedPath, + }) + resp := codeFlowLogin(rClient, proxyAddress, http.StatusForbidden) + Expect(resp.Header().Get(constant.AuthorizationHeader)).To(BeEmpty()) + }) + }) +}) diff --git a/pkg/apperrors/apperrors.go b/pkg/apperrors/apperrors.go index f0021b59..0e8899c7 100644 --- a/pkg/apperrors/apperrors.go +++ b/pkg/apperrors/apperrors.go @@ -5,33 +5,34 @@ import ( ) var ( - ErrPermissionNotInToken = errors.New("permissions missing in token") - ErrResourceRetrieve = errors.New("problem getting resources from IDP") - ErrTokenScopeNotMatchResourceScope = errors.New("scopes in token doesn't match scopes in IDP resource") - ErrMissingScopesForResource = errors.New("missing scopes for resource in IDP provider") - ErrNoIDPResourceForPath = errors.New("could not find resource matching path") - ErrTooManyResources = errors.New("too many resources got from IDP (hint: probably you have multiple resources in IDP with same path and scopes combination)") - ErrResourceIDNotPresent = errors.New("resource id not present in token permissions") - ErrPermissionTicketForResourceID = errors.New("problem getting permission ticket for resourceId") - ErrRetrieveRPT = errors.New("problem getting RPT for resource (hint: do you have permissions assigned to resource?)") - ErrAccessMismatchUmaToken = errors.New("access token and uma token user ID don't match") - ErrNoAuthzFound = errors.New("no authz found") - ErrGetIdentityFromUMA = errors.New("problem getting identity from uma token") - ErrFailedAuthzRequest = errors.New("unexpected error occurred during authz request") - ErrSessionNotFound = errors.New("authentication session not found") - ErrNoSessionStateFound = errors.New("no session state found") - ErrZeroLengthToken = errors.New("token has zero length") - ErrInvalidSession = errors.New("invalid session identifier") - ErrRefreshTokenExpired = errors.New("refresh token has expired") - ErrUMATokenExpired = errors.New("uma token expired") - ErrTokenVerificationFailure = errors.New("token verification failed") - ErrDecryption = errors.New("failed to decrypt token") - ErrDefaultDenyWhitelistConflict = errors.New("you've asked for a default denial but whitelisted everything") - ErrDefaultDenyUserDefinedConflict = errors.New("you've enabled default deny and at the same time defined own rules for /*") - ErrBadDiscoveryURIFormat = errors.New("bad discovery url format") - ErrForwardAuthMissingHeaders = errors.New("seems you are using gatekeeper as forward-auth, but you don't forward X-FORWARDED-* headers from front proxy") - ErrPKCEWithCodeOnly = errors.New("pkce can be enabled only with no-redirect=false") - ErrPKCECodeCreation = errors.New("creation of code verifier failed") - ErrPKCECookieEmpty = errors.New("seems that pkce code verifier cookie value is empty string") - ErrInvalidPostLoginRedirectPath = errors.New("post login redirect path invalid, should be only path not absolute url (no hostname, scheme)") + ErrPermissionNotInToken = errors.New("permissions missing in token") + ErrResourceRetrieve = errors.New("problem getting resources from IDP") + ErrTokenScopeNotMatchResourceScope = errors.New("scopes in token doesn't match scopes in IDP resource") + ErrMissingScopesForResource = errors.New("missing scopes for resource in IDP provider") + ErrNoIDPResourceForPath = errors.New("could not find resource matching path") + ErrTooManyResources = errors.New("too many resources got from IDP (hint: probably you have multiple resources in IDP with same path and scopes combination)") + ErrResourceIDNotPresent = errors.New("resource id not present in token permissions") + ErrPermissionTicketForResourceID = errors.New("problem getting permission ticket for resourceId") + ErrRetrieveRPT = errors.New("problem getting RPT for resource (hint: do you have permissions assigned to resource?)") + ErrAccessMismatchUmaToken = errors.New("access token and uma token user ID don't match") + ErrNoAuthzFound = errors.New("no authz found") + ErrGetIdentityFromUMA = errors.New("problem getting identity from uma token") + ErrFailedAuthzRequest = errors.New("unexpected error occurred during authz request") + ErrSessionNotFound = errors.New("authentication session not found") + ErrNoSessionStateFound = errors.New("no session state found") + ErrZeroLengthToken = errors.New("token has zero length") + ErrInvalidSession = errors.New("invalid session identifier") + ErrRefreshTokenExpired = errors.New("refresh token has expired") + ErrUMATokenExpired = errors.New("uma token expired") + ErrTokenVerificationFailure = errors.New("token verification failed") + ErrDecryption = errors.New("failed to decrypt token") + ErrDefaultDenyWhitelistConflict = errors.New("you've asked for a default denial but whitelisted everything") + ErrDefaultDenyUserDefinedConflict = errors.New("you've enabled default deny and at the same time defined own rules for /*") + ErrBadDiscoveryURIFormat = errors.New("bad discovery url format") + ErrForwardAuthMissingHeaders = errors.New("seems you are using gatekeeper as forward-auth, but you don't forward X-FORWARDED-* headers from front proxy") + ErrPKCEWithCodeOnly = errors.New("pkce can be enabled only with no-redirect=false") + ErrPKCECodeCreation = errors.New("creation of code verifier failed") + ErrPKCECookieEmpty = errors.New("seems that pkce code verifier cookie value is empty string") + ErrInvalidPostLoginRedirectPath = errors.New("post login redirect path invalid, should be only path not absolute url (no hostname, scheme)") + ErrPostLoginRedirectPathNoRedirectsInvalid = errors.New("post login redirect path can be enabled only with no-redirect=false") ) diff --git a/pkg/keycloak/config/config.go b/pkg/keycloak/config/config.go index 2ce64db0..ea3ab85d 100644 --- a/pkg/keycloak/config/config.go +++ b/pkg/keycloak/config/config.go @@ -1000,6 +1000,9 @@ func (r *Config) isPKCEValid() error { } func (r *Config) isPostLoginRedirectValid() error { + if r.PostLoginRedirectPath != "" && r.NoRedirects { + return apperrors.ErrPostLoginRedirectPathNoRedirectsInvalid + } if r.PostLoginRedirectPath != "" { parsedURI, err := url.ParseRequestURI(r.PostLoginRedirectPath) if err != nil { diff --git a/pkg/keycloak/config/config_test.go b/pkg/keycloak/config/config_test.go index 70a04d58..9a46011a 100644 --- a/pkg/keycloak/config/config_test.go +++ b/pkg/keycloak/config/config_test.go @@ -2280,3 +2280,58 @@ func TestIsPKCEValid(t *testing.T) { ) } } + +func TestIsPostLoginRedirectValid(t *testing.T) { + testCases := []struct { + Name string + Config *Config + Valid bool + }{ + { + Name: "OK", + Config: &Config{ + PostLoginRedirectPath: "/some/path", + }, + Valid: true, + }, + { + Name: "OK complex URI", + Config: &Config{ + PostLoginRedirectPath: "/some/path?someparam=lala", + }, + Valid: true, + }, + { + Name: "InvalidPostLoginRedirectPath", + Config: &Config{ + PostLoginRedirectPath: "http://somehost/some/path", + }, + Valid: false, + }, + { + Name: "InvalidCombinationPostLoginRedirectPathWithNoRedirects", + Config: &Config{ + PostLoginRedirectPath: "/some/path", + NoRedirects: true, + }, + Valid: false, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run( + testCase.Name, + func(t *testing.T) { + err := testCase.Config.isPostLoginRedirectValid() + if err != nil && testCase.Valid { + t.Fatalf("Expected test not to fail") + } + + if err == nil && !testCase.Valid { + t.Fatalf("Expected test to fail") + } + }, + ) + } +} diff --git a/pkg/keycloak/proxy/middleware.go b/pkg/keycloak/proxy/middleware.go index bca85e26..c5455814 100644 --- a/pkg/keycloak/proxy/middleware.go +++ b/pkg/keycloak/proxy/middleware.go @@ -480,17 +480,29 @@ func (r *OauthProxy) authorizationMiddleware() func(http.Handler) http.Handler { if r.Config.EnableUma { var methodScope *string if r.Config.EnableUmaMethodScope { - ms := "method:" + req.Method + methSc := "method:" + req.Method if r.Config.NoProxy { xForwardedMethod := req.Header.Get("X-Forwarded-Method") - ms = "method:" + xForwardedMethod + if xForwardedMethod == "" { + scope.Logger.Error(apperrors.ErrForwardAuthMissingHeaders.Error()) + //nolint:contextcheck + next.ServeHTTP(wrt, req.WithContext(r.accessForbidden(wrt, req))) + return + } + methSc = "method:" + xForwardedMethod } - methodScope = &ms + methodScope = &methSc } authzPath := req.URL.Path if r.Config.NoProxy { authzPath = req.Header.Get("X-Forwarded-URI") + if authzPath == "" { + scope.Logger.Error(apperrors.ErrForwardAuthMissingHeaders.Error()) + //nolint:contextcheck + next.ServeHTTP(wrt, req.WithContext(r.accessForbidden(wrt, req))) + return + } } authzFunc := func(