Skip to content

Commit

Permalink
Implement level of authentication (#510)
Browse files Browse the repository at this point in the history
  • Loading branch information
p53 authored Oct 22, 2024
1 parent 6a5b023 commit a40f985
Show file tree
Hide file tree
Showing 13 changed files with 844 additions and 183 deletions.
238 changes: 230 additions & 8 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import (
"time"

"github.com/PuerkitoBio/goquery"
"github.com/go-jose/go-jose/v4/jwt"
. "github.com/onsi/ginkgo/v2" //nolint:revive //we want to use it for ginkgo
. "github.com/onsi/gomega" //nolint:revive //we want to use it for gomega
"github.com/pquerna/otp/totp"
"golang.org/x/oauth2/clientcredentials"

resty "github.com/go-resty/resty/v2"
"github.com/gogatekeeper/gatekeeper/pkg/constant"
keycloakcore "github.com/gogatekeeper/gatekeeper/pkg/keycloak/proxy/core"
"github.com/gogatekeeper/gatekeeper/pkg/proxy"
"github.com/gogatekeeper/gatekeeper/pkg/proxy/models"
"github.com/gogatekeeper/gatekeeper/pkg/testsuite"
)

Expand All @@ -34,6 +37,8 @@ const (
pkceTestClientSecret = "F2GqU40xwX0P2LrTvHUHqwNoSk4U4n5R"
umaTestClient = "test-client-uma"
umaTestClientSecret = "A5vokiGdI3H2r4aXFrANbKvn4R7cbf6P"
loaTestClient = "test-loa"
loaTestClientSecret = "4z9PoOooXNFmSCPZx0xHXaUxX4eYGFO0"
timeout = time.Second * 300
idpURI = "http://localhost:8081"
localURI = "http://localhost:"
Expand All @@ -42,12 +47,19 @@ const (
anyURI = "/any"
testUser = "myuser"
testPass = "baba1234"
testLoAUser = "myloa"
testLoAPass = "baba5678"
testPath = "/test"
umaAllowedPath = "/pets"
umaForbiddenPath = "/pets/1"
umaNonExistentPath = "/cat"
umaMethodAllowedPath = "/horse"
umaFwdMethodAllowedPath = "/turtle"
loaPath = "/level"
loaStepUpPath = "/level2"
loaDefaultLevel = "level1"
loaStepUpLevel = "level2"
otpSecret = "NE4VKZJYKVDDSYTIK5CVOOLVOFDFE2DC"
postLoginRedirectPath = "/post/login/path"
pkceCookieName = "TESTPKCECOOKIE"
)
Expand Down Expand Up @@ -79,7 +91,13 @@ func startAndWait(portNum string, osArgs []string) {
}, timeout, 15*time.Second).Should(Succeed())
}

func codeFlowLogin(client *resty.Client, reqAddress string, expStatusCode int) *resty.Response {
func codeFlowLogin(
client *resty.Client,
reqAddress string,
expStatusCode int,
userName string,
userPass string,
) *resty.Response {
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(5))
resp, err := client.R().Get(reqAddress)
Expect(err).NotTo(HaveOccurred())
Expand All @@ -95,8 +113,8 @@ func codeFlowLogin(client *resty.Client, reqAddress string, expStatusCode int) *
action, exists := s.Attr("action")
Expect(exists).To(BeTrue())

client.FormData.Add("username", testUser)
client.FormData.Add("password", testPass)
client.FormData.Add("username", userName)
client.FormData.Add("password", userPass)
resp, err = client.R().Post(action)

Expect(err).NotTo(HaveOccurred())
Expand Down Expand Up @@ -205,7 +223,7 @@ var _ = Describe("Code Flow login/logout", func() {
func(_ context.Context) {
var err error
rClient := resty.New()
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK)
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testUser, testPass)
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
body := resp.Body()
Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue())
Expand Down Expand Up @@ -269,7 +287,7 @@ var _ = Describe("Code Flow login/logout", func() {
func(_ context.Context) {
var err error
rClient := resty.New()
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK)
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testUser, testPass)
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
body := resp.Body()
Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue())
Expand Down Expand Up @@ -342,7 +360,7 @@ var _ = Describe("Code Flow PKCE login/logout", func() {
func(_ context.Context) {
var err error
rClient := resty.New()
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK)
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testUser, testPass)
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))

