diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index f4b52ddd13bd15c554c640dc489e33e461a023cc..760c5c69186e58450e7f6a55ee51966349bf73fb 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -8,7 +8,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" urlruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apiserver/pkg/authentication/request/bearertoken" unionauth "k8s.io/apiserver/pkg/authentication/request/union" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/klog" @@ -16,6 +15,7 @@ import ( "kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/jwttoken" "kubesphere.io/kubesphere/pkg/apiserver/authentication/request/anonymous" "kubesphere.io/kubesphere/pkg/apiserver/authentication/request/basictoken" + "kubesphere.io/kubesphere/pkg/apiserver/authentication/request/bearertoken" "kubesphere.io/kubesphere/pkg/apiserver/authentication/token" "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizerfactory" "kubesphere.io/kubesphere/pkg/apiserver/authorization/path" diff --git a/pkg/apiserver/authentication/options/authenticate_options.go b/pkg/apiserver/authentication/options/authenticate_options.go index a975990451e3900bb2c78fefc828e4f3c8a372d4..789c56286ed1645c43ffd7bfeeb64c0cbe5a970e 100644 --- a/pkg/apiserver/authentication/options/authenticate_options.go +++ b/pkg/apiserver/authentication/options/authenticate_options.go @@ -69,4 +69,5 @@ func (options *AuthenticationOptions) AddFlags(fs *pflag.FlagSet, s *Authenticat fs.IntVar(&options.MaxAuthenticateRetries, "authenticate-max-retries", s.MaxAuthenticateRetries, "") fs.BoolVar(&options.MultipleLogin, "multiple-login", s.MultipleLogin, "Allow multiple login with the same account, disable means only one user can login at the same time.") fs.StringVar(&options.JwtSecret, "jwt-secret", s.JwtSecret, "Secret to sign jwt token, must not be empty.") + fs.DurationVar(&options.OAuthOptions.AccessTokenMaxAge, "access-token-max-age", s.OAuthOptions.AccessTokenMaxAge, "AccessTokenMaxAgeSeconds control the lifetime of access tokens, 0 means no expiration.") } diff --git a/pkg/apiserver/authentication/request/bearertoken/bearertoken.go b/pkg/apiserver/authentication/request/bearertoken/bearertoken.go new file mode 100644 index 0000000000000000000000000000000000000000..e637fb17a120923d6b63cccfd34a3694792ad4c0 --- /dev/null +++ b/pkg/apiserver/authentication/request/bearertoken/bearertoken.go @@ -0,0 +1,62 @@ +/* +Copyright 2014 The Kubernetes Authors. + +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 bearertoken + +import ( + "errors" + "net/http" + "strings" + + "k8s.io/apiserver/pkg/authentication/authenticator" +) + +type Authenticator struct { + auth authenticator.Token +} + +func New(auth authenticator.Token) *Authenticator { + return &Authenticator{auth} +} + +var invalidToken = errors.New("invalid bearer token") + +func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { + auth := strings.TrimSpace(req.Header.Get("Authorization")) + if auth == "" { + return nil, false, nil + } + parts := strings.Split(auth, " ") + if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" { + return nil, false, nil + } + + token := parts[1] + + // Empty bearer tokens aren't valid + if len(token) == 0 { + return nil, false, nil + } + + resp, ok, err := a.auth.AuthenticateToken(req.Context(), token) + + // If the token authenticator didn't error, provide a default error + if !ok && err == nil { + err = invalidToken + } + + return resp, ok, err +} diff --git a/pkg/apiserver/authentication/request/bearertoken/bearertoken_test.go b/pkg/apiserver/authentication/request/bearertoken/bearertoken_test.go new file mode 100644 index 0000000000000000000000000000000000000000..14125ccc939457275e5e409b5a980f57c799e6ac --- /dev/null +++ b/pkg/apiserver/authentication/request/bearertoken/bearertoken_test.go @@ -0,0 +1,192 @@ +/* +Copyright 2014 The Kubernetes Authors. + +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 bearertoken + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +func TestAuthenticateRequest(t *testing.T) { + auth := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { + if token != "token" { + t.Errorf("unexpected token: %s", token) + } + return &authenticator.Response{User: &user.DefaultInfo{Name: "user"}}, true, nil + })) + resp, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if !ok || resp == nil || err != nil { + t.Errorf("expected valid user") + } +} + +func TestAuthenticateRequestTokenInvalid(t *testing.T) { + auth := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { + return nil, false, nil + })) + resp, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if ok || resp != nil { + t.Errorf("expected not authenticated user") + } + if err != invalidToken { + t.Errorf("expected invalidToken error, got %v", err) + } +} + +func TestAuthenticateRequestTokenInvalidCustomError(t *testing.T) { + customError := errors.New("custom") + auth := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { + return nil, false, customError + })) + resp, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if ok || resp != nil { + t.Errorf("expected not authenticated user") + } + if err != customError { + t.Errorf("expected custom error, got %v", err) + } +} + +func TestAuthenticateRequestTokenError(t *testing.T) { + auth := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { + return nil, false, errors.New("error") + })) + resp, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if ok || resp != nil || err == nil { + t.Errorf("expected error") + } +} + +func TestAuthenticateRequestBadValue(t *testing.T) { + testCases := []struct { + Req *http.Request + }{ + {Req: &http.Request{}}, + {Req: &http.Request{Header: http.Header{"Authorization": []string{"Bearer"}}}}, + {Req: &http.Request{Header: http.Header{"Authorization": []string{"bear token"}}}}, + {Req: &http.Request{Header: http.Header{"Authorization": []string{"Bearer: token"}}}}, + } + for i, testCase := range testCases { + auth := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) { + t.Errorf("authentication should not have been called") + return nil, false, nil + })) + user, ok, err := auth.AuthenticateRequest(testCase.Req) + if ok || user != nil || err != nil { + t.Errorf("%d: expected not authenticated (no token)", i) + } + } +} + +func TestBearerToken(t *testing.T) { + tests := map[string]struct { + AuthorizationHeaders []string + TokenAuth authenticator.Token + + ExpectedUserName string + ExpectedOK bool + ExpectedErr bool + ExpectedAuthorizationHeaders []string + }{ + "no header": { + AuthorizationHeaders: nil, + ExpectedUserName: "", + ExpectedOK: false, + ExpectedErr: false, + ExpectedAuthorizationHeaders: nil, + }, + "empty header": { + AuthorizationHeaders: []string{""}, + ExpectedUserName: "", + ExpectedOK: false, + ExpectedErr: false, + ExpectedAuthorizationHeaders: []string{""}, + }, + "non-bearer header": { + AuthorizationHeaders: []string{"Basic 123"}, + ExpectedUserName: "", + ExpectedOK: false, + ExpectedErr: false, + ExpectedAuthorizationHeaders: []string{"Basic 123"}, + }, + "empty bearer token": { + AuthorizationHeaders: []string{"Bearer "}, + ExpectedUserName: "", + ExpectedOK: false, + ExpectedErr: false, + ExpectedAuthorizationHeaders: []string{"Bearer "}, + }, + "invalid bearer token": { + AuthorizationHeaders: []string{"Bearer 123"}, + TokenAuth: authenticator.TokenFunc(func(ctx context.Context, t string) (*authenticator.Response, bool, error) { return nil, false, nil }), + ExpectedUserName: "", + ExpectedOK: false, + ExpectedErr: true, + ExpectedAuthorizationHeaders: []string{"Bearer 123"}, + }, + "error bearer token": { + AuthorizationHeaders: []string{"Bearer 123"}, + TokenAuth: authenticator.TokenFunc(func(ctx context.Context, t string) (*authenticator.Response, bool, error) { + return nil, false, errors.New("error") + }), + ExpectedUserName: "", + ExpectedOK: false, + ExpectedErr: true, + ExpectedAuthorizationHeaders: []string{"Bearer 123"}, + }, + } + + for k, tc := range tests { + req, _ := http.NewRequest("GET", "/", nil) + for _, h := range tc.AuthorizationHeaders { + req.Header.Add("Authorization", h) + } + + bearerAuth := New(tc.TokenAuth) + resp, ok, err := bearerAuth.AuthenticateRequest(req) + if tc.ExpectedErr != (err != nil) { + t.Errorf("%s: Expected err=%v, got %v", k, tc.ExpectedErr, err) + continue + } + if ok != tc.ExpectedOK { + t.Errorf("%s: Expected ok=%v, got %v", k, tc.ExpectedOK, ok) + continue + } + if ok && resp.User.GetName() != tc.ExpectedUserName { + t.Errorf("%s: Expected username=%v, got %v", k, tc.ExpectedUserName, resp.User.GetName()) + continue + } + if !reflect.DeepEqual(req.Header["Authorization"], tc.ExpectedAuthorizationHeaders) { + t.Errorf("%s: Expected headers=%#v, got %#v", k, tc.ExpectedAuthorizationHeaders, req.Header["Authorization"]) + continue + } + } +} diff --git a/pkg/apiserver/authentication/token/jwt.go b/pkg/apiserver/authentication/token/jwt.go index e505f47650ac28f07dcee10803b92998aa381782..cae79371d8930b3e6c6e17d4665f5269a5a288f6 100644 --- a/pkg/apiserver/authentication/token/jwt.go +++ b/pkg/apiserver/authentication/token/jwt.go @@ -22,6 +22,7 @@ import ( "fmt" "github.com/dgrijalva/jwt-go" "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/klog" authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/server/errors" "kubesphere.io/kubesphere/pkg/simple/client/cache" @@ -53,20 +54,26 @@ func (s *jwtTokenIssuer) Verify(tokenString string) (User, error) { if len(tokenString) == 0 { return nil, errInvalidToken } - _, err := s.cache.Get(tokenCacheKey(tokenString)) + + clm := &Claims{} + + _, err := jwt.ParseWithClaims(tokenString, clm, s.keyFunc) if err != nil { - if err == cache.ErrNoSuchKey { - return nil, errTokenExpired - } return nil, err } - clm := &Claims{} + // 0 means no expiration. + // validate token cache + if s.options.OAuthOptions.AccessTokenMaxAge > 0 { + _, err = s.cache.Get(tokenCacheKey(tokenString)) - _, err = jwt.ParseWithClaims(tokenString, clm, s.keyFunc) - if err != nil { - return nil, err + if err != nil { + if err == cache.ErrNoSuchKey { + return nil, errTokenExpired + } + return nil, err + } } return &user.DefaultInfo{Name: clm.Username, UID: clm.UID}, nil @@ -92,16 +99,28 @@ func (s *jwtTokenIssuer) IssueTo(user User, expiresIn time.Duration) (string, er tokenString, err := token.SignedString([]byte(s.options.JwtSecret)) if err != nil { + klog.Error(err) return "", err } - s.cache.Set(tokenCacheKey(tokenString), tokenString, expiresIn) + // 0 means no expiration. + // validate token cache + if s.options.OAuthOptions.AccessTokenMaxAge > 0 { + err = s.cache.Set(tokenCacheKey(tokenString), tokenString, s.options.OAuthOptions.AccessTokenMaxAge) + if err != nil { + klog.Error(err) + return "", err + } + } return tokenString, nil } func (s *jwtTokenIssuer) Revoke(token string) error { - return s.cache.Del(tokenCacheKey(token)) + if s.options.OAuthOptions.AccessTokenMaxAge > 0 { + return s.cache.Del(tokenCacheKey(token)) + } + return nil } func NewJwtTokenIssuer(issuerName string, options *authoptions.AuthenticationOptions, cache cache.Interface) Issuer { diff --git a/pkg/apiserver/authentication/token/jwt_test.go b/pkg/apiserver/authentication/token/jwt_test.go index a1bd026c37cb76a5a1be761b7a83a4506a345b3e..c326cfa36b3aea771d4e0107ed8a7b6897abdc93 100644 --- a/pkg/apiserver/authentication/token/jwt_test.go +++ b/pkg/apiserver/authentication/token/jwt_test.go @@ -21,6 +21,7 @@ package token import ( "github.com/google/go-cmp/cmp" "k8s.io/apiserver/pkg/authentication/user" + "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" "kubesphere.io/kubesphere/pkg/simple/client/cache" "testing" @@ -70,3 +71,39 @@ func TestJwtTokenIssuer(t *testing.T) { }) } } + +func TestTokenVerifyWithoutCacheValidate(t *testing.T) { + options := authoptions.NewAuthenticateOptions() + + // do not set token cache and disable token cache validate, + options.OAuthOptions = &oauth.Options{AccessTokenMaxAge: 0} + options.JwtSecret = "kubesphere" + issuer := NewJwtTokenIssuer(DefaultIssuerName, options, nil) + + client, err := options.OAuthOptions.OAuthClient("default") + + if err != nil { + t.Fatal(err) + } + + user := &user.DefaultInfo{ + Name: "admin", + UID: "admin", + } + + tokenString, err := issuer.IssueTo(user, *client.AccessTokenMaxAge) + + if err != nil { + t.Fatal(err) + } + + got, err := issuer.Verify(tokenString) + + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(got, user); diff != "" { + t.Error("token validate failed") + } +} diff --git a/pkg/apiserver/filters/authentication.go b/pkg/apiserver/filters/authentication.go index b4a3d92c0160a0fd1ff8a2171d90077c4a8bc187..09e38b0de8226218688b4f99c4fa178729e4ddf9 100644 --- a/pkg/apiserver/filters/authentication.go +++ b/pkg/apiserver/filters/authentication.go @@ -42,9 +42,6 @@ func WithAuthentication(handler http.Handler, auth authenticator.Request) http.H return } - // authorization header is not required anymore in case of a successful authentication. - req.Header.Del("Authorization") - req = req.WithContext(request.WithUser(req.Context(), resp.User)) handler.ServeHTTP(w, req) }) diff --git a/pkg/apiserver/filters/kubeapiserver.go b/pkg/apiserver/filters/kubeapiserver.go index f259dad7c9363ef94b841aa0564b5ad206a0f173..ba972085b77ae14ca858ca95347964d3af3ffd77 100644 --- a/pkg/apiserver/filters/kubeapiserver.go +++ b/pkg/apiserver/filters/kubeapiserver.go @@ -33,6 +33,9 @@ func WithKubeAPIServer(handler http.Handler, config *rest.Config, failed proxy.E s.Host = kubernetes.Host s.Scheme = kubernetes.Scheme + // Do not cover k8s client authorization header + req.Header.Del("Authorization") + httpProxy := proxy.NewUpgradeAwareHandler(&s, defaultTransport, true, false, failed) httpProxy.ServeHTTP(w, req) return