diff --git a/docs/content/configuration/_index.md b/docs/content/configuration/_index.md index 0e891ff7..860c9727 100644 --- a/docs/content/configuration/_index.md +++ b/docs/content/configuration/_index.md @@ -62,7 +62,7 @@ weight: 2 | --localhost-metrics | enforces the metrics page can only been requested from 127.0.0.1 | false | PROXY_LOCALHOST_METRICS | --enable-compression | enable gzip compression for response | false | PROXY_ENABLE_COMPRESSION | --enable-pkce | enable pkce for auth code flow, only S256 code challenge supported | false | PROXY_ENABLE_PKCE -| --enable-idp-session-check | during token validation it also checks if user session is still present, useful for multi app logout | false | PROXY_ENABLE_IDP_SESSION_CHECK +| --enable-idp-session-check | during token validation it also checks if user session is still present, useful for multi app logout | true | PROXY_ENABLE_IDP_SESSION_CHECK | --enable-uma | enable UMA authorization, please don't use in production as it is new feature, we would like to receive feedback first | false | PROXY_ENABLE_UMA | --enable-opa | enable authorization with external Open policy agent | false | PROXY_ENABLE_OPA | --opa-timeout | timeout for connection to OPA | 10s | PROXY_OPA_TIMEOUT diff --git a/pkg/apperrors/apperrors.go b/pkg/apperrors/apperrors.go index 8645c28e..15ddbee7 100644 --- a/pkg/apperrors/apperrors.go +++ b/pkg/apperrors/apperrors.go @@ -61,8 +61,11 @@ var ( ErrParseAccessTokenClaims = errors.New("faled to parse access token claims") ErrParseRefreshTokenClaims = errors.New("faled to parse refresh token claims") - ErrVerifyIDToken = errors.New("unable to verify the ID token") - ErrVerifyAccessToken = errors.New("unable to verify the access token") + ErrTokenSignature = errors.New("invalid token signature") + ErrVerifyIDToken = errors.New("unable to verify ID token") + ErrVerifyAccessToken = errors.New("unable to verify access token") + ErrVerifyRefreshToken = errors.New("refresh token failed verification") + ErrAccRefreshTokenMismatch = errors.New("seems that access token and refresh token doesn't match") ErrCreateRevocationReq = errors.New("unable to construct the revocation request") ErrRevocationReqFailure = errors.New("request to revocation endpoint failed") @@ -76,13 +79,14 @@ var ( // config errors - ErrInvalidPostLoginRedirectPath = errors.New("post login redirect path invalid, should be only path not absolute url (no hostname, scheme)") - ErrPostLoginRedirectPathNoRedirectsInvalid = errors.New("post login redirect path can be enabled only with no-redirect=false") - ErrMissingListenInterface = errors.New("you have not specified the listening interface") - ErrAdminListenerScheme = errors.New("scheme for admin listener must be one of [http, https]") - ErrInvalidIdpProviderProxyURI = errors.New("invalid proxy address for IDP provider proxy") - ErrInvalidMaxIdleConnections = errors.New("max-idle-connections must be a number > 0") - ErrInvalidMaxIdleConnsPerHost = errors.New( + ErrNoRedirectsWithEnableRefreshTokensInvalid = errors.New("no-redirects true cannot be enabled with refresh tokens") + ErrInvalidPostLoginRedirectPath = errors.New("post login redirect path invalid, should be only path not absolute url (no hostname, scheme)") + ErrPostLoginRedirectPathNoRedirectsInvalid = errors.New("post login redirect path can be enabled only with no-redirect=false") + ErrMissingListenInterface = errors.New("you have not specified the listening interface") + ErrAdminListenerScheme = errors.New("scheme for admin listener must be one of [http, https]") + ErrInvalidIdpProviderProxyURI = errors.New("invalid proxy address for IDP provider proxy") + ErrInvalidMaxIdleConnections = errors.New("max-idle-connections must be a number > 0") + ErrInvalidMaxIdleConnsPerHost = errors.New( "maxi-idle-connections-per-host must be a " + "number > 0 and <= max-idle-connections", ) diff --git a/pkg/keycloak/config/config.go b/pkg/keycloak/config/config.go index b7345b73..0f6dec85 100644 --- a/pkg/keycloak/config/config.go +++ b/pkg/keycloak/config/config.go @@ -341,6 +341,7 @@ func NewDefaultConfig() *Config { EnableDefaultDeny: true, EnableSessionCookies: true, EnableTokenHeader: true, + EnableIDPSessionCheck: true, HTTPOnlyCookie: true, Headers: make(map[string]string), LetsEncryptCacheDir: "./cache/", diff --git a/pkg/keycloak/proxy/handlers.go b/pkg/keycloak/proxy/handlers.go index 5792ea29..edb7978e 100644 --- a/pkg/keycloak/proxy/handlers.go +++ b/pkg/keycloak/proxy/handlers.go @@ -29,6 +29,7 @@ import ( "strings" "time" + oidc3 "github.com/coreos/go-oidc/v3/oidc" "github.com/go-jose/go-jose/v3/jwt" "github.com/gogatekeeper/gatekeeper/pkg/apperrors" "github.com/gogatekeeper/gatekeeper/pkg/constant" @@ -193,25 +194,60 @@ func (r *OauthProxy) oauthCallbackHandler(writer http.ResponseWriter, req *http. } rawAccessToken := accessToken - stdClaims, customClaims, err := r.verifyOIDCTokens(scope, accessToken, identityToken, writer, req) + oAccToken, _, err := verifyOIDCTokens( + req.Context(), + r.Provider, + r.Config.ClientID, + accessToken, + identityToken, + r.Config.SkipAccessTokenClientIDCheck, + r.Config.SkipAccessTokenIssuerCheck, + ) if err != nil { + scope.Logger.Error(err.Error()) + r.accessForbidden(writer, req) return } + scope.Logger.Debug( + "issuing access token for user", + zap.String("access token", rawAccessToken), + zap.String("sub", oAccToken.Subject), + zap.String("expires", oAccToken.Expiry.Format(time.RFC3339)), + zap.String("duration", time.Until(oAccToken.Expiry).String()), + ) + + scope.Logger.Info( + "issuing access token for user", + zap.String("sub", oAccToken.Subject), + zap.String("expires", oAccToken.Expiry.Format(time.RFC3339)), + zap.String("duration", time.Until(oAccToken.Expiry).String()), + ) + // @metric a token has been issued metrics.OauthTokensMetric.WithLabelValues("issued").Inc() - oidcTokensCookiesExp := time.Until(stdClaims.Expiry.Time()) + oidcTokensCookiesExp := time.Until(oAccToken.Expiry) // step: does the response have a refresh token and we do NOT ignore refresh tokens? if r.Config.EnableRefreshTokens && refreshToken != "" { var encrypted string - var stdRefreshClaims *jwt.Claims - stdRefreshClaims, err = r.verifyRefreshToken(scope, refreshToken, writer, req) + var oRefresh *oidc3.IDToken + oRefresh, err = verifyToken( + req.Context(), + r.Provider, + refreshToken, + r.Config.ClientID, + r.Config.SkipAccessTokenClientIDCheck, + r.Config.SkipAccessTokenIssuerCheck, + ) + if err != nil { + scope.Logger.Error(apperrors.ErrVerifyRefreshToken.Error(), zap.Error(err)) + r.accessForbidden(writer, req) return } - oidcTokensCookiesExp = time.Until(stdRefreshClaims.Expiry.Time()) + oidcTokensCookiesExp = time.Until(oRefresh.Expiry) encrypted, err = r.encryptToken(scope, refreshToken, r.Config.EncryptionKey, "refresh", writer) if err != nil { return @@ -223,8 +259,7 @@ func (r *OauthProxy) oauthCallbackHandler(writer http.ResponseWriter, req *http. scope.Logger.Error( apperrors.ErrSaveTokToStore.Error(), zap.Error(err), - zap.String("sub", stdClaims.Subject), - zap.String("email", customClaims.Email), + zap.String("sub", oAccToken.Subject), ) r.accessForbidden(writer, req) return diff --git a/pkg/keycloak/proxy/middleware.go b/pkg/keycloak/proxy/middleware.go index dc030076..5197ccc8 100644 --- a/pkg/keycloak/proxy/middleware.go +++ b/pkg/keycloak/proxy/middleware.go @@ -17,6 +17,7 @@ package proxy import ( "context" + "errors" "fmt" "net/http" "regexp" @@ -32,7 +33,6 @@ import ( "golang.org/x/oauth2" "github.com/PuerkitoBio/purell" - oidc3 "github.com/coreos/go-oidc/v3/oidc" "github.com/go-chi/chi/v5/middleware" "github.com/gogatekeeper/gatekeeper/pkg/apperrors" "github.com/unrolled/secure" @@ -219,17 +219,25 @@ func (r *OauthProxy) authenticationMiddleware() func(http.Handler) http.Handler return } } else { //nolint:gocritic - verifier := r.Provider.Verifier( - &oidc3.Config{ - ClientID: r.Config.ClientID, - SkipClientIDCheck: r.Config.SkipAccessTokenClientIDCheck, - SkipIssuerCheck: r.Config.SkipAccessTokenIssuerCheck, - }, + _, err := verifyToken( + ctx, + r.Provider, + user.RawToken, + r.Config.ClientID, + r.Config.SkipAccessTokenClientIDCheck, + r.Config.SkipAccessTokenIssuerCheck, ) - - //nolint:contextcheck - _, err := verifier.Verify(context.Background(), user.RawToken) if err != nil { + if errors.Is(err, apperrors.ErrTokenSignature) { + lLog.Error( + apperrors.ErrAccTokenVerifyFailure.Error(), + zap.Error(err), + ) + //nolint:contextcheck + next.ServeHTTP(wrt, req.WithContext(r.accessForbidden(wrt, req))) + return + } + if !strings.Contains(err.Error(), "token is expired") { lLog.Error( apperrors.ErrAccTokenVerifyFailure.Error(), @@ -261,6 +269,33 @@ func (r *OauthProxy) authenticationMiddleware() func(http.Handler) http.Handler return } + oRefresh, err := verifyToken( + ctx, + r.Provider, + refresh, + r.Config.ClientID, + r.Config.SkipAccessTokenClientIDCheck, + r.Config.SkipAccessTokenIssuerCheck, + ) + if err != nil { + lLog.Error( + apperrors.ErrVerifyRefreshToken.Error(), + zap.Error(err), + ) + //nolint:contextcheck + next.ServeHTTP(wrt, req.WithContext(r.accessForbidden(wrt, req))) + return + } + if user.ID != oRefresh.Subject { + lLog.Error( + apperrors.ErrAccRefreshTokenMismatch.Error(), + zap.Error(err), + ) + //nolint:contextcheck + next.ServeHTTP(wrt, req.WithContext(r.accessForbidden(wrt, req))) + return + } + // attempt to refresh the access token, possibly with a renewed refresh token // // NOTE: atm, this does not retrieve explicit refresh token expiry from oauth2, @@ -278,7 +313,7 @@ func (r *OauthProxy) authenticationMiddleware() func(http.Handler) http.Handler ) httpClient := r.IdpClient.RestyClient().GetClient() - _, newRawAccToken, newRefreshToken, accessExpiresAt, refreshExpiresIn, err := getRefreshedToken(ctx, conf, httpClient, refresh) + newAccToken, newRawAccToken, newRefreshToken, accessExpiresAt, refreshExpiresIn, err := getRefreshedToken(ctx, conf, httpClient, refresh) if err != nil { switch err { case apperrors.ErrRefreshTokenExpired: @@ -331,7 +366,8 @@ func (r *OauthProxy) authenticationMiddleware() func(http.Handler) http.Handler apperrors.ErrEncryptAccToken.Error(), zap.Error(err), ) - wrt.WriteHeader(http.StatusInternalServerError) + //nolint:contextcheck + next.ServeHTTP(wrt, req.WithContext(r.accessForbidden(wrt, req))) return } } @@ -339,14 +375,23 @@ func (r *OauthProxy) authenticationMiddleware() func(http.Handler) http.Handler // step: inject the refreshed access token r.Cm.DropAccessTokenCookie(req.WithContext(ctx), wrt, accessToken, accessExpiresIn) + // update the with the new access token and inject into the context + newUser, err := ExtractIdentity(&newAccToken) + if err != nil { + lLog.Error(err.Error()) + //nolint:contextcheck + next.ServeHTTP(wrt, req.WithContext(r.accessForbidden(wrt, req))) + return + } + // step: inject the renewed refresh token if newRefreshToken != "" { lLog.Debug( "renew refresh cookie with new refresh token", zap.Duration("refresh_expires_in", refreshExpiresIn), ) - - encryptedRefreshToken, err := encryption.EncodeText(newRefreshToken, r.Config.EncryptionKey) + var encryptedRefreshToken string + encryptedRefreshToken, err = encryption.EncodeText(newRefreshToken, r.Config.EncryptionKey) if err != nil { lLog.Error( apperrors.ErrEncryptRefreshToken.Error(), @@ -358,14 +403,14 @@ func (r *OauthProxy) authenticationMiddleware() func(http.Handler) http.Handler if r.useStore() { go func(old, newToken string, encrypted string) { - if err := r.DeleteRefreshToken(req.Context(), old); err != nil { + if err = r.DeleteRefreshToken(req.Context(), old); err != nil { lLog.Error( apperrors.ErrDelTokFromStore.Error(), zap.Error(err), ) } - if err := r.StoreRefreshToken(req.Context(), newToken, encrypted, refreshExpiresIn); err != nil { + if err = r.StoreRefreshToken(req.Context(), newToken, encrypted, refreshExpiresIn); err != nil { lLog.Error( apperrors.ErrSaveTokToStore.Error(), zap.Error(err), @@ -378,8 +423,10 @@ func (r *OauthProxy) authenticationMiddleware() func(http.Handler) http.Handler } } - // update the with the new access token and inject into the context - user.RawToken = newRawAccToken + // IMPORTANT: on this rely other middlewares, must be refreshed + // with new identity! + newUser.RawToken = newRawAccToken + scope.Identity = newUser ctx = context.WithValue(req.Context(), constant.ContextScopeName, scope) } } diff --git a/pkg/keycloak/proxy/misc.go b/pkg/keycloak/proxy/misc.go index 341821ca..9cb93b4e 100644 --- a/pkg/keycloak/proxy/misc.go +++ b/pkg/keycloak/proxy/misc.go @@ -18,6 +18,7 @@ package proxy import ( "context" "encoding/base64" + "errors" "fmt" "html/template" "net/http" @@ -581,121 +582,84 @@ func (r *OauthProxy) verifyUmaToken( return nil } -func (r *OauthProxy) verifyOIDCTokens( - scope *RequestScope, +func verifyOIDCTokens( + ctx context.Context, + provider *oidc3.Provider, + clientID string, rawAccessToken string, rawIDToken string, - writer http.ResponseWriter, - req *http.Request, -) (*jwt.Claims, *custClaims, error) { - var idToken *oidc3.IDToken + skipClientIDCheck bool, + skipIssuerCheck bool, +) (*oidc3.IDToken, *oidc3.IDToken, error) { + var oIDToken *oidc3.IDToken + var oAccToken *oidc3.IDToken var err error - verifier := r.Provider.Verifier(&oidc3.Config{ClientID: r.Config.ClientID}) - - ctx, cancel := context.WithTimeout( - req.Context(), - r.Config.OpenIDProviderTimeout, - ) - defer cancel() - - idToken, err = verifier.Verify(ctx, rawIDToken) + oIDToken, err = verifyToken(ctx, provider, rawIDToken, clientID, false, false) if err != nil { - scope.Logger.Error(apperrors.ErrVerifyIDToken.Error(), zap.Error(err)) - r.accessForbidden(writer, req) - return nil, nil, err - } - - token, err := jwt.ParseSigned(rawIDToken) - if err != nil { - scope.Logger.Error(apperrors.ErrParseIDToken.Error(), zap.Error(err)) - r.accessForbidden(writer, req) - return nil, nil, err - } - - stdClaims := &jwt.Claims{} - customClaims := &custClaims{} - - err = token.UnsafeClaimsWithoutVerification(stdClaims, customClaims) - if err != nil { - scope.Logger.Error(apperrors.ErrParseIDTokenClaims.Error(), zap.Error(err)) - r.accessForbidden(writer, req) - return nil, nil, err + return nil, nil, errors.Join(apperrors.ErrVerifyIDToken, err) } // check https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken - at_hash // keycloak seems doesnt support yet at_hash // https://stackoverflow.com/questions/60818373/configure-keycloak-to-include-an-at-hash-claim-in-the-id-token - if idToken.AccessTokenHash != "" { - err = idToken.VerifyAccessToken(rawAccessToken) - + if oIDToken.AccessTokenHash != "" { + err = oIDToken.VerifyAccessToken(rawAccessToken) if err != nil { - scope.Logger.Error(apperrors.ErrVerifyAccessToken.Error(), zap.Error(err)) - r.accessForbidden(writer, req) - return nil, nil, err + return nil, nil, errors.Join(apperrors.ErrVerifyAccessToken, err) } } - accToken, err := jwt.ParseSigned(rawAccessToken) - if err != nil { - scope.Logger.Error(apperrors.ErrParseAccessToken.Error()) - r.accessForbidden(writer, req) - return nil, nil, err - } - - token = accToken - stdClaims = &jwt.Claims{} - customClaims = &custClaims{} - - err = token.UnsafeClaimsWithoutVerification(stdClaims, customClaims) + oAccToken, err = verifyToken( + ctx, + provider, + rawAccessToken, + clientID, + skipClientIDCheck, + skipIssuerCheck, + ) if err != nil { - scope.Logger.Error(apperrors.ErrParseAccessTokenClaims.Error(), zap.Error(err)) - r.accessForbidden(writer, req) - return nil, nil, err + return nil, nil, errors.Join(apperrors.ErrVerifyAccessToken, err) } - scope.Logger.Debug( - "issuing access token for user", - zap.String("access token", rawAccessToken), - zap.String("email", customClaims.Email), - zap.String("sub", stdClaims.Subject), - zap.String("expires", stdClaims.Expiry.Time().Format(time.RFC3339)), - zap.String("duration", time.Until(stdClaims.Expiry.Time()).String()), - ) - - scope.Logger.Info( - "issuing access token for user", - zap.String("email", customClaims.Email), - zap.String("sub", stdClaims.Subject), - zap.String("expires", stdClaims.Expiry.Time().Format(time.RFC3339)), - zap.String("duration", time.Until(stdClaims.Expiry.Time()).String()), - ) - - return stdClaims, customClaims, nil + return oAccToken, oIDToken, nil } -func (r *OauthProxy) verifyRefreshToken( - scope *RequestScope, - rawRefreshToken string, - writer http.ResponseWriter, - req *http.Request, -) (*jwt.Claims, error) { - refreshToken, err := jwt.ParseSigned(rawRefreshToken) +func verifyToken( + ctx context.Context, + provider *oidc3.Provider, + rawToken string, + clientID string, + skipClientIDCheck bool, + skipIssuerCheck bool, +) (*oidc3.IDToken, error) { + // This verifier with this configuration checks only signatures + // we want to know if we are using valid token + // bad is that Verify method doesn't check first signatures, so + // we have to do it like this + oidcConf := &oidc3.Config{ + ClientID: clientID, + SkipClientIDCheck: skipClientIDCheck, + SkipIssuerCheck: skipIssuerCheck, + SkipExpiryCheck: true, + } + + verifier := provider.Verifier(oidcConf) + _, err := verifier.Verify(ctx, rawToken) if err != nil { - scope.Logger.Error(apperrors.ErrParseRefreshToken.Error(), zap.Error(err)) - writer.WriteHeader(http.StatusInternalServerError) - return nil, err + return nil, errors.Join(apperrors.ErrTokenSignature, err) } - stdRefreshClaims := &jwt.Claims{} - err = refreshToken.UnsafeClaimsWithoutVerification(stdRefreshClaims) + // Now doing expiration check + oidcConf.SkipExpiryCheck = false + verifier = provider.Verifier(oidcConf) + + oToken, err := verifier.Verify(ctx, rawToken) if err != nil { - scope.Logger.Error(apperrors.ErrParseRefreshTokenClaims.Error(), zap.Error(err)) - r.accessForbidden(writer, req) return nil, err } - return stdRefreshClaims, nil + return oToken, nil } func (r *OauthProxy) encryptToken( diff --git a/pkg/testsuite/fake_authserver.go b/pkg/testsuite/fake_authserver.go index 17f07b4b..7849a37e 100644 --- a/pkg/testsuite/fake_authserver.go +++ b/pkg/testsuite/fake_authserver.go @@ -114,7 +114,6 @@ func (t *fakeToken) getToken() (string, error) { var priv interface{} priv, err0 := x509.ParsePKCS8PrivateKey(input) - if err0 != nil { return "", err0 } @@ -122,14 +121,12 @@ func (t *fakeToken) getToken() (string, error) { alg := jose2.SignatureAlgorithm("RS256") privKey := &jose2.JSONWebKey{Key: priv, Algorithm: string(alg), KeyID: "test-kid"} signer, err := jose2.NewSigner(jose2.SigningKey{Algorithm: alg, Key: privKey}, nil) - if err != nil { return "", err } b := jwt.Signed(signer).Claims(&t.claims) jwt, err := b.CompactSerialize() - if err != nil { return "", err } @@ -137,7 +134,7 @@ func (t *fakeToken) getToken() (string, error) { return jwt, nil } -// getUnsignedToken returns a unsigned JWT token from the clains +// getUnsignedToken returns a unsigned JWT token from the claims func (t *fakeToken) getUnsignedToken() (string, error) { input := []byte("") block, _ := pem.Decode([]byte(fakePrivateKey)) @@ -168,7 +165,7 @@ func (t *fakeToken) getUnsignedToken() (string, error) { } items := strings.Split(jwt, ".") - jwt = strings.Join(items[0:1], ".") + jwt = strings.Join(items[0:2], ".") return jwt, nil } @@ -501,6 +498,11 @@ func (r *fakeAuthServer) userInfoHandler(wrt http.ResponseWriter, req *http.Requ return } + if user.IsExpired() { + wrt.WriteHeader(http.StatusUnauthorized) + return + } + renderJSON(http.StatusOK, wrt, map[string]interface{}{ "sub": user.Claims["sub"], "name": user.Claims["name"], diff --git a/pkg/testsuite/fake_proxy.go b/pkg/testsuite/fake_proxy.go index 9845f1f5..ce13e606 100644 --- a/pkg/testsuite/fake_proxy.go +++ b/pkg/testsuite/fake_proxy.go @@ -565,11 +565,23 @@ func (f *fakeProxy) performUserLogin(reqCfg *fakeRequest) error { func setRequestAuthentication(cfg *config.Config, client *resty.Client, request *resty.Request, c *fakeRequest, token string) { switch c.HasCookieToken { case true: - client.SetCookie(&http.Cookie{ - Name: cfg.CookieAccessName, - Path: "/", - Value: token, - }) + cookies := client.Cookies + present := false + for _, cook := range cookies { + if cook.Name == cfg.CookieAccessName { + present = true + cook.Value = token + cook.Path = "/" + break + } + } + if !present { + client.SetCookie(&http.Cookie{ + Name: cfg.CookieAccessName, + Path: "/", + Value: token, + }) + } default: request.SetAuthToken(token) } diff --git a/pkg/testsuite/middleware_test.go b/pkg/testsuite/middleware_test.go index 50affd90..a45f0cf5 100644 --- a/pkg/testsuite/middleware_test.go +++ b/pkg/testsuite/middleware_test.go @@ -1417,7 +1417,7 @@ func TestRefreshToken(t *testing.T) { Redirects: false, HasLogin: false, ExpectedProxy: false, - ExpectedCode: http.StatusUnauthorized, + ExpectedCode: http.StatusForbidden, }, }, }, @@ -2622,3 +2622,109 @@ func TestEnableOpa(t *testing.T) { ) } } + +func TestAuthenticationMiddleware(t *testing.T) { + // proxy := newFakeProxy(nil, &fakeAuthConfig{}) + // token := newTestToken(proxy.idp.getLocation()) + tok := newTestToken("example") + tok.setExpiration(time.Now().Add(-5 * time.Minute)) + unsignedToken, err := tok.getUnsignedToken() + if err != nil { + t.Fatalf(err.Error()) + } + + badlySignedToken := unsignedToken + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + cfg := newFakeKeycloakConfig() + + requests := []struct { + Name string + ProxySettings func(c *config.Config) + ExecutionSettings []fakeRequest + }{ + { + Name: "TestForgedExpiredTokenWithIdpSessionCheckDisabled", + ProxySettings: func(conf *config.Config) { + conf.EnableIDPSessionCheck = false + conf.EnableRefreshTokens = true + conf.EncryptionKey = testEncryptionKey + conf.ClientID = ValidUsername + conf.ClientSecret = ValidPassword + conf.NoRedirects = false + }, + ExecutionSettings: []fakeRequest{ + { + URI: FakeAuthAllURL, + HasLogin: true, + Redirects: true, + SkipClientIDCheck: true, + SkipIssuerCheck: true, + OnResponse: func(int, *resty.Request, *resty.Response) { + <-time.After(time.Duration(int64(2500)) * time.Millisecond) + }, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, + ExpectedLoginCookiesValidator: map[string]func(*testing.T, *config.Config, string) bool{cfg.CookieAccessName: nil}, + }, + { + URI: FakeAuthAllURL, + Redirects: true, + SkipClientIDCheck: true, + SkipIssuerCheck: true, + HasLogin: false, + RawToken: badlySignedToken, + HasCookieToken: true, + ExpectedProxy: false, + ExpectedCode: http.StatusForbidden, + }, + }, + }, + { + Name: "TestForgedExpiredTokenWithIdpSessionCheckEnabled", + ProxySettings: func(conf *config.Config) { + conf.EnableIDPSessionCheck = true + conf.EnableRefreshTokens = true + conf.EncryptionKey = testEncryptionKey + conf.ClientID = ValidUsername + conf.ClientSecret = ValidPassword + conf.NoRedirects = false + }, + ExecutionSettings: []fakeRequest{ + { + URI: FakeAuthAllURL, + HasLogin: true, + Redirects: true, + OnResponse: func(int, *resty.Request, *resty.Response) { + <-time.After(time.Duration(int64(2500)) * time.Millisecond) + }, + ExpectedProxy: true, + ExpectedCode: http.StatusOK, + ExpectedLoginCookiesValidator: map[string]func(*testing.T, *config.Config, string) bool{cfg.CookieAccessName: nil}, + }, + { + URI: FakeAuthAllURL, + Redirects: true, + SkipClientIDCheck: true, + SkipIssuerCheck: true, + HasLogin: false, + RawToken: badlySignedToken, + HasCookieToken: true, + ExpectedProxy: false, + ExpectedCode: http.StatusSeeOther, + }, + }, + }, + } + + for _, testCase := range requests { + testCase := testCase + t.Run( + testCase.Name, + func(t *testing.T) { + cfg := newFakeKeycloakConfig() + testCase.ProxySettings(cfg) + p := newFakeProxy(cfg, &fakeAuthConfig{}) + p.RunTests(t, testCase.ExecutionSettings) + }, + ) + } +}