Skip to content

Commit a40f985

Browse files
authored
Implement level of authentication (#510)
1 parent 6a5b023 commit a40f985

File tree

13 files changed

+844
-183
lines changed

13 files changed

+844
-183
lines changed

e2e/e2e_test.go

Lines changed: 230 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@ import (
1515
"time"
1616

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

2224
resty "github.com/go-resty/resty/v2"
2325
"github.com/gogatekeeper/gatekeeper/pkg/constant"
2426
keycloakcore "github.com/gogatekeeper/gatekeeper/pkg/keycloak/proxy/core"
2527
"github.com/gogatekeeper/gatekeeper/pkg/proxy"
28+
"github.com/gogatekeeper/gatekeeper/pkg/proxy/models"
2629
"github.com/gogatekeeper/gatekeeper/pkg/testsuite"
2730
)
2831

@@ -34,6 +37,8 @@ const (
3437
pkceTestClientSecret = "F2GqU40xwX0P2LrTvHUHqwNoSk4U4n5R"
3538
umaTestClient = "test-client-uma"
3639
umaTestClientSecret = "A5vokiGdI3H2r4aXFrANbKvn4R7cbf6P"
40+
loaTestClient = "test-loa"
41+
loaTestClientSecret = "4z9PoOooXNFmSCPZx0xHXaUxX4eYGFO0"
3742
timeout = time.Second * 300
3843
idpURI = "http://localhost:8081"
3944
localURI = "http://localhost:"
@@ -42,12 +47,19 @@ const (
4247
anyURI = "/any"
4348
testUser = "myuser"
4449
testPass = "baba1234"
50+
testLoAUser = "myloa"
51+
testLoAPass = "baba5678"
4552
testPath = "/test"
4653
umaAllowedPath = "/pets"
4754
umaForbiddenPath = "/pets/1"
4855
umaNonExistentPath = "/cat"
4956
umaMethodAllowedPath = "/horse"
5057
umaFwdMethodAllowedPath = "/turtle"
58+
loaPath = "/level"
59+
loaStepUpPath = "/level2"
60+
loaDefaultLevel = "level1"
61+
loaStepUpLevel = "level2"
62+
otpSecret = "NE4VKZJYKVDDSYTIK5CVOOLVOFDFE2DC"
5163
postLoginRedirectPath = "/post/login/path"
5264
pkceCookieName = "TESTPKCECOOKIE"
5365
)
@@ -79,7 +91,13 @@ func startAndWait(portNum string, osArgs []string) {
7991
}, timeout, 15*time.Second).Should(Succeed())
8092
}
8193

