@@ -15,14 +15,17 @@ import (
15
15
"time"
16
16
17
17
"github.com/PuerkitoBio/goquery"
18
+ "github.com/go-jose/go-jose/v4/jwt"
18
19
. "github.com/onsi/ginkgo/v2" //nolint:revive //we want to use it for ginkgo
19
20
. "github.com/onsi/gomega" //nolint:revive //we want to use it for gomega
21
+ "github.com/pquerna/otp/totp"
20
22
"golang.org/x/oauth2/clientcredentials"
21
23
22
24
resty "github.com/go-resty/resty/v2"
23
25
"github.com/gogatekeeper/gatekeeper/pkg/constant"
24
26
keycloakcore "github.com/gogatekeeper/gatekeeper/pkg/keycloak/proxy/core"
25
27
"github.com/gogatekeeper/gatekeeper/pkg/proxy"
28
+ "github.com/gogatekeeper/gatekeeper/pkg/proxy/models"
26
29
"github.com/gogatekeeper/gatekeeper/pkg/testsuite"
27
30
)
28
31
@@ -34,6 +37,8 @@ const (
34
37
pkceTestClientSecret = "F2GqU40xwX0P2LrTvHUHqwNoSk4U4n5R"
35
38
umaTestClient = "test-client-uma"
36
39
umaTestClientSecret = "A5vokiGdI3H2r4aXFrANbKvn4R7cbf6P"
40
+ loaTestClient = "test-loa"
41
+ loaTestClientSecret = "4z9PoOooXNFmSCPZx0xHXaUxX4eYGFO0"
37
42
timeout = time .Second * 300
38
43
idpURI = "http://localhost:8081"
39
44
localURI = "http://localhost:"
@@ -42,12 +47,19 @@ const (
42
47
anyURI = "/any"
43
48
testUser = "myuser"
44
49
testPass = "baba1234"
50
+ testLoAUser = "myloa"
51
+ testLoAPass = "baba5678"
45
52
testPath = "/test"
46
53
umaAllowedPath = "/pets"
47
54
umaForbiddenPath = "/pets/1"
48
55
umaNonExistentPath = "/cat"
49
56
umaMethodAllowedPath = "/horse"
50
57
umaFwdMethodAllowedPath = "/turtle"
58
+ loaPath = "/level"
59
+ loaStepUpPath = "/level2"
60
+ loaDefaultLevel = "level1"
61
+ loaStepUpLevel = "level2"
62
+ otpSecret = "NE4VKZJYKVDDSYTIK5CVOOLVOFDFE2DC"
51
63
postLoginRedirectPath = "/post/login/path"
52
64
pkceCookieName = "TESTPKCECOOKIE"
53
65
)
@@ -79,7 +91,13 @@ func startAndWait(portNum string, osArgs []string) {
79
91
}, timeout , 15 * time .Second ).Should (Succeed ())
80
92
}
81
93
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 {
83
101
client .SetRedirectPolicy (resty .FlexibleRedirectPolicy (5 ))
84
102
resp , err := client .R ().Get (reqAddress )
85
103
Expect (err ).NotTo (HaveOccurred ())
@@ -95,8 +113,8 @@ func codeFlowLogin(client *resty.Client, reqAddress string, expStatusCode int) *
95
113
action , exists := s .Attr ("action" )
96
114
Expect (exists ).To (BeTrue ())
97
115
98
- client .FormData .Add ("username" , testUser )
99
- client .FormData .Add ("password" , testPass )
116
+ client .FormData .Add ("username" , userName )
117
+ client .FormData .Add ("password" , userPass )
100
118
resp , err = client .R ().Post (action )
101
119
102
120
Expect (err ).NotTo (HaveOccurred ())
@@ -205,7 +223,7 @@ var _ = Describe("Code Flow login/logout", func() {
205
223
func (_ context.Context ) {
206
224
var err error
207
225
rClient := resty .New ()
208
- resp := codeFlowLogin (rClient , proxyAddress , http .StatusOK )
226
+ resp := codeFlowLogin (rClient , proxyAddress , http .StatusOK , testUser , testPass )
209
227
Expect (resp .Header ().Get ("Proxy-Accepted" )).To (Equal ("true" ))
210
228
body := resp .Body ()
211
229
Expect (strings .Contains (string (body ), postLoginRedirectPath )).To (BeTrue ())
@@ -269,7 +287,7 @@ var _ = Describe("Code Flow login/logout", func() {
269
287
func (_ context.Context ) {
270
288
var err error
271
289
rClient := resty .New ()
272
- resp := codeFlowLogin (rClient , proxyAddress , http .StatusOK )
290
+ resp := codeFlowLogin (rClient , proxyAddress , http .StatusOK , testUser , testPass )
273
291
Expect (resp .Header ().Get ("Proxy-Accepted" )).To (Equal ("true" ))
274
292
body := resp .Body ()
275
293
Expect (strings .Contains (string (body ), postLoginRedirectPath )).To (BeTrue ())
@@ -342,7 +360,7 @@ var _ = Describe("Code Flow PKCE login/logout", func() {
342
360
func (_ context.Context ) {
343
361
var err error
344
362
rClient := resty .New ()
345
- resp := codeFlowLogin (rClient , proxyAddress , http .StatusOK )
363
+ resp := codeFlowLogin (rClient , proxyAddress , http .StatusOK , testUser , testPass )
346
364
Expect (resp .Header ().Get ("Proxy-Accepted" )).To (Equal ("true" ))
347
365
348
366
body := resp .Body ()
@@ -423,9 +441,9 @@ var _ = Describe("Code Flow login/logout with session check", func() {
423
441
It ("should logout on both successfully" , func (_ context.Context ) {
424
442
var err error
425
443
rClient := resty .New ()
426
- resp := codeFlowLogin (rClient , proxyAddressFirst , http .StatusOK )
444
+ resp := codeFlowLogin (rClient , proxyAddressFirst , http .StatusOK , testUser , testPass )
427
445
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 )
429
447
Expect (resp .Header ().Get ("Proxy-Accepted" )).To (Equal ("true" ))
430
448
431
449
resp , err = rClient .R ().Get (proxyAddressFirst + testPath )
@@ -456,3 +474,207 @@ var _ = Describe("Code Flow login/logout with session check", func() {
456
474
})
457
475
})
458
476
})
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
+ })
0 commit comments