body := resp.Body()
Expand Down Expand Up @@ -423,9 +441,9 @@ var _ = Describe("Code Flow login/logout with session check", func() {
It("should logout on both successfully", func(_ context.Context) {
var err error
rClient := resty.New()
resp := codeFlowLogin(rClient, proxyAddressFirst, http.StatusOK)
resp := codeFlowLogin(rClient, proxyAddressFirst, http.StatusOK, testUser, testPass)
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
resp = codeFlowLogin(rClient, proxyAddressSec, http.StatusOK)
resp = codeFlowLogin(rClient, proxyAddressSec, http.StatusOK, testUser, testPass)
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))

resp, err = rClient.R().Get(proxyAddressFirst + testPath)
Expand Down Expand Up @@ -456,3 +474,207 @@ var _ = Describe("Code Flow login/logout with session check", func() {
})
})
})

var _ = Describe("Level Of Authentication Code Flow login/logout", func() {
var portNum string
var proxyAddress string

BeforeEach(func() {
server := httptest.NewServer(&testsuite.FakeUpstreamService{})
portNum = generateRandomPort()
proxyAddress = localURI + portNum

osArgs := []string{os.Args[0]}
proxyArgs := []string{
"--discovery-url=" + idpRealmURI,
"--openid-provider-timeout=120s",
"--listen=" + allInterfaces + portNum,
"--client-id=" + loaTestClient,
"--client-secret=" + loaTestClientSecret,
"--upstream-url=" + server.URL,
"--no-redirects=false",
"--skip-access-token-clientid-check=true",
"--skip-access-token-issuer-check=true",
"--enable-idp-session-check=false",
"--enable-default-deny=true",
"--enable-loa=true",
"--verbose=true",
"--resources=uri=" + loaPath + "|acr=level1,level2",
"--resources=uri=" + loaStepUpPath + "|acr=level2",
"--openid-provider-retry-count=30",
"--enable-refresh-tokens=true",
"--encryption-key=sdkljfalisujeoir",
"--secure-cookie=false",
"--post-login-redirect-path=" + postLoginRedirectPath,
}

osArgs = append(osArgs, proxyArgs...)
startAndWait(portNum, osArgs)
})

When("Performing standard loa login", func() {
It("should login with loa level1=user/password and logout successfully",
Label("code_flow"),
Label("basic_case"),
Label("loa"),
func(_ context.Context) {
var err error
rClient := resty.New()
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testLoAUser, testLoAPass)
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
body := resp.Body()
Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue())
jarURI, err := url.Parse(proxyAddress)
Expect(err).NotTo(HaveOccurred())
cookiesLogin := rClient.GetClient().Jar.Cookies(jarURI)

var accessCookieLogin string
for _, cook := range cookiesLogin {
if cook.Name == constant.AccessCookie {
accessCookieLogin = cook.Value
}
}

By("wait for access token expiration")
time.Sleep(32 * time.Second)
resp, err = rClient.R().Get(proxyAddress + anyURI)
Expect(err).NotTo(HaveOccurred())
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
body = resp.Body()
Expect(strings.Contains(string(body), anyURI)).To(BeTrue())
Expect(resp.StatusCode()).To(Equal(http.StatusOK))
Expect(err).NotTo(HaveOccurred())
cookiesAfterRefresh := rClient.GetClient().Jar.Cookies(jarURI)

var accessCookieAfterRefresh string
for _, cook := range cookiesAfterRefresh {
if cook.Name == constant.AccessCookie {
accessCookieLogin = cook.Value
}
}

By("check if access token cookie has changed")
Expect(accessCookieLogin).NotTo(Equal(accessCookieAfterRefresh))

By("make another request with new access token")
resp, err = rClient.R().Get(proxyAddress + anyURI)
Expect(err).NotTo(HaveOccurred())
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
body = resp.Body()
Expect(strings.Contains(string(body), anyURI)).To(BeTrue())
Expect(resp.StatusCode()).To(Equal(http.StatusOK))

By("verify access token contains default acr value")
token, err := jwt.ParseSigned(accessCookieLogin, constant.SignatureAlgs[:])
Expect(err).NotTo(HaveOccurred())
customClaims := models.CustClaims{}

err = token.UnsafeClaimsWithoutVerification(&customClaims)
Expect(err).NotTo(HaveOccurred())
Expect(customClaims.Acr).To(Equal(loaDefaultLevel))

By("log out")
resp, err = rClient.R().Get(proxyAddress + logoutURI)
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode()).To(Equal(http.StatusOK))

rClient.SetRedirectPolicy(resty.NoRedirectPolicy())
resp, _ = rClient.R().Get(proxyAddress)
Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther))
},
)
})