82-
func codeFlowLogin(client *resty.Client, reqAddress string, expStatusCode int) *resty.Response {
94+
func codeFlowLogin(
95+
client *resty.Client,
96+
reqAddress string,
97+
expStatusCode int,
98+
userName string,
99+
userPass string,
100+
) *resty.Response {
83101
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(5))
84102
resp, err := client.R().Get(reqAddress)
85103
Expect(err).NotTo(HaveOccurred())
@@ -95,8 +113,8 @@ func codeFlowLogin(client *resty.Client, reqAddress string, expStatusCode int) *
95113
action, exists := s.Attr("action")
96114
Expect(exists).To(BeTrue())
97115

98-
client.FormData.Add("username", testUser)
99-
client.FormData.Add("password", testPass)
116+
client.FormData.Add("username", userName)
117+
client.FormData.Add("password", userPass)
100118
resp, err = client.R().Post(action)
101119

102120
Expect(err).NotTo(HaveOccurred())
@@ -205,7 +223,7 @@ var _ = Describe("Code Flow login/logout", func() {
205223
func(_ context.Context) {
206224
var err error
207225
rClient := resty.New()
208-
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK)
226+
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testUser, testPass)
209227
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
210228
body := resp.Body()
211229
Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue())
@@ -269,7 +287,7 @@ var _ = Describe("Code Flow login/logout", func() {
269287
func(_ context.Context) {
270288
var err error
271289
rClient := resty.New()
272-
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK)
290+
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testUser, testPass)
273291
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
274292
body := resp.Body()
275293
Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue())
@@ -342,7 +360,7 @@ var _ = Describe("Code Flow PKCE login/logout", func() {
342360
func(_ context.Context) {
343361
var err error
344362
rClient := resty.New()
345-
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK)
363+
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testUser, testPass)
346364
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
347365

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

431449
resp, err = rClient.R().Get(proxyAddressFirst + testPath)
@@ -456,3 +474,207 @@ var _ = Describe("Code Flow login/logout with session check", func() {
456474
})
457475
})
458476
})
477+
478+
var _ = Describe("Level Of Authentication Code Flow login/logout", func() {
479+
var portNum string
480+
var proxyAddress string
481+
482+
BeforeEach(func() {
483+
server := httptest.NewServer(&testsuite.FakeUpstreamService{})
484+
portNum = generateRandomPort()
485+
proxyAddress = localURI + portNum
486+
487+
osArgs := []string{os.Args[0]}
488+
proxyArgs := []string{
489+
"--discovery-url=" + idpRealmURI,
490+
"--openid-provider-timeout=120s",
491+
"--listen=" + allInterfaces + portNum,
492+
"--client-id=" + loaTestClient,
493+
"--client-secret=" + loaTestClientSecret,
494+
"--upstream-url=" + server.URL,
495+
"--no-redirects=false",
496+
"--skip-access-token-clientid-check=true",
497+
"--skip-access-token-issuer-check=true",
498+
"--enable-idp-session-check=false",
499+
"--enable-default-deny=true",
500+
"--enable-loa=true",
501+
"--verbose=true",
502+
"--resources=uri=" + loaPath + "|acr=level1,level2",
503+
"--resources=uri=" + loaStepUpPath + "|acr=level2",
504+
"--openid-provider-retry-count=30",
505+
"--enable-refresh-tokens=true",
506+
"--encryption-key=sdkljfalisujeoir",
507+
"--secure-cookie=false",
508+
"--post-login-redirect-path=" + postLoginRedirectPath,
509+
}
510+
511+
osArgs = append(osArgs, proxyArgs...)
512+
startAndWait(portNum, osArgs)
513+
})
514+
515+
When("Performing standard loa login", func() {
516+
It("should login with loa level1=user/password and logout successfully",
517+
Label("code_flow"),
518+
Label("basic_case"),
519+
Label("loa"),
520+
func(_ context.Context) {
521+
var err error
522+
rClient := resty.New()
523+
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testLoAUser, testLoAPass)
524+
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
525+
body := resp.Body()
526+
Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue())
527+
jarURI, err := url.Parse(proxyAddress)
528+
Expect(err).NotTo(HaveOccurred())
529+
cookiesLogin := rClient.GetClient().Jar.Cookies(jarURI)
530+
531+
var accessCookieLogin string
532+
for _, cook := range cookiesLogin {
533+
if cook.Name == constant.AccessCookie {
534+
accessCookieLogin = cook.Value
535+
}
536+
}
537+
538+
By("wait for access token expiration")
539+
time.Sleep(32 * time.Second)
540+
resp, err = rClient.R().Get(proxyAddress + anyURI)
541+
Expect(err).NotTo(HaveOccurred())
542+
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
543+
body = resp.Body()
544+
Expect(strings.Contains(string(body), anyURI)).To(BeTrue())
545+
Expect(resp.StatusCode()).To(Equal(http.StatusOK))
546+
Expect(err).NotTo(HaveOccurred())
547+
cookiesAfterRefresh := rClient.GetClient().Jar.Cookies(jarURI)
548+
549+
var accessCookieAfterRefresh string
550+
for _, cook := range cookiesAfterRefresh {
551+
if cook.Name == constant.AccessCookie {
552+
accessCookieLogin = cook.Value
553+
}
554+
}
555+
556+
By("check if access token cookie has changed")
557+
Expect(accessCookieLogin).NotTo(Equal(accessCookieAfterRefresh))
558+
559+
By("make another request with new access token")
560+
resp, err = rClient.R().Get(proxyAddress + anyURI)
561+
Expect(err).NotTo(HaveOccurred())
562+
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
563+
body = resp.Body()
564+
Expect(strings.Contains(string(body), anyURI)).To(BeTrue())
565+
Expect(resp.StatusCode()).To(Equal(http.StatusOK))
566+
567+
By("verify access token contains default acr value")
568+
token, err := jwt.ParseSigned(accessCookieLogin, constant.SignatureAlgs[:])
569+
Expect(err).NotTo(HaveOccurred())
570+
customClaims := models.CustClaims{}
571+
572+
err = token.UnsafeClaimsWithoutVerification(&customClaims)
573+
Expect(err).NotTo(HaveOccurred())
574+
Expect(customClaims.Acr).To(Equal(loaDefaultLevel))
575+
576+
By("log out")
577+
resp, err = rClient.R().Get(proxyAddress + logoutURI)
578+
Expect(err).NotTo(HaveOccurred())
579+
Expect(resp.StatusCode()).To(Equal(http.StatusOK))
580+
581+
rClient.SetRedirectPolicy(resty.NoRedirectPolicy())
582+
resp, _ = rClient.R().Get(proxyAddress)
583+
Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther))
584+
},
585+
)
586+
})
587+
588+
When("Performing step up loa login", func() {
589+
It("should login with loa level2=user/password and logout successfully",
590+
Label("code_flow"),
591+
Label("basic_case"),
592+
Label("loa"),
593+
func(_ context.Context) {
594+
var err error
595+
rClient := resty.New()
596+
resp := codeFlowLogin(rClient, proxyAddress, http.StatusOK, testLoAUser, testLoAPass)
597+
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
598+
body := resp.Body()
599+
Expect(strings.Contains(string(body), postLoginRedirectPath)).To(BeTrue())
600+
jarURI, err := url.Parse(proxyAddress)
601+
Expect(err).NotTo(HaveOccurred())
602+
cookiesLogin := rClient.GetClient().Jar.Cookies(jarURI)
603+
604+
var accessCookieLogin string
605+
for _, cook := range cookiesLogin {
606+
if cook.Name == constant.AccessCookie {
607+
accessCookieLogin = cook.Value
608+
}
609+
}
610+
611+
By("verify access token contains default acr value")
612+
token, err := jwt.ParseSigned(accessCookieLogin, constant.SignatureAlgs[:])
613+
Expect(err).NotTo(HaveOccurred())
614+
customClaims := models.CustClaims{}
615+
616+
err = token.UnsafeClaimsWithoutVerification(&customClaims)
617+
Expect(err).NotTo(HaveOccurred())
618+
Expect(customClaims.Acr).To(Equal(loaDefaultLevel))
619+
620+
By("make step up request")
621+
resp, err = rClient.R().Get(proxyAddress + loaStepUpPath)
622+
Expect(err).NotTo(HaveOccurred())
623+
body = resp.Body()
624+
625+
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))
626+
Expect(err).NotTo(HaveOccurred())
627+
628+
selection := doc.Find("#kc-otp-login-form")
629+
Expect(selection).ToNot(BeNil())
630+
Expect(selection.Nodes).ToNot(BeEmpty())
631+
632+
selection.Each(func(_ int, s *goquery.Selection) {
633+
action, exists := s.Attr("action")
634+
Expect(exists).To(BeTrue())
635+
636+
otp, errOtp := totp.GenerateCode(otpSecret, time.Now().UTC())
637+
Expect(errOtp).NotTo(HaveOccurred())
638+
rClient.FormData.Del("username")
639+
rClient.FormData.Del("password")
640+
rClient.FormData.Set("otp", otp)
641+
rClient.SetRedirectPolicy(resty.FlexibleRedirectPolicy(2))
642+
rClient.SetBaseURL(proxyAddress)
643+
resp, err = rClient.R().Post(action)
644+
loc := resp.Header().Get("Location")
645+
646+
resp, err = rClient.R().Get(loc)
647+
Expect(err).NotTo(HaveOccurred())
648+
Expect(strings.Contains(string(resp.Body()), loaStepUpPath)).To(BeTrue())
649+
Expect(resp.StatusCode()).To(Equal(http.StatusOK))
650+
651+
By("verify access token contains raised acr value")
652+
cookiesLogin := rClient.GetClient().Jar.Cookies(jarURI)
653+
654+
var accessCookieLogin string
655+
for _, cook := range cookiesLogin {
656+
if cook.Name == constant.AccessCookie {
657+
accessCookieLogin = cook.Value
658+
}
659+
}
660+
token, err = jwt.ParseSigned(accessCookieLogin, constant.SignatureAlgs[:])
661+
Expect(err).NotTo(HaveOccurred())
662+
customClaims := models.CustClaims{}
663+
664+
err = token.UnsafeClaimsWithoutVerification(&customClaims)
665+
Expect(err).NotTo(HaveOccurred())
666+
Expect(customClaims.Acr).To(Equal(loaStepUpLevel))
667+
})
668+
669+
By("log out")
670+
resp, err = rClient.R().Get(proxyAddress + logoutURI)
671+
Expect(err).NotTo(HaveOccurred())
672+
Expect(resp.StatusCode()).To(Equal(http.StatusOK))
673+
674+
rClient.SetRedirectPolicy(resty.NoRedirectPolicy())
675+
resp, _ = rClient.R().Get(proxyAddress)
676+
Expect(resp.StatusCode()).To(Equal(http.StatusSeeOther))
677+
},
678+
)
679+
})
680+
})

