From 9de8d1d5d8fec6493fdbcc1bdbcfbdda7a39f8d2 Mon Sep 17 00:00:00 2001 From: p53 Date: Fri, 24 May 2024 00:36:56 +0200 Subject: [PATCH] Move code to packages, regroup (#467) Refactor, regrouping code to packages, separating what is reusable --- pkg/authorization/external_keycloak.go | 15 +- pkg/keycloak/proxy/forwarding.go | 5 +- pkg/keycloak/proxy/handlers.go | 28 +-- pkg/keycloak/proxy/middleware.go | 38 ++-- pkg/keycloak/proxy/misc.go | 57 ++---- pkg/keycloak/proxy/oauth_proxy.go | 159 +---------------- pkg/keycloak/proxy/server.go | 3 +- pkg/keycloak/proxy/session.go | 79 --------- pkg/proxy/cookie/cookies.go | 38 ++++ pkg/proxy/handlers/handlers.go | 13 +- pkg/proxy/models/models.go | 17 ++ pkg/proxy/models/rest.go | 18 ++ pkg/proxy/models/user.go | 77 ++++++++ pkg/proxy/session/token.go | 233 +++++++++++++++++++++++++ pkg/proxy/session/token_test.go | 64 +++++++ pkg/testsuite/fake_authserver.go | 72 ++++---- pkg/testsuite/fake_proxy.go | 8 +- pkg/testsuite/handlers_test.go | 20 ++- pkg/testsuite/middleware_test.go | 18 +- pkg/testsuite/session_test.go | 14 +- pkg/utils/token.go | 110 ------------ pkg/utils/utils.go | 11 -- pkg/utils/utils_test.go | 59 +------ 23 files changed, 584 insertions(+), 572 deletions(-) delete mode 100644 pkg/keycloak/proxy/session.go create mode 100644 pkg/proxy/models/models.go create mode 100644 pkg/proxy/models/rest.go create mode 100644 pkg/proxy/models/user.go create mode 100644 pkg/proxy/session/token.go create mode 100644 pkg/proxy/session/token_test.go delete mode 100644 pkg/utils/token.go diff --git a/pkg/authorization/external_keycloak.go b/pkg/authorization/external_keycloak.go index 153839bb..9b1736b3 100644 --- a/pkg/authorization/external_keycloak.go +++ b/pkg/authorization/external_keycloak.go @@ -6,22 +6,13 @@ import ( "github.com/Nerzal/gocloak/v12" "github.com/gogatekeeper/gatekeeper/pkg/apperrors" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" ) -type Permission struct { - Scopes []string `json:"scopes"` - ResourceID string `json:"rsid"` - ResourceName string `json:"rsname"` -} - -type Permissions struct { - Permissions []Permission `json:"permissions"` -} - var _ Provider = (*KeycloakAuthorizationProvider)(nil) type KeycloakAuthorizationProvider struct { - perms Permissions + perms models.Permissions targetPath string idpClient *gocloak.GoCloak idpTimeout time.Duration @@ -31,7 +22,7 @@ type KeycloakAuthorizationProvider struct { } func NewKeycloakAuthorizationProvider( - perms Permissions, + perms models.Permissions, targetPath string, idpClient *gocloak.GoCloak, idpTimeout time.Duration, diff --git a/pkg/keycloak/proxy/forwarding.go b/pkg/keycloak/proxy/forwarding.go index 6f32b8ad..4b397b9a 100644 --- a/pkg/keycloak/proxy/forwarding.go +++ b/pkg/keycloak/proxy/forwarding.go @@ -22,6 +22,7 @@ import ( "github.com/gogatekeeper/gatekeeper/pkg/apperrors" "github.com/gogatekeeper/gatekeeper/pkg/constant" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" "github.com/gogatekeeper/gatekeeper/pkg/utils" "go.uber.org/zap" ) @@ -45,10 +46,10 @@ func proxyMiddleware( // @step: retrieve the request scope ctxVal := req.Context().Value(constant.ContextScopeName) - var scope *RequestScope + var scope *models.RequestScope if ctxVal != nil { var assertOk bool - scope, assertOk = ctxVal.(*RequestScope) + scope, assertOk = ctxVal.(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return diff --git a/pkg/keycloak/proxy/handlers.go b/pkg/keycloak/proxy/handlers.go index 2086deac..1776ba67 100644 --- a/pkg/keycloak/proxy/handlers.go +++ b/pkg/keycloak/proxy/handlers.go @@ -37,6 +37,8 @@ import ( "github.com/gogatekeeper/gatekeeper/pkg/proxy/cookie" "github.com/gogatekeeper/gatekeeper/pkg/proxy/handlers" "github.com/gogatekeeper/gatekeeper/pkg/proxy/metrics" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/session" "github.com/gogatekeeper/gatekeeper/pkg/storage" "github.com/gogatekeeper/gatekeeper/pkg/utils" "github.com/grokify/go-pkce" @@ -66,7 +68,7 @@ func oauthAuthorizationHandler( return } - scope, assertOk := req.Context().Value(constant.ContextScopeName).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return @@ -194,7 +196,7 @@ func oauthCallbackHandler( return } - scope, assertOk := req.Context().Value(constant.ContextScopeName).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return @@ -396,7 +398,7 @@ func loginHandler( store storage.Storage, ) func(wrt http.ResponseWriter, req *http.Request) { return func(writer http.ResponseWriter, req *http.Request) { - scope, assertOk := req.Context().Value(constant.ContextScopeName).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) @@ -455,7 +457,7 @@ func loginHandler( errors.Join(apperrors.ErrParseAccessToken, err) } - identity, err := ExtractIdentity(accessTokenObj) + identity, err := session.ExtractIdentity(accessTokenObj) if err != nil { return http.StatusNotImplemented, errors.Join(apperrors.ErrExtractIdentityFromAccessToken, err) @@ -576,10 +578,10 @@ func loginHandler( } } - var resp TokenResponse + var resp models.TokenResponse if enableEncryptedToken { - resp = TokenResponse{ + resp = models.TokenResponse{ IDToken: idToken, AccessToken: accessToken, RefreshToken: refreshToken, @@ -588,7 +590,7 @@ func loginHandler( TokenType: token.TokenType, } } else { - resp = TokenResponse{ + resp = models.TokenResponse{ IDToken: plainIDToken, AccessToken: token.AccessToken, RefreshToken: refreshToken, @@ -643,7 +645,7 @@ func logoutHandler( cookManager *cookie.Manager, idpClient *gocloak.GoCloak, accessError func(wrt http.ResponseWriter, req *http.Request) context.Context, - GetIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*UserContext, error), + GetIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*models.UserContext, error), ) func(wrt http.ResponseWriter, req *http.Request) { return func(writer http.ResponseWriter, req *http.Request) { // @check if the redirection is there @@ -667,7 +669,7 @@ func logoutHandler( } } - scope, assertOk := req.Context().Value(constant.ContextScopeName).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) writer.WriteHeader(http.StatusInternalServerError) @@ -835,7 +837,7 @@ func logoutHandler( // expirationHandler checks if the token has expired func expirationHandler( - getIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*UserContext, error), + getIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*models.UserContext, error), cookieAccessName string, ) func(wrt http.ResponseWriter, req *http.Request) { return func(wrt http.ResponseWriter, req *http.Request) { @@ -856,7 +858,7 @@ func expirationHandler( // tokenHandler display access token to screen func tokenHandler( - getIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*UserContext, error), + getIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*models.UserContext, error), cookieAccessName string, accessError func(wrt http.ResponseWriter, req *http.Request) context.Context, ) func(wrt http.ResponseWriter, req *http.Request) { @@ -897,7 +899,7 @@ func retrieveRefreshToken( cookieRefreshName string, encryptionKey string, req *http.Request, - user *UserContext, + user *models.UserContext, ) (string, string, error) { var token string var err error @@ -906,7 +908,7 @@ func retrieveRefreshToken( case true: token, err = GetRefreshTokenFromStore(req.Context(), store, user.RawToken) default: - token, err = utils.GetRefreshTokenFromCookie(req, cookieRefreshName) + token, err = session.GetRefreshTokenFromCookie(req, cookieRefreshName) } if err != nil { diff --git a/pkg/keycloak/proxy/middleware.go b/pkg/keycloak/proxy/middleware.go index 0ae5fb46..e14efe6e 100644 --- a/pkg/keycloak/proxy/middleware.go +++ b/pkg/keycloak/proxy/middleware.go @@ -37,6 +37,8 @@ import ( "github.com/gogatekeeper/gatekeeper/pkg/encryption" "github.com/gogatekeeper/gatekeeper/pkg/proxy/cookie" "github.com/gogatekeeper/gatekeeper/pkg/proxy/metrics" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/session" "github.com/gogatekeeper/gatekeeper/pkg/storage" "github.com/gogatekeeper/gatekeeper/pkg/utils" "golang.org/x/oauth2" @@ -59,7 +61,7 @@ func entrypointMiddleware(logger *zap.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(wrt http.ResponseWriter, req *http.Request) { // @step: create a context for the request - scope := &RequestScope{} + scope := &models.RequestScope{} // Save the exact formatting of the incoming request so we can use it later scope.Path = req.URL.Path scope.RawPath = req.URL.RawPath @@ -123,7 +125,7 @@ func loggingMiddleware( return } - scope, assertOk := req.Context().Value(constant.ContextScopeName).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return @@ -174,7 +176,7 @@ func authenticationMiddleware( logger *zap.Logger, cookieAccessName string, cookieRefreshName string, - getIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*UserContext, error), + getIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*models.UserContext, error), idpClient *gocloak.GoCloak, enableIDPSessionCheck bool, provider *oidc3.Provider, @@ -196,7 +198,7 @@ func authenticationMiddleware( ) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(wrt http.ResponseWriter, req *http.Request) { - scope, assertOk := req.Context().Value(constant.ContextScopeName).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return @@ -404,7 +406,7 @@ func authenticationMiddleware( cookMgr.DropAccessTokenCookie(req.WithContext(ctx), wrt, accessToken, accessExpiresIn) // update the with the new access token and inject into the context - newUser, err := ExtractIdentity(&newAccToken) + newUser, err := session.ExtractIdentity(&newAccToken) if err != nil { lLog.Error(err.Error()) accessForbidden(wrt, req) @@ -492,12 +494,12 @@ func authorizationMiddleware( clientID string, skipClientIDCheck bool, skipIssuerCheck bool, - getIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*UserContext, error), + getIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*models.UserContext, error), 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) { - scope, assertOk := req.Context().Value(constant.ContextScopeName).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return @@ -545,7 +547,7 @@ func authorizationMiddleware( authzFunc := func( targetPath string, - userPerms authorization.Permissions, + userPerms models.Permissions, ) (authorization.AuthzDecision, error) { pat.m.RLock() token := pat.Token.AccessToken @@ -575,7 +577,7 @@ func authorizationMiddleware( authzFunc, ) if err != nil { - var umaUser *UserContext + var umaUser *models.UserContext scope.Logger.Error(err.Error()) scope.Logger.Info("trying to get new uma token") @@ -686,7 +688,7 @@ func authorizationMiddleware( //nolint:cyclop func checkClaim( logger *zap.Logger, - user *UserContext, + user *models.UserContext, claimName string, match *regexp.Regexp, resourceURL string, @@ -783,7 +785,7 @@ func admissionMiddleware( 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).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return @@ -912,7 +914,7 @@ func identityHeadersMiddleware( return func(next http.Handler) http.Handler { return http.HandlerFunc(func(wrt http.ResponseWriter, req *http.Request) { - scope, assertOk := req.Context().Value(constant.ContextScopeName).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return @@ -946,7 +948,7 @@ func identityHeadersMiddleware( } // are we filtering out the cookies if !enableAuthzCookies { - _ = filterCookies(req, cookieFilter) + _ = cookie.FilterCookies(req, cookieFilter) } // inject any custom claims for claim, header := range customClaims { @@ -986,7 +988,7 @@ func securityMiddleware( }) return http.HandlerFunc(func(wrt http.ResponseWriter, req *http.Request) { - scope, assertOk := req.Context().Value(constant.ContextScopeName).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return @@ -1026,12 +1028,12 @@ func proxyDenyMiddleware(logger *zap.Logger) func(http.Handler) http.Handler { return http.HandlerFunc(func(wrt http.ResponseWriter, req *http.Request) { ctxVal := req.Context().Value(constant.ContextScopeName) - var scope *RequestScope + var scope *models.RequestScope if ctxVal == nil { - scope = &RequestScope{} + scope = &models.RequestScope{} } else { var assertOk bool - scope, assertOk = ctxVal.(*RequestScope) + scope, assertOk = ctxVal.(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return @@ -1064,7 +1066,7 @@ func denyMiddleware( func hmacMiddleware(logger *zap.Logger, encKey string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(wrt http.ResponseWriter, req *http.Request) { - scope, assertOk := req.Context().Value(constant.ContextScopeName).(*RequestScope) + scope, assertOk := req.Context().Value(constant.ContextScopeName).(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) return diff --git a/pkg/keycloak/proxy/misc.go b/pkg/keycloak/proxy/misc.go index 8031fb7e..fb1589f3 100644 --- a/pkg/keycloak/proxy/misc.go +++ b/pkg/keycloak/proxy/misc.go @@ -37,52 +37,27 @@ import ( "github.com/gogatekeeper/gatekeeper/pkg/constant" "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/proxy/session" "github.com/gogatekeeper/gatekeeper/pkg/utils" "go.uber.org/zap" "golang.org/x/oauth2" ) -// filterCookies is responsible for censoring any cookies we don't want sent -func filterCookies(req *http.Request, filter []string) error { - // @NOTE: there doesn't appear to be a way of removing a cookie from the http.Request as - // AddCookie() just append - cookies := req.Cookies() - // @step: empty the current cookies - req.Header.Set("Cookie", "") - // @step: iterate the cookies and filter out anything we - for _, cookie := range cookies { - var found bool - // @step: does this cookie match our filter? - for _, n := range filter { - if strings.HasPrefix(cookie.Name, n) { - req.AddCookie(&http.Cookie{Name: cookie.Name, Value: "censored"}) - found = true - break - } - } - - if !found { - req.AddCookie(cookie) - } - } - - return nil -} - // revokeProxy is responsible for stopping middleware from proxying the request func revokeProxy(logger *zap.Logger, req *http.Request) context.Context { - var scope *RequestScope + var scope *models.RequestScope ctxVal := req.Context().Value(constant.ContextScopeName) switch ctxVal { case nil: - scope = &RequestScope{AccessDenied: true} + scope = &models.RequestScope{AccessDenied: true} default: var assertOk bool - scope, assertOk = ctxVal.(*RequestScope) + scope, assertOk = ctxVal.(*models.RequestScope) if !assertOk { logger.Error(apperrors.ErrAssertionFailed.Error()) - scope = &RequestScope{AccessDenied: true} + scope = &models.RequestScope{AccessDenied: true} } } @@ -276,7 +251,7 @@ func GetAccessCookieExpiration( logger.Error("unable to parse token") } - if ident, err := ExtractIdentity(webToken); err == nil { + if ident, err := session.ExtractIdentity(webToken); err == nil { delta := time.Until(ident.ExpiresAt) if delta > 0 { @@ -416,14 +391,14 @@ func getPAT( func WithUMAIdentity( req *http.Request, targetPath string, - user *UserContext, + user *models.UserContext, cookieUMAName string, provider *oidc3.Provider, clientID string, skipClientIDCheck bool, skipIssuerCheck bool, - getIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*UserContext, error), - authzFunc func(targetPath string, userPerms authorization.Permissions) (authorization.AuthzDecision, error), + getIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*models.UserContext, error), + authzFunc func(targetPath string, userPerms models.Permissions) (authorization.AuthzDecision, error), ) (authorization.AuthzDecision, error) { umaUser, err := getIdentity(req, cookieUMAName, constant.UMAHeader) if err != nil { @@ -561,7 +536,7 @@ func getRPT( } func getCodeFlowTokens( - scope *RequestScope, + scope *models.RequestScope, writer http.ResponseWriter, req *http.Request, enablePKCE bool, @@ -705,7 +680,7 @@ func verifyToken( } func encryptToken( - scope *RequestScope, + scope *models.RequestScope, rawToken string, encKey string, tokenType string, @@ -726,7 +701,7 @@ func encryptToken( } func getRequestURIFromCookie( - scope *RequestScope, + scope *models.RequestScope, encodedRequestURI *http.Cookie, ) string { // some clients URL-escape padding characters @@ -758,9 +733,9 @@ func refreshUmaToken( idpClient *gocloak.GoCloak, realm string, targetPath string, - user *UserContext, + user *models.UserContext, methodScope *string, -) (*UserContext, error) { +) (*models.UserContext, error) { tok, err := getRPT( ctx, pat, @@ -779,7 +754,7 @@ func refreshUmaToken( return nil, err } - umaUser, err := ExtractIdentity(token) + umaUser, err := session.ExtractIdentity(token) if err != nil { return nil, err } diff --git a/pkg/keycloak/proxy/oauth_proxy.go b/pkg/keycloak/proxy/oauth_proxy.go index 64d3f6d3..082309e2 100644 --- a/pkg/keycloak/proxy/oauth_proxy.go +++ b/pkg/keycloak/proxy/oauth_proxy.go @@ -2,22 +2,16 @@ package proxy import ( "context" - "fmt" "net" "net/http" "net/url" - "strings" "sync" - "time" "github.com/Nerzal/gocloak/v12" 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/authorization" - "github.com/gogatekeeper/gatekeeper/pkg/constant" "github.com/gogatekeeper/gatekeeper/pkg/keycloak/config" "github.com/gogatekeeper/gatekeeper/pkg/proxy/cookie" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" "github.com/gogatekeeper/gatekeeper/pkg/storage" "go.uber.org/zap" "golang.org/x/oauth2" @@ -56,158 +50,9 @@ type OauthProxy struct { accessForbidden func(wrt http.ResponseWriter, req *http.Request) context.Context accessError func(wrt http.ResponseWriter, req *http.Request) context.Context customSignInPage func(wrt http.ResponseWriter, authURL string) - GetIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*UserContext, error) + GetIdentity func(req *http.Request, tokenCookie string, tokenHeader string) (*models.UserContext, error) Cm *cookie.Manager WithOAuthURI func(uri string) string getRedirectionURL func(wrt http.ResponseWriter, req *http.Request) string newOAuth2Config func(redirectionURL string) *oauth2.Config } - -// TokenResponse -type TokenResponse struct { - TokenType string `json:"token_type"` - AccessToken string `json:"access_token"` - IDToken string `json:"id_token"` - RefreshToken string `json:"refresh_token,omitempty"` - ExpiresIn float64 `json:"expires_in"` - Scope string `json:"scope,omitempty"` -} - -// RequestScope is a request level context scope passed between middleware -type RequestScope struct { - // AccessDenied indicates the request should not be proxied on - AccessDenied bool - // Identity is the user Identity of the request - Identity *UserContext - // The parsed (unescaped) value of the request path - Path string - // Preserve the original request path: KEYCLOAK-10864, KEYCLOAK-11276, KEYCLOAK-13315 - // The exact path received in the request, if different than Path - RawPath string - Logger *zap.Logger -} - -type RealmRoles struct { - Roles []string `json:"roles"` -} - -// Extract custom claims -type custClaims struct { - Email string `json:"email"` - PrefName string `json:"preferred_username"` - RealmAccess RealmRoles `json:"realm_access"` - Groups []string `json:"groups"` - ResourceAccess map[string]interface{} `json:"resource_access"` - FamilyName string `json:"family_name"` - GivenName string `json:"given_name"` - Username string `json:"username"` - Authorization authorization.Permissions `json:"authorization"` -} - -// ExtractIdentity parse the jwt token and extracts the various elements is order to construct -func ExtractIdentity(token *jwt.JSONWebToken) (*UserContext, error) { - stdClaims := &jwt.Claims{} - customClaims := custClaims{} - - err := token.UnsafeClaimsWithoutVerification(stdClaims, &customClaims) - - if err != nil { - return nil, err - } - - jsonMap := make(map[string]interface{}) - err = token.UnsafeClaimsWithoutVerification(&jsonMap) - - if err != nil { - return nil, err - } - - // @step: ensure we have and can extract the preferred name of the user, if not, we set to the ID - preferredName := customClaims.PrefName - if preferredName == "" { - preferredName = customClaims.Email - } - - audiences := stdClaims.Audience - - // @step: extract the realm roles - roleList := make([]string, 0) - roleList = append(roleList, customClaims.RealmAccess.Roles...) - - // @step: extract the client roles from the access token - for name, list := range customClaims.ResourceAccess { - scopes, assertOk := list.(map[string]interface{}) - - if !assertOk { - return nil, apperrors.ErrAssertionFailed - } - - if roles, found := scopes[constant.ClaimResourceRoles]; found { - rolesVal, assertOk := roles.([]interface{}) - - if !assertOk { - return nil, apperrors.ErrAssertionFailed - } - - for _, r := range rolesVal { - roleList = append(roleList, fmt.Sprintf("%s:%s", name, r)) - } - } - } - - return &UserContext{ - Audiences: audiences, - Email: customClaims.Email, - ExpiresAt: stdClaims.Expiry.Time(), - Groups: customClaims.Groups, - ID: stdClaims.Subject, - Name: preferredName, - PreferredName: preferredName, - Roles: roleList, - Claims: jsonMap, - Permissions: customClaims.Authorization, - }, nil -} - -// isExpired checks if the token has expired -func (r *UserContext) IsExpired() bool { - return r.ExpiresAt.Before(time.Now()) -} - -// String returns a string representation of the user context -func (r *UserContext) String() string { - return fmt.Sprintf( - "user: %s, expires: %s, roles: %s", - r.PreferredName, - r.ExpiresAt.String(), - strings.Join(r.Roles, ","), - ) -} - -// userContext holds the information extracted the token -type UserContext struct { - // the id of the user - ID string - // the audience for the token - Audiences []string - // whether the context is from a session cookie or authorization header - BearerToken bool - // the email associated to the user - Email string - // the expiration of the access token - ExpiresAt time.Time - // groups is a collection of groups where user is member - Groups []string - // a name of the user - Name string - // preferredName is the name of the user - PreferredName string - // roles is a collection of roles the users holds - Roles []string - // rawToken - RawToken string - // claims - Claims map[string]interface{} - // permissions - Permissions authorization.Permissions -} diff --git a/pkg/keycloak/proxy/server.go b/pkg/keycloak/proxy/server.go index 299a0a5b..78c7862d 100644 --- a/pkg/keycloak/proxy/server.go +++ b/pkg/keycloak/proxy/server.go @@ -56,6 +56,7 @@ import ( proxycore "github.com/gogatekeeper/gatekeeper/pkg/proxy/core" "github.com/gogatekeeper/gatekeeper/pkg/proxy/handlers" "github.com/gogatekeeper/gatekeeper/pkg/proxy/metrics" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/session" "github.com/gogatekeeper/gatekeeper/pkg/storage" "github.com/gogatekeeper/gatekeeper/pkg/utils" "github.com/prometheus/client_golang/prometheus" @@ -339,7 +340,7 @@ func (r *OauthProxy) CreateReverseProxy() error { r.Config.Scopes, ) - r.GetIdentity = GetIdentity( + r.GetIdentity = session.GetIdentity( r.Log, r.Config.SkipAuthorizationHeaderIdentity, r.Config.EnableEncryptedToken, diff --git a/pkg/keycloak/proxy/session.go b/pkg/keycloak/proxy/session.go deleted file mode 100644 index 423874bd..00000000 --- a/pkg/keycloak/proxy/session.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright 2015 All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package proxy - -import ( - "net/http" - "strings" - - "github.com/go-jose/go-jose/v3/jwt" - "github.com/gogatekeeper/gatekeeper/pkg/apperrors" - "github.com/gogatekeeper/gatekeeper/pkg/encryption" - "github.com/gogatekeeper/gatekeeper/pkg/utils" - "go.uber.org/zap" -) - -// GetIdentity retrieves the user identity from a request, either from a session cookie or a bearer token -func GetIdentity( - logger *zap.Logger, - skipAuthorizationHeaderIdentity bool, - enableEncryptedToken bool, - forceEncryptedCookie bool, - encKey string, -) func(req *http.Request, tokenCookie string, tokenHeader string) (*UserContext, error) { - return func(req *http.Request, tokenCookie string, tokenHeader string) (*UserContext, error) { - var isBearer bool - // step: check for a bearer token or cookie with jwt token - access, isBearer, err := utils.GetTokenInRequest( - req, - tokenCookie, - skipAuthorizationHeaderIdentity, - tokenHeader, - ) - if err != nil { - return nil, err - } - - if enableEncryptedToken || forceEncryptedCookie && !isBearer { - if access, err = encryption.DecodeText(access, encKey); err != nil { - return nil, apperrors.ErrDecryption - } - } - - rawToken := access - token, err := jwt.ParseSigned(access) - if err != nil { - return nil, err - } - - user, err := ExtractIdentity(token) - if err != nil { - return nil, err - } - - user.BearerToken = isBearer - user.RawToken = rawToken - - logger.Debug("found the user identity", - zap.String("id", user.ID), - zap.String("name", user.Name), - zap.String("email", user.Email), - zap.String("roles", strings.Join(user.Roles, ",")), - zap.String("groups", strings.Join(user.Groups, ","))) - - return user, nil - } -} diff --git a/pkg/proxy/cookie/cookies.go b/pkg/proxy/cookie/cookies.go index 82259e97..9f8ff0b8 100644 --- a/pkg/proxy/cookie/cookies.go +++ b/pkg/proxy/cookie/cookies.go @@ -264,3 +264,41 @@ func (cm *Manager) ClearStateParameterCookie(req *http.Request, wrt http.Respons cm.ClearCookie(req, wrt, cm.CookieRequestURIName) cm.ClearCookie(req, wrt, cm.CookieOAuthStateName) } + +// findCookie looks for a cookie in a list of cookies +func FindCookie(name string, cookies []*http.Cookie) *http.Cookie { + for _, cookie := range cookies { + if cookie.Name == name { + return cookie + } + } + + return nil +} + +// filterCookies is responsible for censoring any cookies we don't want sent +func FilterCookies(req *http.Request, filter []string) error { + // @NOTE: there doesn't appear to be a way of removing a cookie from the http.Request as + // AddCookie() just append + cookies := req.Cookies() + // @step: empty the current cookies + req.Header.Set("Cookie", "") + // @step: iterate the cookies and filter out anything we + for _, cookie := range cookies { + var found bool + // @step: does this cookie match our filter? + for _, n := range filter { + if strings.HasPrefix(cookie.Name, n) { + req.AddCookie(&http.Cookie{Name: cookie.Name, Value: "censored"}) + found = true + break + } + } + + if !found { + req.AddCookie(cookie) + } + } + + return nil +} diff --git a/pkg/proxy/handlers/handlers.go b/pkg/proxy/handlers/handlers.go index 4a9f6bc2..ceff0938 100644 --- a/pkg/proxy/handlers/handlers.go +++ b/pkg/proxy/handlers/handlers.go @@ -28,17 +28,12 @@ import ( "github.com/gogatekeeper/gatekeeper/pkg/constant" "github.com/gogatekeeper/gatekeeper/pkg/encryption" proxycore "github.com/gogatekeeper/gatekeeper/pkg/proxy/core" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/session" "github.com/gogatekeeper/gatekeeper/pkg/utils" "go.uber.org/zap" ) -type DiscoveryResponse struct { - ExpiredURL string `json:"expired_endpoint"` - LogoutURL string `json:"logout_endpoint"` - TokenURL string `json:"token_endpoint"` - LoginURL string `json:"login_endpoint"` -} - // EmptyHandler is responsible for doing nothing func EmptyHandler(_ http.ResponseWriter, _ *http.Request) {} @@ -123,7 +118,7 @@ func RetrieveIDToken( var err error var encrypted string - token, err = utils.GetTokenInCookie(req, cookieIDTokenName) + token, err = session.GetTokenInCookie(req, cookieIDTokenName) if err != nil { return token, "", err @@ -143,7 +138,7 @@ func DiscoveryHandler( withOAuthURI func(string) string, ) func(wrt http.ResponseWriter, _ *http.Request) { return func(wrt http.ResponseWriter, _ *http.Request) { - resp := &DiscoveryResponse{ + resp := &models.DiscoveryResponse{ ExpiredURL: withOAuthURI(constant.ExpiredURL), LogoutURL: withOAuthURI(constant.LogoutURL), TokenURL: withOAuthURI(constant.TokenURL), diff --git a/pkg/proxy/models/models.go b/pkg/proxy/models/models.go new file mode 100644 index 00000000..346aae6a --- /dev/null +++ b/pkg/proxy/models/models.go @@ -0,0 +1,17 @@ +package models + +import "go.uber.org/zap" + +// RequestScope is a request level context scope passed between middleware +type RequestScope struct { + // AccessDenied indicates the request should not be proxied on + AccessDenied bool + // Identity is the user Identity of the request + Identity *UserContext + // The parsed (unescaped) value of the request path + Path string + // Preserve the original request path: KEYCLOAK-10864, KEYCLOAK-11276, KEYCLOAK-13315 + // The exact path received in the request, if different than Path + RawPath string + Logger *zap.Logger +} diff --git a/pkg/proxy/models/rest.go b/pkg/proxy/models/rest.go new file mode 100644 index 00000000..34de9778 --- /dev/null +++ b/pkg/proxy/models/rest.go @@ -0,0 +1,18 @@ +package models + +// models.TokenResponse +type TokenResponse struct { + TokenType string `json:"token_type"` + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + RefreshToken string `json:"refresh_token,omitempty"` + ExpiresIn float64 `json:"expires_in"` + Scope string `json:"scope,omitempty"` +} + +type DiscoveryResponse struct { + ExpiredURL string `json:"expired_endpoint"` + LogoutURL string `json:"logout_endpoint"` + TokenURL string `json:"token_endpoint"` + LoginURL string `json:"login_endpoint"` +} diff --git a/pkg/proxy/models/user.go b/pkg/proxy/models/user.go new file mode 100644 index 00000000..f769996a --- /dev/null +++ b/pkg/proxy/models/user.go @@ -0,0 +1,77 @@ +package models + +import ( + "fmt" + "strings" + "time" +) + +type Permission struct { + Scopes []string `json:"scopes"` + ResourceID string `json:"rsid"` + ResourceName string `json:"rsname"` +} + +type Permissions struct { + Permissions []Permission `json:"permissions"` +} + +type RealmRoles struct { + Roles []string `json:"roles"` +} + +// Extract custom claims +type CustClaims struct { + Email string `json:"email"` + PrefName string `json:"preferred_username"` + RealmAccess RealmRoles `json:"realm_access"` + Groups []string `json:"groups"` + ResourceAccess map[string]interface{} `json:"resource_access"` + FamilyName string `json:"family_name"` + GivenName string `json:"given_name"` + Username string `json:"username"` + Authorization Permissions `json:"authorization"` +} + +// isExpired checks if the token has expired +func (r *UserContext) IsExpired() bool { + return r.ExpiresAt.Before(time.Now()) +} + +// String returns a string representation of the user context +func (r *UserContext) String() string { + return fmt.Sprintf( + "user: %s, expires: %s, roles: %s", + r.PreferredName, + r.ExpiresAt.String(), + strings.Join(r.Roles, ","), + ) +} + +// userContext holds the information extracted the token +type UserContext struct { + // the id of the user + ID string + // the audience for the token + Audiences []string + // whether the context is from a session cookie or authorization header + BearerToken bool + // the email associated to the user + Email string + // the expiration of the access token + ExpiresAt time.Time + // groups is a collection of groups where user is member + Groups []string + // a name of the user + Name string + // preferredName is the name of the user + PreferredName string + // roles is a collection of roles the users holds + Roles []string + // rawToken + RawToken string + // claims + Claims map[string]interface{} + // permissions + Permissions Permissions +} diff --git a/pkg/proxy/session/token.go b/pkg/proxy/session/token.go new file mode 100644 index 00000000..935fd74a --- /dev/null +++ b/pkg/proxy/session/token.go @@ -0,0 +1,233 @@ +package session + +import ( + "bytes" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/go-jose/go-jose/v3/jwt" + "github.com/gogatekeeper/gatekeeper/pkg/apperrors" + "github.com/gogatekeeper/gatekeeper/pkg/constant" + "github.com/gogatekeeper/gatekeeper/pkg/encryption" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/cookie" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" + "go.uber.org/zap" +) + +// GetRefreshTokenFromCookie returns the refresh token from the cookie if any +func GetRefreshTokenFromCookie(req *http.Request, cookieName string) (string, error) { + token, err := GetTokenInCookie(req, cookieName) + if err != nil { + return "", err + } + + return token, nil +} + +// getTokenInRequest returns the token from the http request +// +//nolint:cyclop +func GetTokenInRequest( + req *http.Request, + name string, + skipAuthorizationHeaderIdentity bool, + tokenHeader string, +) (string, bool, error) { + bearer := true + token := "" + var err error + + if tokenHeader == "" && !skipAuthorizationHeaderIdentity { + token, err = GetTokenInBearer(req) + if err != nil && err != apperrors.ErrSessionNotFound { + return "", false, err + } + } + + if tokenHeader != "" { + token, err = GetTokenInHeader(req, tokenHeader) + if err != nil && err != apperrors.ErrSessionNotFound { + return "", false, err + } + } + + // step: check for a token in the authorization header + if err != nil || (err == nil && skipAuthorizationHeaderIdentity) { + if token, err = GetTokenInCookie(req, name); err != nil { + return token, false, err + } + bearer = false + } + + return token, bearer, nil +} + +// getTokenInBearer retrieves a access token from the authorization header +func GetTokenInBearer(req *http.Request) (string, error) { + token := req.Header.Get(constant.AuthorizationHeader) + if token == "" { + return "", apperrors.ErrSessionNotFound + } + + items := strings.Split(token, " ") + if len(items) != 2 { + return "", apperrors.ErrInvalidSession + } + + if items[0] != constant.AuthorizationType { + return "", apperrors.ErrSessionNotFound + } + return items[1], nil +} + +// getTokenInHeader retrieves a token from the header +func GetTokenInHeader(req *http.Request, headerName string) (string, error) { + token := req.Header.Get(headerName) + if token == "" { + return "", apperrors.ErrSessionNotFound + } + return token, nil +} + +// getTokenInCookie retrieves the access token from the request cookies +func GetTokenInCookie(req *http.Request, name string) (string, error) { + var token bytes.Buffer + + if cookie := cookie.FindCookie(name, req.Cookies()); cookie != nil { + token.WriteString(cookie.Value) + } + + // add also divided cookies + for i := 1; i < 600; i++ { + cookie := cookie.FindCookie(name+"-"+strconv.Itoa(i), req.Cookies()) + if cookie == nil { + break + } + token.WriteString(cookie.Value) + } + + if token.Len() == 0 { + return "", apperrors.ErrSessionNotFound + } + + return token.String(), nil +} + +// GetIdentity retrieves the user identity from a request, either from a session cookie or a bearer token +func GetIdentity( + logger *zap.Logger, + skipAuthorizationHeaderIdentity bool, + enableEncryptedToken bool, + forceEncryptedCookie bool, + encKey string, +) func(req *http.Request, tokenCookie string, tokenHeader string) (*models.UserContext, error) { + return func(req *http.Request, tokenCookie string, tokenHeader string) (*models.UserContext, error) { + var isBearer bool + // step: check for a bearer token or cookie with jwt token + access, isBearer, err := GetTokenInRequest( + req, + tokenCookie, + skipAuthorizationHeaderIdentity, + tokenHeader, + ) + if err != nil { + return nil, err + } + + if enableEncryptedToken || forceEncryptedCookie && !isBearer { + if access, err = encryption.DecodeText(access, encKey); err != nil { + return nil, apperrors.ErrDecryption + } + } + + rawToken := access + token, err := jwt.ParseSigned(access) + if err != nil { + return nil, err + } + + user, err := ExtractIdentity(token) + if err != nil { + return nil, err + } + + user.BearerToken = isBearer + user.RawToken = rawToken + + logger.Debug("found the user identity", + zap.String("id", user.ID), + zap.String("name", user.Name), + zap.String("email", user.Email), + zap.String("roles", strings.Join(user.Roles, ",")), + zap.String("groups", strings.Join(user.Groups, ","))) + + return user, nil + } +} + +// ExtractIdentity parse the jwt token and extracts the various elements is order to construct +func ExtractIdentity(token *jwt.JSONWebToken) (*models.UserContext, error) { + stdClaims := &jwt.Claims{} + customClaims := models.CustClaims{} + + err := token.UnsafeClaimsWithoutVerification(stdClaims, &customClaims) + + if err != nil { + return nil, err + } + + jsonMap := make(map[string]interface{}) + err = token.UnsafeClaimsWithoutVerification(&jsonMap) + + if err != nil { + return nil, err + } + + // @step: ensure we have and can extract the preferred name of the user, if not, we set to the ID + preferredName := customClaims.PrefName + if preferredName == "" { + preferredName = customClaims.Email + } + + audiences := stdClaims.Audience + + // @step: extract the realm roles + roleList := make([]string, 0) + roleList = append(roleList, customClaims.RealmAccess.Roles...) + + // @step: extract the client roles from the access token + for name, list := range customClaims.ResourceAccess { + scopes, assertOk := list.(map[string]interface{}) + + if !assertOk { + return nil, apperrors.ErrAssertionFailed + } + + if roles, found := scopes[constant.ClaimResourceRoles]; found { + rolesVal, assertOk := roles.([]interface{}) + + if !assertOk { + return nil, apperrors.ErrAssertionFailed + } + + for _, r := range rolesVal { + roleList = append(roleList, fmt.Sprintf("%s:%s", name, r)) + } + } + } + + return &models.UserContext{ + Audiences: audiences, + Email: customClaims.Email, + ExpiresAt: stdClaims.Expiry.Time(), + Groups: customClaims.Groups, + ID: stdClaims.Subject, + Name: preferredName, + PreferredName: preferredName, + Roles: roleList, + Claims: jsonMap, + Permissions: customClaims.Authorization, + }, nil +} diff --git a/pkg/proxy/session/token_test.go b/pkg/proxy/session/token_test.go new file mode 100644 index 00000000..6d9c33c8 --- /dev/null +++ b/pkg/proxy/session/token_test.go @@ -0,0 +1,64 @@ +package session + +import ( + "net/http" + "net/url" + "testing" + + "github.com/gogatekeeper/gatekeeper/pkg/constant" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetRefreshTokenFromCookie(t *testing.T) { + cases := []struct { + Cookies *http.Cookie + Expected string + Ok bool + }{ + { + Cookies: &http.Cookie{}, + }, + { + Cookies: &http.Cookie{ + Name: "not_a_session_cookie", + Path: "/", + Domain: "127.0.0.1", + }, + }, + { + Cookies: &http.Cookie{ + Name: "kc-state", + Path: "/", + Domain: "127.0.0.1", + Value: "refresh_token", + }, + Expected: "refresh_token", + Ok: true, + }, + } + + for _, testCase := range cases { + req := &http.Request{ + Method: http.MethodGet, + Header: make(map[string][]string), + Host: "127.0.0.1", + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1", + Path: "/", + }, + } + req.AddCookie(testCase.Cookies) + token, err := GetRefreshTokenFromCookie(req, constant.RefreshCookie) + switch testCase.Ok { + case true: + require.NoError(t, err) + assert.NotEmpty(t, token) + assert.Equal(t, testCase.Expected, token) + default: + require.Error(t, err) + assert.Empty(t, token) + } + } +} diff --git a/pkg/testsuite/fake_authserver.go b/pkg/testsuite/fake_authserver.go index 7e6fc179..b3198e42 100644 --- a/pkg/testsuite/fake_authserver.go +++ b/pkg/testsuite/fake_authserver.go @@ -19,10 +19,10 @@ import ( "github.com/go-chi/chi/v5/middleware" jose2 "github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v3/jwt" - "github.com/gogatekeeper/gatekeeper/pkg/authorization" configcore "github.com/gogatekeeper/gatekeeper/pkg/config/core" "github.com/gogatekeeper/gatekeeper/pkg/constant" - "github.com/gogatekeeper/gatekeeper/pkg/keycloak/proxy" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/session" "github.com/grokify/go-pkce" "github.com/jochasinga/relay" ) @@ -32,32 +32,32 @@ type RoleClaim struct { } type DefaultTestTokenClaims struct { - Aud string `json:"aud"` - Azp string `json:"azp"` - ClientSession string `json:"client_session"` - Email string `json:"email"` - FamilyName string `json:"family_name"` - GivenName string `json:"given_name"` - Username string `json:"username"` - Iat int64 `json:"iat"` - Iss string `json:"iss"` - Jti string `json:"jti"` - Name string `json:"name"` - Nbf int `json:"nbf"` - Exp int64 `json:"exp"` - PreferredUsername string `json:"preferred_username"` - SessionState string `json:"session_state"` - Sub string `json:"sub"` - Typ string `json:"typ"` - Groups []string `json:"groups"` - RealmAccess RoleClaim `json:"realm_access"` - ResourceAccess map[string]RoleClaim `json:"resource_access"` - Item string `json:"item"` - Found string `json:"found"` - Item1 []string `json:"item1"` - Item2 []string `json:"item2"` - Item3 []string `json:"item3"` - Authorization authorization.Permissions `json:"authorization"` + Aud string `json:"aud"` + Azp string `json:"azp"` + ClientSession string `json:"client_session"` + Email string `json:"email"` + FamilyName string `json:"family_name"` + GivenName string `json:"given_name"` + Username string `json:"username"` + Iat int64 `json:"iat"` + Iss string `json:"iss"` + Jti string `json:"jti"` + Name string `json:"name"` + Nbf int `json:"nbf"` + Exp int64 `json:"exp"` + PreferredUsername string `json:"preferred_username"` + SessionState string `json:"session_state"` + Sub string `json:"sub"` + Typ string `json:"typ"` + Groups []string `json:"groups"` + RealmAccess RoleClaim `json:"realm_access"` + ResourceAccess map[string]RoleClaim `json:"resource_access"` + Item string `json:"item"` + Found string `json:"found"` + Item1 []string `json:"item1"` + Item2 []string `json:"item2"` + Item3 []string `json:"item3"` + Authorization models.Permissions `json:"authorization"` } var defTestTokenClaims = DefaultTestTokenClaims{ @@ -491,7 +491,7 @@ func (r *fakeAuthServer) userInfoHandler(wrt http.ResponseWriter, req *http.Requ return } - user, err := proxy.ExtractIdentity(token) + user, err := session.ExtractIdentity(token) if err != nil { wrt.WriteHeader(http.StatusUnauthorized) @@ -526,8 +526,8 @@ func (r *fakeAuthServer) tokenHandler(writer http.ResponseWriter, req *http.Requ codeVerifier := "" if req.FormValue("grant_type") == configcore.GrantTypeUmaTicket { - token.Claims.Authorization = authorization.Permissions{ - Permissions: []authorization.Permission{ + token.Claims.Authorization = models.Permissions{ + Permissions: []models.Permission{ { Scopes: []string{"test"}, ResourceID: "6ef1b62e-0fd4-47f2-81fc-eead97a01c22", @@ -570,7 +570,7 @@ func (r *fakeAuthServer) tokenHandler(writer http.ResponseWriter, req *http.Requ } if username == ValidUsername && password == ValidPassword { - renderJSON(http.StatusOK, writer, proxy.TokenResponse{ + renderJSON(http.StatusOK, writer, models.TokenResponse{ TokenType: "Bearer", IDToken: jwtAccess, AccessToken: jwtAccess, @@ -600,7 +600,7 @@ func (r *fakeAuthServer) tokenHandler(writer http.ResponseWriter, req *http.Requ } if clientID == ValidUsername && clientSecret == ValidPassword { - renderJSON(http.StatusOK, writer, proxy.TokenResponse{ + renderJSON(http.StatusOK, writer, models.TokenResponse{ TokenType: "Bearer", IDToken: jwtAccess, AccessToken: jwtAccess, @@ -653,7 +653,7 @@ func (r *fakeAuthServer) tokenHandler(writer http.ResponseWriter, req *http.Requ return } - renderJSON(http.StatusOK, writer, proxy.TokenResponse{ + renderJSON(http.StatusOK, writer, models.TokenResponse{ TokenType: "Bearer", IDToken: jwtAccess, AccessToken: jwtAccess, @@ -668,7 +668,7 @@ func (r *fakeAuthServer) tokenHandler(writer http.ResponseWriter, req *http.Requ } } - renderJSON(http.StatusOK, writer, proxy.TokenResponse{ + renderJSON(http.StatusOK, writer, models.TokenResponse{ TokenType: "Bearer", IDToken: jwtAccess, AccessToken: jwtAccess, @@ -676,7 +676,7 @@ func (r *fakeAuthServer) tokenHandler(writer http.ResponseWriter, req *http.Requ ExpiresIn: float64(expires.Second()), }) case configcore.GrantTypeUmaTicket: - renderJSON(http.StatusOK, writer, proxy.TokenResponse{ + renderJSON(http.StatusOK, writer, models.TokenResponse{ TokenType: "Bearer", IDToken: jwtAccess, AccessToken: jwtAccess, diff --git a/pkg/testsuite/fake_proxy.go b/pkg/testsuite/fake_proxy.go index f202e216..9fc18106 100644 --- a/pkg/testsuite/fake_proxy.go +++ b/pkg/testsuite/fake_proxy.go @@ -20,6 +20,8 @@ import ( "github.com/gogatekeeper/gatekeeper/pkg/constant" "github.com/gogatekeeper/gatekeeper/pkg/keycloak/config" "github.com/gogatekeeper/gatekeeper/pkg/keycloak/proxy" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/cookie" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" "github.com/gogatekeeper/gatekeeper/pkg/utils" "github.com/oleiade/reflections" "github.com/stoewer/go-strcase" @@ -51,7 +53,7 @@ type fakeRequest struct { SkipIssuerCheck bool RequestCA string TokenClaims map[string]interface{} - TokenAuthorization *authorization.Permissions + TokenAuthorization *models.Permissions URI string URL string Username string @@ -473,7 +475,7 @@ func (f *fakeProxy) RunTests(t *testing.T, requests []fakeRequest) { if len(reqCfg.ExpectedCookies) > 0 { for cookName, expVal := range reqCfg.ExpectedCookies { - cookie := utils.FindCookie(cookName, resp.Cookies()) + cookie := cookie.FindCookie(cookName, resp.Cookies()) if !assert.NotNil( t, @@ -501,7 +503,7 @@ func (f *fakeProxy) RunTests(t *testing.T, requests []fakeRequest) { if len(reqCfg.ExpectedCookiesValidator) > 0 { for cookName, cookValidator := range reqCfg.ExpectedCookiesValidator { - cookie := utils.FindCookie(cookName, resp.Cookies()) + cookie := cookie.FindCookie(cookName, resp.Cookies()) if !assert.NotNil( t, diff --git a/pkg/testsuite/handlers_test.go b/pkg/testsuite/handlers_test.go index fdc7fe8d..7aa26a5e 100644 --- a/pkg/testsuite/handlers_test.go +++ b/pkg/testsuite/handlers_test.go @@ -31,6 +31,8 @@ import ( "github.com/gogatekeeper/gatekeeper/pkg/constant" "github.com/gogatekeeper/gatekeeper/pkg/keycloak/config" "github.com/gogatekeeper/gatekeeper/pkg/keycloak/proxy" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/session" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -165,7 +167,7 @@ func TestLoginHandler(t *testing.T) { "username": "test", }, ExpectedContent: func(body string, testNum int) { - resp := proxy.TokenResponse{} + resp := models.TokenResponse{} err := json.Unmarshal([]byte(body), &resp) require.NoError(t, err) assert.Equal(t, "Bearer", resp.TokenType) @@ -325,7 +327,7 @@ func TestTokenEncryptionLoginHandler(t *testing.T) { cfg.CookieIDTokenName: checkAccessTokenEncryption, }, ExpectedContent: func(body string, testNum int) { - resp := proxy.TokenResponse{} + resp := models.TokenResponse{} err := json.Unmarshal([]byte(body), &resp) require.NoError(t, err) assert.True(t, checkAccessTokenEncryption(t, cfg, resp.AccessToken)) @@ -360,7 +362,7 @@ func TestTokenEncryptionLoginHandler(t *testing.T) { cfg.CookieRefreshName: checkRefreshTokenEncryption, }, ExpectedContent: func(body string, testNum int) { - resp := proxy.TokenResponse{} + resp := models.TokenResponse{} err := json.Unmarshal([]byte(body), &resp) require.NoError(t, err) assert.True(t, checkAccessTokenEncryption(t, cfg, resp.AccessToken)) @@ -393,7 +395,7 @@ func TestTokenEncryptionLoginHandler(t *testing.T) { cfg.CookieAccessName: checkAccessTokenEncryption, }, ExpectedContent: func(body string, testNum int) { - resp := proxy.TokenResponse{} + resp := models.TokenResponse{} err := json.Unmarshal([]byte(body), &resp) require.NoError(t, err) assert.False(t, checkAccessTokenEncryption(t, cfg, resp.AccessToken)) @@ -428,7 +430,7 @@ func TestTokenEncryptionLoginHandler(t *testing.T) { cfg.CookieRefreshName: checkRefreshTokenEncryption, }, ExpectedContent: func(body string, testNum int) { - resp := proxy.TokenResponse{} + resp := models.TokenResponse{} err := json.Unmarshal([]byte(body), &resp) require.NoError(t, err) assert.False(t, checkAccessTokenEncryption(t, cfg, resp.AccessToken)) @@ -464,7 +466,7 @@ func TestTokenEncryptionLoginHandler(t *testing.T) { return false } - user, err := proxy.ExtractIdentity(token) + user, err := session.ExtractIdentity(token) if err != nil { return false @@ -474,7 +476,7 @@ func TestTokenEncryptionLoginHandler(t *testing.T) { }, }, ExpectedContent: func(body string, testNum int) { - resp := proxy.TokenResponse{} + resp := models.TokenResponse{} err := json.Unmarshal([]byte(body), &resp) require.NoError(t, err) assert.False(t, checkAccessTokenEncryption(t, cfg, resp.AccessToken)) @@ -515,7 +517,7 @@ func TestTokenEncryptionLoginHandler(t *testing.T) { return false } - user, err := proxy.ExtractIdentity(token) + user, err := session.ExtractIdentity(token) if err != nil { return false @@ -526,7 +528,7 @@ func TestTokenEncryptionLoginHandler(t *testing.T) { cfg.CookieRefreshName: checkRefreshTokenEncryption, }, ExpectedContent: func(body string, testNum int) { - resp := proxy.TokenResponse{} + resp := models.TokenResponse{} err := json.Unmarshal([]byte(body), &resp) require.NoError(t, err) assert.False(t, checkAccessTokenEncryption(t, cfg, resp.AccessToken)) diff --git a/pkg/testsuite/middleware_test.go b/pkg/testsuite/middleware_test.go index 0e68088e..e5272efa 100644 --- a/pkg/testsuite/middleware_test.go +++ b/pkg/testsuite/middleware_test.go @@ -43,6 +43,8 @@ import ( "github.com/gogatekeeper/gatekeeper/pkg/encryption" "github.com/gogatekeeper/gatekeeper/pkg/keycloak/config" "github.com/gogatekeeper/gatekeeper/pkg/keycloak/proxy" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/session" "github.com/gogatekeeper/gatekeeper/pkg/utils" "github.com/go-jose/go-jose/v3/jwt" @@ -1624,7 +1626,7 @@ func checkAccessTokenEncryption(t *testing.T, cfg *config.Config, value string) return false } - user, err := proxy.ExtractIdentity(token) + user, err := session.ExtractIdentity(token) if err != nil { return false @@ -2330,7 +2332,7 @@ func TestEnableUma(t *testing.T) { ExpectedProxy: false, HasToken: true, ExpectedCode: http.StatusForbidden, - TokenAuthorization: &authorization.Permissions{}, + TokenAuthorization: &models.Permissions{}, ExpectedContent: func(body string, testNum int) { assert.Contains(t, body, "") }, @@ -2356,8 +2358,8 @@ func TestEnableUma(t *testing.T) { ExpectedProxy: true, HasToken: true, ExpectedCode: http.StatusOK, - TokenAuthorization: &authorization.Permissions{ - Permissions: []authorization.Permission{ + TokenAuthorization: &models.Permissions{ + Permissions: []models.Permission{ { Scopes: []string{"test"}, ResourceID: "", @@ -2389,8 +2391,8 @@ func TestEnableUma(t *testing.T) { ExpectedProxy: true, HasToken: true, ExpectedCode: http.StatusOK, - TokenAuthorization: &authorization.Permissions{ - Permissions: []authorization.Permission{ + TokenAuthorization: &models.Permissions{ + Permissions: []models.Permission{ { Scopes: []string{}, ResourceID: "6ef1b62e-0fd4-47f2-81fc-eead97a01c22", @@ -2422,8 +2424,8 @@ func TestEnableUma(t *testing.T) { ExpectedProxy: true, HasToken: true, ExpectedCode: http.StatusOK, - TokenAuthorization: &authorization.Permissions{ - Permissions: []authorization.Permission{ + TokenAuthorization: &models.Permissions{ + Permissions: []models.Permission{ { Scopes: []string{"test"}, ResourceID: "6ef1b62e-0fd4-47f2-81fc-eead97a01c22", diff --git a/pkg/testsuite/session_test.go b/pkg/testsuite/session_test.go index 34dec481..7a09fcfa 100644 --- a/pkg/testsuite/session_test.go +++ b/pkg/testsuite/session_test.go @@ -25,8 +25,8 @@ import ( "github.com/gogatekeeper/gatekeeper/pkg/apperrors" "github.com/gogatekeeper/gatekeeper/pkg/constant" "github.com/gogatekeeper/gatekeeper/pkg/keycloak/config" - "github.com/gogatekeeper/gatekeeper/pkg/keycloak/proxy" - "github.com/gogatekeeper/gatekeeper/pkg/utils" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/models" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/session" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -230,7 +230,7 @@ func TestGetTokenInRequest(t *testing.T) { }) } } - access, bearer, err := utils.GetTokenInRequest(req, defaultName, testCase.SkipAuthorizationHeaderIdentity, "") + access, bearer, err := session.GetTokenInRequest(req, defaultName, testCase.SkipAuthorizationHeaderIdentity, "") switch testCase.Error { case nil: require.NoError(t, err, "case %d should not have thrown an error", idx) @@ -243,7 +243,7 @@ func TestGetTokenInRequest(t *testing.T) { } func TestIsExpired(t *testing.T) { - user := &proxy.UserContext{ + user := &models.UserContext{ ExpiresAt: time.Now(), } time.Sleep(1 * time.Millisecond) @@ -262,7 +262,7 @@ func TestGetUserContext(t *testing.T) { require.NoError(t, err) webToken, err := jwt.ParseSigned(jwtToken) require.NoError(t, err) - context, err := proxy.ExtractIdentity(webToken) + context, err := session.ExtractIdentity(webToken) require.NoError(t, err) assert.NotNil(t, context) assert.Equal(t, "1e11e539-8256-4b3b-bda8-cc0d56cddb48", context.ID) @@ -279,7 +279,7 @@ func TestGetUserRealmRoleContext(t *testing.T) { require.NoError(t, err) webToken, err := jwt.ParseSigned(jwtToken) require.NoError(t, err) - context, err := proxy.ExtractIdentity(webToken) + context, err := session.ExtractIdentity(webToken) require.NoError(t, err) assert.NotNil(t, context) assert.Equal(t, "1e11e539-8256-4b3b-bda8-cc0d56cddb48", context.ID) @@ -296,7 +296,7 @@ func TestUserContextString(t *testing.T) { require.NoError(t, err) webToken, err := jwt.ParseSigned(jwtToken) require.NoError(t, err) - context, err := proxy.ExtractIdentity(webToken) + context, err := session.ExtractIdentity(webToken) require.NoError(t, err) assert.NotNil(t, context) assert.NotEmpty(t, context.String()) diff --git a/pkg/utils/token.go b/pkg/utils/token.go deleted file mode 100644 index 73ea517e..00000000 --- a/pkg/utils/token.go +++ /dev/null @@ -1,110 +0,0 @@ -package utils - -import ( - "bytes" - "net/http" - "strconv" - "strings" - - "github.com/gogatekeeper/gatekeeper/pkg/apperrors" - "github.com/gogatekeeper/gatekeeper/pkg/constant" -) - -// GetRefreshTokenFromCookie returns the refresh token from the cookie if any -func GetRefreshTokenFromCookie(req *http.Request, cookieName string) (string, error) { - token, err := GetTokenInCookie(req, cookieName) - if err != nil { - return "", err - } - - return token, nil -} - -// getTokenInRequest returns the token from the http request -// -//nolint:cyclop -func GetTokenInRequest( - req *http.Request, - name string, - skipAuthorizationHeaderIdentity bool, - tokenHeader string, -) (string, bool, error) { - bearer := true - token := "" - var err error - - if tokenHeader == "" && !skipAuthorizationHeaderIdentity { - token, err = GetTokenInBearer(req) - if err != nil && err != apperrors.ErrSessionNotFound { - return "", false, err - } - } - - if tokenHeader != "" { - token, err = GetTokenInHeader(req, tokenHeader) - if err != nil && err != apperrors.ErrSessionNotFound { - return "", false, err - } - } - - // step: check for a token in the authorization header - if err != nil || (err == nil && skipAuthorizationHeaderIdentity) { - if token, err = GetTokenInCookie(req, name); err != nil { - return token, false, err - } - bearer = false - } - - return token, bearer, nil -} - -// getTokenInBearer retrieves a access token from the authorization header -func GetTokenInBearer(req *http.Request) (string, error) { - token := req.Header.Get(constant.AuthorizationHeader) - if token == "" { - return "", apperrors.ErrSessionNotFound - } - - items := strings.Split(token, " ") - if len(items) != 2 { - return "", apperrors.ErrInvalidSession - } - - if items[0] != constant.AuthorizationType { - return "", apperrors.ErrSessionNotFound - } - return items[1], nil -} - -// getTokenInHeader retrieves a token from the header -func GetTokenInHeader(req *http.Request, headerName string) (string, error) { - token := req.Header.Get(headerName) - if token == "" { - return "", apperrors.ErrSessionNotFound - } - return token, nil -} - -// getTokenInCookie retrieves the access token from the request cookies -func GetTokenInCookie(req *http.Request, name string) (string, error) { - var token bytes.Buffer - - if cookie := FindCookie(name, req.Cookies()); cookie != nil { - token.WriteString(cookie.Value) - } - - // add also divided cookies - for i := 1; i < 600; i++ { - cookie := FindCookie(name+"-"+strconv.Itoa(i), req.Cookies()) - if cookie == nil { - break - } - token.WriteString(cookie.Value) - } - - if token.Len() == 0 { - return "", apperrors.ErrSessionNotFound - } - - return token.String(), nil -} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 24c6afe9..b1cce3d0 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -252,17 +252,6 @@ func DialAddress(location *url.URL) string { return location.Host } -// findCookie looks for a cookie in a list of cookies -func FindCookie(name string, cookies []*http.Cookie) *http.Cookie { - for _, cookie := range cookies { - if cookie.Name == name { - return cookie - } - } - - return nil -} - // toHeader is a helper method to play nice in the headers func ToHeader(v string) string { symbols := symbolsFilter.Split(v, -1) diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 65fb4535..fafd0a45 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -30,8 +30,8 @@ import ( uuid "github.com/gofrs/uuid" "github.com/gogatekeeper/gatekeeper/pkg/constant" + "github.com/gogatekeeper/gatekeeper/pkg/proxy/cookie" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestDecodeKeyPairs(t *testing.T) { @@ -196,8 +196,8 @@ func TestFindCookie(t *testing.T) { cookies := []*http.Cookie{ {Name: "cookie_there"}, } - assert.NotNil(t, FindCookie("cookie_there", cookies)) - assert.Nil(t, FindCookie("not_there", cookies)) + assert.NotNil(t, cookie.FindCookie("cookie_there", cookies)) + assert.Nil(t, cookie.FindCookie("not_there", cookies)) } func TestHasAccessOK(t *testing.T) { @@ -507,56 +507,3 @@ func getFakeURL(location string) *url.URL { u, _ := url.Parse(location) return u } - -func TestGetRefreshTokenFromCookie(t *testing.T) { - cases := []struct { - Cookies *http.Cookie - Expected string - Ok bool - }{ - { - Cookies: &http.Cookie{}, - }, - { - Cookies: &http.Cookie{ - Name: "not_a_session_cookie", - Path: "/", - Domain: "127.0.0.1", - }, - }, - { - Cookies: &http.Cookie{ - Name: "kc-state", - Path: "/", - Domain: "127.0.0.1", - Value: "refresh_token", - }, - Expected: "refresh_token", - Ok: true, - }, - } - - for _, testCase := range cases { - req := &http.Request{ - Method: http.MethodGet, - Header: make(map[string][]string), - Host: "127.0.0.1", - URL: &url.URL{ - Scheme: "http", - Host: "127.0.0.1", - Path: "/", - }, - } - req.AddCookie(testCase.Cookies) - token, err := GetRefreshTokenFromCookie(req, constant.RefreshCookie) - switch testCase.Ok { - case true: - require.NoError(t, err) - assert.NotEmpty(t, token) - assert.Equal(t, testCase.Expected, token) - default: - require.Error(t, err) - assert.Empty(t, token) - } - } -}