When("Performing step up loa login", func() {
It("should login with loa level2=user/password and logout successfully",
Label("code_flow"),
Label("basic_case"),
Label("loa"),
func(_ context.Context) {
var err error
rClient := resty.New()
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testLoAUser, testLoAPass)
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
body := resp.Body()
Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue())
jarURI, err := url.Parse(proxyAddress)
Expect(err).NotTo(HaveOccurred())
cookiesLogin := rClient.GetClient().Jar.Cookies(jarURI)

var accessCookieLogin string
for _, cook := range cookiesLogin {
if cook.Name == constant.AccessCookie {
accessCookieLogin = cook.Value
}
}

By("verify access token contains default acr value")
token, err := jwt.ParseSigned(accessCookieLogin, constant.SignatureAlgs[:])
Expect(err).NotTo(HaveOccurred())
customClaims := models.CustClaims{}

err = token.UnsafeClaimsWithoutVerification(&customClaims)
Expect(err).NotTo(HaveOccurred())
Expect(customClaims.Acr).To(Equal(loaDefaultLevel))

By("make step up request")
resp, err = rClient.R().Get(proxyAddress + loaStepUpPath)
Expect(err).NotTo(HaveOccurred())
body = resp.Body()

doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))
Expect(err).NotTo(HaveOccurred())

selection := doc.Find("#kc-otp-login-form")
Expect(selection).ToNot(BeNil())
Expect(selection.Nodes).ToNot(BeEmpty())

selection.Each(func(_ int, s *goquery.Selection) {
action, exists := s.Attr("action")
Expect(exists).To(BeTrue())

otp, errOtp := totp.GenerateCode(otpSecret, time.Now().UTC())
Expect(errOtp).NotTo(HaveOccurred())
rClient.FormData.Del("username")
rClient.FormData.Del("password")
rClient.FormData.Set("otp", otp)
rClient.SetRedirectPolicy(resty.FlexibleRedirectPolicy(2))
rClient.SetBaseURL(proxyAddress)
resp, err = rClient.R().Post(action)
loc := resp.Header().Get("Location")

resp, err = rClient.R().Get(loc)
Expect(err).NotTo(HaveOccurred())
Expect(strings.Contains(string(resp.Body()), loaStepUpPath)).To(BeTrue())
Expect(resp.StatusCode()).To(Equal(http.StatusOK))

By("verify access token contains raised acr value")
cookiesLogin := rClient.GetClient().Jar.Cookies(jarURI)

var accessCookieLogin string
for _, cook := range cookiesLogin {
if cook.Name == constant.AccessCookie {
accessCookieLogin = cook.Value
}
}
token, err = jwt.ParseSigned(accessCookieLogin, constant.SignatureAlgs[:])
Expect(err).NotTo(HaveOccurred())
customClaims := models.CustClaims{}

err = token.UnsafeClaimsWithoutVerification(&customClaims)
Expect(err).NotTo(HaveOccurred())
Expect(customClaims.Acr).To(Equal(loaStepUpLevel))
})