e2e/e2e_uma_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ var _ = Describe("UMA Code Flow authorization", func() {
5050
It("should login with user/password and logout successfully", func(_ context.Context) {
5151
var err error
5252
rClient := resty.New()
53-
resp := codeFlowLogin(rClient, proxyAddress+umaAllowedPath, http.StatusOK)
53+
resp := codeFlowLogin(rClient, proxyAddress+umaAllowedPath, http.StatusOK, testUser, testPass)
5454
Expect(resp.Header().Get("Proxy-Accepted")).To(Equal("true"))
5555

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

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

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

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

397397
resp, err = rClient.R().Get(proxyAddress + logoutURI)
@@ -413,7 +413,7 @@ var _ = Describe("UMA Code Flow, NOPROXY authorization with method scope", func(
413413
"X-Forwarded-URI": umaMethodAllowedPath,
414414
"X-Forwarded-Method": "POST",
415415
})
416-
resp := codeFlowLogin(rClient, proxyAddress, http.StatusForbidden)
416+
resp := codeFlowLogin(rClient, proxyAddress, http.StatusForbidden, testUser, testPass)
417417
Expect(resp.Header().Get(constant.AuthorizationHeader)).To(BeEmpty())
418418
})
419419
})
@@ -426,7 +426,7 @@ var _ = Describe("UMA Code Flow, NOPROXY authorization with method scope", func(
426426
"X-Forwarded-Host": strings.Split(proxyAddress, "//")[1],
427427
"X-Forwarded-URI": umaMethodAllowedPath,
428428
})
429-
resp := codeFlowLogin(rClient, proxyAddress, http.StatusForbidden)
429+
resp := codeFlowLogin(rClient, proxyAddress, http.StatusForbidden, testUser, testPass)
430430
Expect(resp.Header().Get(constant.AuthorizationHeader)).To(BeEmpty())
431431
})
432432
})

0 commit comments

Comments
 (0)