From a40f985cc5d27f054afb557724e3b873c9d86935 Mon Sep 17 00:00:00 2001 From: p53 Date: Tue, 22 Oct 2024 15:13:55 +0200 Subject: [PATCH] Implement level of authentication (#510) --- e2e/e2e_test.go | 238 ++++++++++++- e2e/e2e_uma_test.go | 14 +- e2e/k8s/manifest.yml | 589 ++++++++++++++++++++++--------- go.mod | 12 +- go.sum | 14 + pkg/apperrors/apperrors.go | 1 + pkg/authorization/resource.go | 23 +- pkg/keycloak/config/config.go | 9 + pkg/keycloak/proxy/middleware.go | 72 ++++ pkg/keycloak/proxy/server.go | 50 ++- pkg/proxy/middleware/security.go | 1 - pkg/proxy/models/user.go | 3 + pkg/proxy/session/token.go | 1 + 13 files changed, 844 insertions(+), 183 deletions(-) diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 97c541fd..1152159f 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -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" ) @@ -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:" @@ -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" ) @@ -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()) @@ -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()) @@ -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()) @@ -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()) @@ -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() @@ -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) @@ -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)) + }, + ) + }) +}) diff --git a/e2e/e2e_uma_test.go b/e2e/e2e_uma_test.go index 07be3718..7a697122 100644 --- a/e2e/e2e_uma_test.go +++ b/e2e/e2e_uma_test.go @@ -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() @@ -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()) @@ -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()) @@ -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() @@ -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) @@ -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()) }) }) @@ -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()) }) }) diff --git a/e2e/k8s/manifest.yml b/e2e/k8s/manifest.yml index 5c15a2cb..a465c204 100644 --- a/e2e/k8s/manifest.yml +++ b/e2e/k8s/manifest.yml @@ -129,6 +129,37 @@ data: "clientRoles" : { "account" : [ "manage-account", "view-profile" ] } + }, + { + "createdTimestamp" : 1476191007298, + "username" : "myloa", + "enabled" : true, + "totp" : false, + "emailVerified" : true, + "firstName" : "Test", + "lastName" : "Test", + "email" : "myloa@somewhere.com", + "credentials" : [ + { + "type": "password", + "value": "baba5678" + }, + { + "id" : "8df1ec93-7431-4f9c-9b87-659208887b94", + "type" : "otp", + "userLabel" : "loa", + "createdDate" : 1729510491685, + "secretData" : "{\"value\":\"i9Ue8UF9bhWEW9uqFRhb\"}", + "credentialData" : "{\"subType\":\"totp\",\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\"}" + } + ], + "disableableCredentialTypes" : [], + "requiredActions" : [], + "federatedIdentities" : [], + "realmRoles" : [ "offline_access", "uma_authorization", "user" ], + "clientRoles" : { + "account" : [ "manage-account", "view-profile" ] + } } ], "clients": [ @@ -758,9 +789,130 @@ data: ], "decisionStrategy": "UNANIMOUS" } + }, + { + "clientId": "test-loa", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "4z9PoOooXNFmSCPZx0xHXaUxX4eYGFO0", + "redirectUris": [ + "*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "oauth2.device.authorization.grant.enabled": "false", + "client.secret.creation.time": "1729499068", + "backchannel.logout.session.required": "true", + "backchannel.logout.revoke.offline.tokens": "false", + "login_theme": "", + "display.on.consent.screen": false, + "frontchannel.logout.url": "", + "backchannel.logout.url": "", + "logoUri": "", + "policyUri": "", + "tosUri": "", + "access.token.signed.response.alg": "", + "id.token.signed.response.alg": "", + "id.token.encrypted.response.alg": "", + "id.token.encrypted.response.enc": "", + "user.info.response.signature.alg": "", + "user.info.encrypted.response.alg": "", + "user.info.encrypted.response.enc": "", + "request.object.signature.alg": "", + "request.object.encryption.alg": "", + "request.object.encryption.enc": "", + "request.object.required": "", + "authorization.signed.response.alg": "", + "authorization.encrypted.response.alg": "", + "authorization.encrypted.response.enc": "", + "exclude.session.state.from.auth.response": "", + "exclude.issuer.from.auth.response": "", + "use.refresh.tokens": "true", + "client_credentials.use_refresh_token": "false", + "token.response.type.bearer.lower-case": "false", + "access.token.lifespan": "", + "client.session.idle.timeout": "", + "client.session.max.lifespan": "", + "client.offline.session.idle.timeout": "", + "client.offline.session.max.lifespan": "", + "tls.client.certificate.bound.access.tokens": false, + "dpop.bound.access.tokens": false, + "pkce.code.challenge.method": "", + "require.pushed.authorization.requests": "false", + "client.use.lightweight.access.token.enabled": "false", + "acr.loa.map": "{\"level1\":1,\"level2\":2}", + "default.acr.values": "level1" + }, + "authenticationFlowBindingOverrides": { + "browser": "714d8436-409b-47cd-839f-9184ae6a09fc", + "direct_grant": "" + }, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + }, + "authorizationServicesEnabled": false } ], "clientScopes": [ + { + "id": "96698009-108e-412b-be85-10fd4a0b09e8", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "f7c429fb-7e03-4736-bdd3-8e87ef7a517c", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, { "id": "5f9694e4-0695-4f31-bc52-4e23331af1f4", "name": "address", @@ -1560,6 +1712,60 @@ data: "" ], "authenticationFlows": [ + { + "id": "155f4454-afb1-40b8-af7a-4d620c009f5c", + "alias": "1stCondition", + "description": "", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticatorConfig": "Level 1", + "authenticator": "conditional-level-of-authentication", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 0, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 1, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "24eca914-d4c1-4283-889f-c8bde0cbfa4b", + "alias": "2ndCondition", + "description": "", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticatorConfig": "Level 2", + "authenticator": "conditional-level-of-authentication", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 0, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 1, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, { "id": "70696f6a-7660-49d9-a975-dcc5efe91112", "alias": "Account verification options", @@ -1570,48 +1776,45 @@ data: "authenticationExecutions": [ { "authenticator": "idp-email-verification", + "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { + "authenticatorFlow": true, "requirement": "ALTERNATIVE", "priority": 20, + "autheticatorFlow": true, "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, { - "id": "93a425ac-2855-4262-8550-1a4c99aa57d2", - "alias": "Authentication Options", - "description": "Authentication options.", + "id": "efbf6505-28f9-4e36-b295-c356b5fa2cdf", + "alias": "Authflow", + "description": "", "providerId": "basic-flow", "topLevel": false, - "builtIn": true, + "builtIn": false, "authenticationExecutions": [ { - "authenticator": "basic-auth", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "basic-auth-otp", - "requirement": "DISABLED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 0, + "autheticatorFlow": true, + "flowAlias": "1stCondition", + "userSetupAllowed": false }, { - "authenticator": "auth-spnego", - "requirement": "DISABLED", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 1, + "autheticatorFlow": true, + "flowAlias": "2ndCondition", + "userSetupAllowed": false } ] }, @@ -1625,25 +1828,28 @@ data: "authenticationExecutions": [ { "authenticator": "auth-cookie", + "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 0, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 1, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "registration-page-form", + "authenticatorFlow": true, "requirement": "ALTERNATIVE", "priority": 2, + "autheticatorFlow": true, "flowAlias": "Form", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -1657,17 +1863,19 @@ data: "authenticationExecutions": [ { "authenticator": "conditional-user-configured", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "auth-otp-form", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, @@ -1681,18 +1889,20 @@ data: "authenticationExecutions": [ { "authenticator": "registration-page-form", + "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 31, + "autheticatorFlow": true, "flowAlias": "Basic", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false }, { "authenticator": "script-cloud-auth-entlmnts.js", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 32, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, @@ -1706,17 +1916,19 @@ data: "authenticationExecutions": [ { "authenticator": "conditional-user-configured", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "auth-otp-form", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, @@ -1730,17 +1942,19 @@ data: "authenticationExecutions": [ { "authenticator": "conditional-user-configured", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, @@ -1754,17 +1968,19 @@ data: "authenticationExecutions": [ { "authenticator": "conditional-user-configured", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "auth-otp-form", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, @@ -1778,18 +1994,20 @@ data: "authenticationExecutions": [ { "authenticator": "auth-username-password-form", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 0, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "registration-page-form", + "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 1, + "autheticatorFlow": true, "flowAlias": "OTP", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -1803,17 +2021,19 @@ data: "authenticationExecutions": [ { "authenticator": "idp-confirm-link", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { + "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 20, + "autheticatorFlow": true, "flowAlias": "Account verification options", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -1827,17 +2047,19 @@ data: "authenticationExecutions": [ { "authenticator": "conditional-user-configured", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 0, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "auth-otp-form", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 1, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, @@ -1851,17 +2073,45 @@ data: "authenticationExecutions": [ { "authenticator": "conditional-user-configured", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "reset-otp", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "714d8436-409b-47cd-839f-9184ae6a09fc", + "alias": "StepUp", + "description": "", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 0, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 1, + "autheticatorFlow": true, + "flowAlias": "Authflow", + "userSetupAllowed": false } ] }, @@ -1876,17 +2126,19 @@ data: { "authenticatorConfig": "create unique user config", "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { + "authenticatorFlow": true, "requirement": "ALTERNATIVE", "priority": 20, + "autheticatorFlow": true, "flowAlias": "Handle Existing Account", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -1900,17 +2152,19 @@ data: "authenticationExecutions": [ { "authenticator": "idp-username-password-form", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { + "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 20, + "autheticatorFlow": true, "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -1924,31 +2178,35 @@ data: "authenticationExecutions": [ { "authenticator": "auth-cookie", + "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "auth-spnego", + "authenticatorFlow": false, "requirement": "DISABLED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 25, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { + "authenticatorFlow": true, "requirement": "ALTERNATIVE", "priority": 30, + "autheticatorFlow": true, "flowAlias": "forms", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -1962,31 +2220,35 @@ data: "authenticationExecutions": [ { "authenticator": "client-secret", + "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "client-jwt", + "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "client-secret-jwt", + "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "client-x509", + "authenticatorFlow": false, "requirement": "ALTERNATIVE", "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, @@ -2000,24 +2262,27 @@ data: "authenticationExecutions": [ { "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { + "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 30, + "autheticatorFlow": true, "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -2031,10 +2296,11 @@ data: "authenticationExecutions": [ { "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, @@ -2049,17 +2315,19 @@ data: { "authenticatorConfig": "review profile config", "authenticator": "idp-review-profile", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { + "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 20, + "autheticatorFlow": true, "flowAlias": "User creation or linking", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -2073,41 +2341,19 @@ data: "authenticationExecutions": [ { "authenticator": "auth-username-password-form", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { + "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 20, + "autheticatorFlow": true, "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "610065bf-dd8d-4dc3-b845-2b6bd85c80fb", - "alias": "http challenge", - "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "no-cookie-redirect", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "REQUIRED", - "priority": 20, - "flowAlias": "Authentication Options", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -2121,11 +2367,12 @@ data: "authenticationExecutions": [ { "authenticator": "registration-page-form", + "authenticatorFlow": true, "requirement": "REQUIRED", "priority": 10, + "autheticatorFlow": true, "flowAlias": "registration form", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -2139,31 +2386,27 @@ data: "authenticationExecutions": [ { "authenticator": "registration-user-creation", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-profile-action", - "requirement": "REQUIRED", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "registration-password-action", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 50, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, "requirement": "DISABLED", "priority": 60, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, @@ -2177,31 +2420,35 @@ data: "authenticationExecutions": [ { "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "reset-credential-email", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { "authenticator": "reset-password", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false }, { + "authenticatorFlow": true, "requirement": "CONDITIONAL", "priority": 40, + "autheticatorFlow": true, "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true + "userSetupAllowed": false } ] }, @@ -2215,10 +2462,11 @@ data: "authenticationExecutions": [ { "authenticator": "script-cloud-auth-303-pswd-qa.js", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 1, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] }, @@ -2232,15 +2480,40 @@ data: "authenticationExecutions": [ { "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, "requirement": "REQUIRED", "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false + "autheticatorFlow": false, + "userSetupAllowed": false } ] } ], "authenticatorConfig": [ + { + "id": "051b6647-48c6-4525-b63b-7782ea7403c7", + "alias": "Level 1", + "config": { + "loa-condition-level": "1", + "loa-max-age": "36000" + } + }, + { + "id": "b73c80ba-b902-4860-9e98-8557d72c24fc", + "alias": "Level 1", + "config": { + "loa-condition-level": "1", + "loa-max-age": "36000" + } + }, + { + "id": "6812208a-e24f-402d-87d2-8b3abb252217", + "alias": "Level 2", + "config": { + "loa-condition-level": "2", + "loa-max-age": "0" + } + }, { "id": "9e442554-acfa-4659-bc10-73f79e4b428b", "alias": "create unique user config", diff --git a/go.mod b/go.mod index 3d6cb6ff..da200ff5 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 github.com/go-chi/chi/v5 v5.1.0 github.com/go-jose/go-jose/v4 v4.0.4 - github.com/go-resty/resty/v2 v2.14.0 + github.com/go-resty/resty/v2 v2.15.3 github.com/gofrs/uuid v4.4.0+incompatible github.com/grokify/go-pkce v0.2.3 github.com/jochasinga/relay v0.0.0-20161125200856-6a088273228f @@ -29,8 +29,8 @@ require ( github.com/urfave/cli/v2 v2.27.4 go.uber.org/automaxprocs v1.5.3 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.26.0 - golang.org/x/net v0.28.0 + golang.org/x/crypto v0.28.0 + golang.org/x/net v0.30.0 golang.org/x/oauth2 v0.23.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -44,6 +44,7 @@ require ( github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -95,6 +96,7 @@ require ( github.com/peterh/liner v1.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pquerna/otp v1.4.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.58.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -135,8 +137,8 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 542a6d8f..a22c9756 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,8 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLj github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -159,6 +161,8 @@ github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPr github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-resty/resty/v2 v2.14.0 h1:/rhkzsAqGQkozwfKS5aFAbb6TyKd3zyFRWcdRXLPCAU= github.com/go-resty/resty/v2 v2.14.0/go.mod h1:IW6mekUOsElt9C7oWr0XRt9BNSD6D5rr9mhk6NjmNHg= +github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= +github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -329,6 +333,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= @@ -542,6 +548,8 @@ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= @@ -585,6 +593,8 @@ golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= @@ -636,6 +646,8 @@ golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -662,6 +674,8 @@ golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/pkg/apperrors/apperrors.go b/pkg/apperrors/apperrors.go index 7e8864db..8415fcf2 100644 --- a/pkg/apperrors/apperrors.go +++ b/pkg/apperrors/apperrors.go @@ -158,6 +158,7 @@ var ( ErrTooManyDefaultAllowedQueryParams = errors.New("you have more default query params than allowed query params") ErrMissingDefaultQueryParamInAllowed = errors.New("param is present in default query params but missing in allowed") ErrDefaultQueryParamNotAllowed = errors.New("default query param is not in allowed query params") + ErrLoAWithNoRedirects = errors.New("level of authentication is not valid with noredirects=true") ErrCertSelfNoHostname = errors.New("no hostnames specified") ErrCertSelfLowExpiration = errors.New("expiration must be greater then 5 minutes") diff --git a/pkg/authorization/resource.go b/pkg/authorization/resource.go index 0157211d..0e74f24d 100644 --- a/pkg/authorization/resource.go +++ b/pkg/authorization/resource.go @@ -41,6 +41,8 @@ type Resource struct { Roles []string `json:"roles" yaml:"roles"` // Groups is a list of groups the user is in Groups []string `json:"groups" yaml:"groups"` + // Acr (Authentication Context Class Reference) is a list of allowed levels of authentication for user + Acr []string `json:"acr" yaml:"acr"` } func NewResource() *Resource { @@ -66,7 +68,7 @@ func (r *Resource) Parse(resource string) (*Resource, error) { return nil, errors.New( "invalid resource keypair, should be " + - "(uri|roles|headers|methods|white-listed)=comma_values", + "(uri|roles|headers|methods|acr|white-listed)=comma_values", ) } @@ -114,9 +116,11 @@ func (r *Resource) Parse(resource string) (*Resource, error) { } r.WhiteListed = value + case "acr": + r.Acr = strings.Split(keyPair[1], ",") default: return nil, - errors.New("invalid identifier, should be roles, uri or methods") + errors.New("invalid identifier, should be uri|roles|headers|methods|acr|white-listed") } } @@ -124,6 +128,8 @@ func (r *Resource) Parse(resource string) (*Resource, error) { } // valid ensure the resource is valid +// +//nolint:cyclop func (r *Resource) Valid() error { if r.Methods == nil { r.Methods = make([]string, 0) @@ -133,6 +139,10 @@ func (r *Resource) Valid() error { r.Roles = make([]string, 0) } + if r.Acr == nil { + r.Acr = make([]string, 0) + } + if r.URL == "" { return errors.New("resource does not have url") } @@ -166,6 +176,11 @@ func (r Resource) GetRoles() string { return strings.Join(r.Roles, ",") } +// GetAcr returns a list of authentication levels for this resource +func (r Resource) GetAcr() string { + return strings.Join(r.Acr, ",") +} + // GetHeaders returns a list of headers for this resource func (r Resource) GetHeaders() string { return strings.Join(r.Headers, ",") @@ -184,6 +199,10 @@ func (r Resource) String() string { roles = strings.Join(r.Roles, ",") } + if len(r.Acr) > 0 { + roles = strings.Join(r.Acr, ",") + } + if len(r.Methods) > 0 { methods = strings.Join(r.Methods, ",") } diff --git a/pkg/keycloak/config/config.go b/pkg/keycloak/config/config.go index 9cdb7770..44db35f8 100644 --- a/pkg/keycloak/config/config.go +++ b/pkg/keycloak/config/config.go @@ -179,6 +179,7 @@ type Config struct { EnableProxyProtocol bool `env:"ENABLE_PROXY_PROTOCOL" json:"enabled-proxy-protocol" usage:"enable proxy protocol" yaml:"enabled-proxy-protocol"` UseLetsEncrypt bool `env:"USE_LETS_ENCRYPT" json:"use-letsencrypt" usage:"use letsencrypt for certificates" yaml:"use-letsencrypt"` DisableAllLogging bool `env:"DISABLE_ALL_LOGGING" json:"disable-all-logging" usage:"disables all logging to stdout and stderr" yaml:"disable-all-logging"` + EnableLoA bool `env:"ENABLE_LOA" json:"enable-loa" usage:"enables level of authentication" yaml:"enable-loa"` IsDiscoverURILegacy bool } @@ -555,6 +556,7 @@ func (r *Config) isReverseProxySettingsValid() error { r.isEnableHmacValid, r.isPostLogoutRedirectURIValid, r.isAllowedQueryParamsValid, + r.isEnableLoAValid, } for _, validationFunc := range validationRegistry { @@ -897,3 +899,10 @@ func (r *Config) isAllowedQueryParamsValid() error { } return nil } + +func (r *Config) isEnableLoAValid() error { + if r.EnableLoA && r.NoRedirects { + return apperrors.ErrLoAWithNoRedirects + } + return nil +} diff --git a/pkg/keycloak/proxy/middleware.go b/pkg/keycloak/proxy/middleware.go index 6f06169a..2a24dde0 100644 --- a/pkg/keycloak/proxy/middleware.go +++ b/pkg/keycloak/proxy/middleware.go @@ -31,6 +31,8 @@ import ( "github.com/gogatekeeper/gatekeeper/pkg/encryption" "github.com/gogatekeeper/gatekeeper/pkg/proxy/cookie" "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" + "github.com/gogatekeeper/gatekeeper/pkg/utils" + "golang.org/x/oauth2" "github.com/gogatekeeper/gatekeeper/pkg/apperrors" "go.uber.org/zap" @@ -250,3 +252,73 @@ func authorizationMiddleware( }) } } + +func levelOfAuthenticationMiddleware( + logger *zap.Logger, + skipTokenVerification bool, + scopes []string, + enablePKCE bool, + signInPage string, + cookManager *cookie.Manager, + newOAuth2Config func(redirectionURL string) *oauth2.Config, + getRedirectionURL func(wrt http.ResponseWriter, req *http.Request) string, + customSignInPage func(wrt http.ResponseWriter, authURL string), + resource *authorization.Resource, + accessForbidden func(wrt http.ResponseWriter, req *http.Request) context.Context, +) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(wrt http.ResponseWriter, req *http.Request) { + // we don't need to continue is a decision has been made + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) + if !assertOk { + logger.Error(apperrors.ErrAssertionFailed.Error()) + return + } + if scope.AccessDenied { + next.ServeHTTP(wrt, req) + return + } + + user := scope.Identity + lLog := scope.Logger.With( + zap.String("middleware", "levelOfAuthentication"), + zap.String("userID", user.ID), + zap.String("resource", resource.URL), + ) + if len(resource.Acr) > 0 && user.Acr == "" { + lLog.Error("token is missing acr claim=level of authentication") + accessForbidden(wrt, req) + return + } + if len(resource.Acr) > 0 && !utils.HasAccess( + resource.Acr, + []string{user.Acr}, + false, + ) { + lLog.Info("token doesn't match required level of authentication") + allowedQueryParams := map[string]string{"acr_values": resource.Acr[0]} + defaultAllowedQueryParams := map[string]string{"acr_values": resource.Acr[0]} + uuid := cookManager.DropStateParameterCookie(req, wrt) + query := req.URL.Query() + query.Add("state", uuid) + req.URL.RawQuery = query.Encode() + oauthAuthorizationHandler( + lLog, + skipTokenVerification, + scopes, + enablePKCE, + signInPage, + cookManager, + newOAuth2Config, + getRedirectionURL, + customSignInPage, + allowedQueryParams, + defaultAllowedQueryParams, + )(wrt, req) + return + } + + next.ServeHTTP(wrt, req) + }) + } +} diff --git a/pkg/keycloak/proxy/server.go b/pkg/keycloak/proxy/server.go index ab63a2a9..e48ccde0 100644 --- a/pkg/keycloak/proxy/server.go +++ b/pkg/keycloak/proxy/server.go @@ -648,6 +648,29 @@ func (r *OauthProxy) CreateReverseProxy() error { r.Config.MatchClaims, accessForbidden, ), + } + + if r.Config.EnableLoA { + middlewares = append( + middlewares, + levelOfAuthenticationMiddleware( + r.Log, + r.Config.SkipTokenVerification, + r.Config.Scopes, + r.Config.EnablePKCE, + r.Config.SignInPage, + r.Cm, + newOAuth2Config, + getRedirectionURL, + customSignInPage, + res, + accessForbidden, + ), + ) + } + + middlewares = append( + middlewares, gmiddleware.IdentityHeadersMiddleware( r.Log, r.Config.AddClaims, @@ -658,7 +681,7 @@ func (r *OauthProxy) CreateReverseProxy() error { r.Config.EnableAuthorizationHeader, r.Config.EnableAuthorizationCookies, ), - } + ) if res.URL == constant.AllPath && !res.WhiteListed && enableDefaultDenyStrict { middlewares = []func(http.Handler) http.Handler{ @@ -701,6 +724,29 @@ func (r *OauthProxy) CreateReverseProxy() error { r.Config.MatchClaims, accessForbidden, ), + } + + if r.Config.EnableLoA { + middlewares = append( + middlewares, + levelOfAuthenticationMiddleware( + r.Log, + r.Config.SkipTokenVerification, + r.Config.Scopes, + r.Config.EnablePKCE, + r.Config.SignInPage, + r.Cm, + newOAuth2Config, + getRedirectionURL, + customSignInPage, + res, + accessForbidden, + ), + ) + } + + middlewares = append( + middlewares, gmiddleware.IdentityHeadersMiddleware( r.Log, r.Config.AddClaims, @@ -711,7 +757,7 @@ func (r *OauthProxy) CreateReverseProxy() error { r.Config.EnableAuthorizationHeader, r.Config.EnableAuthorizationCookies, ), - } + ) } e := engine.With(middlewares...) diff --git a/pkg/proxy/middleware/security.go b/pkg/proxy/middleware/security.go index b181693f..b1339b13 100644 --- a/pkg/proxy/middleware/security.go +++ b/pkg/proxy/middleware/security.go @@ -141,7 +141,6 @@ func AdmissionMiddleware( if len(resource.Headers) > 0 { var reqHeaders []string - for _, resVal := range resource.Headers { resVals := strings.Split(resVal, ":") name := resVals[0] diff --git a/pkg/proxy/models/user.go b/pkg/proxy/models/user.go index f769996a..bce7d31a 100644 --- a/pkg/proxy/models/user.go +++ b/pkg/proxy/models/user.go @@ -23,6 +23,7 @@ type RealmRoles struct { // Extract custom claims type CustClaims struct { Email string `json:"email"` + Acr string `json:"acr"` PrefName string `json:"preferred_username"` RealmAccess RealmRoles `json:"realm_access"` Groups []string `json:"groups"` @@ -58,6 +59,8 @@ type UserContext struct { BearerToken bool // the email associated to the user Email string + // current level of authentication for user + Acr string // the expiration of the access token ExpiresAt time.Time // groups is a collection of groups where user is member diff --git a/pkg/proxy/session/token.go b/pkg/proxy/session/token.go index 61e60112..a5f3f3bc 100644 --- a/pkg/proxy/session/token.go +++ b/pkg/proxy/session/token.go @@ -230,6 +230,7 @@ func ExtractIdentity(token *jwt.JSONWebToken) (*models.UserContext, error) { return &models.UserContext{ Audiences: audiences, Email: customClaims.Email, + Acr: customClaims.Acr, ExpiresAt: stdClaims.Expiry.Time(), Groups: customClaims.Groups, ID: stdClaims.Subject,