By("log out")
resp, err = rClient.R().Get(proxyAddress + logoutURI)
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode()).To(Equal(http.StatusOK))

rClient.SetRedirectPolicy(resty.NoRedirectPolicy())
resp, _ = rClient.R().Get(proxyAddress)
Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther))
},
)
})
})
14 changes: 7 additions & 7 deletions e2e/e2e_uma_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var _ = Describe("UMA Code Flow authorization", func() {
It("should login with user/password and logout successfully", func(_ context.Context) {
var err error
rClient := resty.New()
resp := codeFlowLogin(rClient, proxyAddress+umaAllowedPath, http.StatusOK)
resp := codeFlowLogin(rClient, proxyAddress+umaAllowedPath, http.StatusOK, testUser, testPass)
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))

body := resp.Body()
Expand All @@ -76,7 +76,7 @@ var _ = Describe("UMA Code Flow authorization", func() {
When("Accessing resource, which does not exist", func() {
It("should be forbidden without permission ticket", func(_ context.Context) {
rClient := resty.New()
resp := codeFlowLogin(rClient, proxyAddress+umaNonExistentPath, http.StatusForbidden)
resp := codeFlowLogin(rClient, proxyAddress+umaNonExistentPath, http.StatusForbidden, testUser, testPass)

body := resp.Body()
Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse())
Expand All @@ -87,7 +87,7 @@ var _ = Describe("UMA Code Flow authorization", func() {
It("should be forbidden and then allowed", func(_ context.Context) {
var err error
rClient := resty.New()
resp := codeFlowLogin(rClient, proxyAddress+umaForbiddenPath, http.StatusForbidden)
resp := codeFlowLogin(rClient, proxyAddress+umaForbiddenPath, http.StatusForbidden, testUser, testPass)

body := resp.Body()
Expect(strings.Contains(string(body), umaCookieName)).To(BeFalse())
Expand Down Expand Up @@ -146,7 +146,7 @@ var _ = Describe("UMA Code Flow authorization with method scope", func() {
It("should login with user/password, don't access forbidden resource and logout successfully", func(_ context.Context) {
var err error
rClient := resty.New()
resp := codeFlowLogin(rClient, proxyAddress+umaMethodAllowedPath, http.StatusOK)
resp := codeFlowLogin(rClient, proxyAddress+umaMethodAllowedPath, http.StatusOK, testUser, testPass)
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))

body := resp.Body()
Expand Down Expand Up @@ -391,7 +391,7 @@ var _ = Describe("UMA Code Flow, NOPROXY authorization with method scope", func(
"X-Forwarded-URI": umaMethodAllowedPath,
"X-Forwarded-Method": "GET",
})
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK)
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testUser, testPass)
Expect(resp.Header().Get(constant.AuthorizationHeader)).ToNot(BeEmpty())

resp, err = rClient.R().Get(proxyAddress + logoutURI)
Expand All @@ -413,7 +413,7 @@ var _ = Describe("UMA Code Flow, NOPROXY authorization with method scope", func(
"X-Forwarded-URI": umaMethodAllowedPath,
"X-Forwarded-Method": "POST",
})
resp := codeFlowLogin(rClient, proxyAddress, http.StatusForbidden)
resp := codeFlowLogin(rClient, proxyAddress, http.StatusForbidden, testUser, testPass)
Expect(resp.Header().Get(constant.AuthorizationHeader)).To(BeEmpty())
})
})
Expand All @@ -426,7 +426,7 @@ var _ = Describe("UMA Code Flow, NOPROXY authorization with method scope", func(
"X-Forwarded-Host": strings.Split(proxyAddress, "//")[1],
"X-Forwarded-URI": umaMethodAllowedPath,
})
resp := codeFlowLogin(rClient, proxyAddress, http.StatusForbidden)
resp := codeFlowLogin(rClient, proxyAddress, http.StatusForbidden, testUser, testPass)
Expect(resp.Header().Get(constant.AuthorizationHeader)).To(BeEmpty())
})
})
Expand Down
Loading

0 comments on commit a40f985

Please sign in to comment.