From 9378d5a34581020315d45dfdff387d240152b0a4 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Tue, 10 May 2016 22:01:38 +0800 Subject: [PATCH 1/4] add ut for registry client --- utils/registry/auth/authorizer.go | 4 +- utils/registry/auth/tokenhandler.go | 1 + utils/registry/errors/error.go | 3 +- utils/registry/registry.go | 5 + utils/registry/repository.go | 70 ++++++++- utils/registry/repository_test.go | 211 ++++++++++++++++++++++++++++ 6 files changed, 290 insertions(+), 4 deletions(-) create mode 100644 utils/registry/repository_test.go diff --git a/utils/registry/auth/authorizer.go b/utils/registry/auth/authorizer.go index cea731246..26ea177a5 100644 --- a/utils/registry/auth/authorizer.go +++ b/utils/registry/auth/authorizer.go @@ -46,8 +46,8 @@ func NewRequestAuthorizer(handlers []Handler, challenges []au.Challenge) *Reques // ModifyRequest adds authorization to the request func (r *RequestAuthorizer) ModifyRequest(req *http.Request) error { - for _, handler := range r.handlers { - for _, challenge := range r.challenges { + for _, challenge := range r.challenges { + for _, handler := range r.handlers { if handler.Scheme() == challenge.Scheme { if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil { return err diff --git a/utils/registry/auth/tokenhandler.go b/utils/registry/auth/tokenhandler.go index f546bac0c..d734ee935 100644 --- a/utils/registry/auth/tokenhandler.go +++ b/utils/registry/auth/tokenhandler.go @@ -168,6 +168,7 @@ func (s *standardTokenHandler) generateToken(realm, service string, scopes []str if resp.StatusCode != http.StatusOK { err = registry_errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } return diff --git a/utils/registry/errors/error.go b/utils/registry/errors/error.go index 60b8d6ce5..7a1311b00 100644 --- a/utils/registry/errors/error.go +++ b/utils/registry/errors/error.go @@ -23,12 +23,13 @@ import ( // an Error instance will be returned type Error struct { StatusCode int + StatusText string Message string } // Error ... func (e Error) Error() string { - return fmt.Sprintf("%d %s", e.StatusCode, e.Message) + return fmt.Sprintf("%d %s %s", e.StatusCode, e.StatusText, e.Message) } // ParseError parses err, if err is type Error, convert it to Error diff --git a/utils/registry/registry.go b/utils/registry/registry.go index 1ee01892e..845c8e876 100644 --- a/utils/registry/registry.go +++ b/utils/registry/registry.go @@ -89,6 +89,10 @@ func (r *Registry) Catalog() ([]string, error) { resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + return repos, e + } return repos, err } @@ -115,6 +119,7 @@ func (r *Registry) Catalog() ([]string, error) { return repos, errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } } diff --git a/utils/registry/repository.go b/utils/registry/repository.go index 507634415..479d44408 100644 --- a/utils/registry/repository.go +++ b/utils/registry/repository.go @@ -72,6 +72,9 @@ func NewRepositoryWithCredential(name, endpoint string, credential auth.Credenti } client, err := newClient(endpoint, "", credential, "repository", name, "pull", "push") + if err != nil { + return nil, err + } repository := &Repository{ Name: name, @@ -108,6 +111,16 @@ func NewRepositoryWithUsername(name, endpoint, username string) (*Repository, er return repository, nil } +func isUnauthorizedError(err error) (error, bool) { + if strings.Contains(err.Error(), http.StatusText(http.StatusUnauthorized)) { + return errors.Error{ + StatusCode: http.StatusUnauthorized, + StatusText: http.StatusText(http.StatusUnauthorized), + }, true + } + return err, false +} + // ListTag ... func (r *Repository) ListTag() ([]string, error) { tags := []string{} @@ -118,6 +131,10 @@ func (r *Repository) ListTag() ([]string, error) { resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + return tags, e + } return tags, err } @@ -141,9 +158,9 @@ func (r *Repository) ListTag() ([]string, error) { return tags, nil } - return tags, errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } @@ -161,6 +178,11 @@ func (r *Repository) ManifestExist(reference string) (digest string, exist bool, resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + err = e + return + } return } @@ -183,6 +205,7 @@ func (r *Repository) ManifestExist(reference string) (digest string, exist bool, err = errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } return @@ -201,6 +224,11 @@ func (r *Repository) PullManifest(reference string, acceptMediaTypes []string) ( resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + err = e + return + } return } @@ -219,6 +247,7 @@ func (r *Repository) PullManifest(reference string, acceptMediaTypes []string) ( err = errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } @@ -236,6 +265,11 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) ( resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + err = e + return + } return } @@ -253,6 +287,7 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) ( err = errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } @@ -268,6 +303,10 @@ func (r *Repository) DeleteManifest(digest string) error { resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + return e + } return err } @@ -284,6 +323,7 @@ func (r *Repository) DeleteManifest(digest string) error { return errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } } @@ -298,6 +338,7 @@ func (r *Repository) DeleteTag(tag string) error { if !exist { return errors.Error{ StatusCode: http.StatusNotFound, + StatusText: http.StatusText(http.StatusNotFound), } } @@ -313,6 +354,10 @@ func (r *Repository) BlobExist(digest string) (bool, error) { resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + return false, e + } return false, err } @@ -333,6 +378,7 @@ func (r *Repository) BlobExist(digest string) (bool, error) { return false, errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } } @@ -346,6 +392,11 @@ func (r *Repository) PullBlob(digest string) (size int64, data []byte, err error resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + err = e + return + } return } @@ -367,6 +418,7 @@ func (r *Repository) PullBlob(digest string) (size int64, data []byte, err error err = errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } @@ -379,6 +431,11 @@ func (r *Repository) initiateBlobUpload(name string) (location, uploadUUID strin resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + err = e + return + } return } @@ -397,6 +454,7 @@ func (r *Repository) initiateBlobUpload(name string) (location, uploadUUID strin err = errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } @@ -411,6 +469,10 @@ func (r *Repository) monolithicBlobUpload(location, digest string, size int64, d resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + return e + } return err } @@ -427,6 +489,7 @@ func (r *Repository) monolithicBlobUpload(location, digest string, size int64, d return errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } } @@ -460,6 +523,10 @@ func (r *Repository) DeleteBlob(digest string) error { resp, err := r.client.Do(req) if err != nil { + e, ok := isUnauthorizedError(err) + if ok { + return e + } return err } @@ -476,6 +543,7 @@ func (r *Repository) DeleteBlob(digest string) error { return errors.Error{ StatusCode: resp.StatusCode, + StatusText: resp.Status, Message: string(b), } } diff --git a/utils/registry/repository_test.go b/utils/registry/repository_test.go new file mode 100644 index 000000000..32332171a --- /dev/null +++ b/utils/registry/repository_test.go @@ -0,0 +1,211 @@ +/* + Copyright (c) 2016 VMware, Inc. 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 registry + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/vmware/harbor/utils/log" + "github.com/vmware/harbor/utils/registry/auth" + "github.com/vmware/harbor/utils/registry/errors" +) + +var ( + username string = "user" + password string = "P@ssw0rd" + repo string = "samalba/my-app" + tags tagResp = tagResp{Tags: []string{"1.0", "2.0", "3.0"}} + validToken string = "valid_token" + invalidToken string = "invalid_token" + credential auth.Credential + registryServer *httptest.Server + tokenServer *httptest.Server + repositoryClient *Repository +) + +type tagResp struct { + Tags []string `json:"tags"` +} + +func TestMain(m *testing.M) { + log.SetLevel(log.DebugLevel) + credential = auth.NewBasicAuthCredential(username, password) + + tokenServer = initTokenServer() + defer tokenServer.Close() + + registryServer = initRegistryServer() + defer registryServer.Close() + + os.Exit(m.Run()) +} + +func initRegistryServer() *httptest.Server { + mux := http.NewServeMux() + mux.HandleFunc("/v2/", servePing) + mux.HandleFunc(fmt.Sprintf("/v2/%s/tags/list", repo), serveTaglisting) + + return httptest.NewServer(mux) +} + +//response ping request: http://registry/v2 +func servePing(w http.ResponseWriter, r *http.Request) { + if !isTokenValid(r) { + challenge(w) + return + } +} + +func serveTaglisting(w http.ResponseWriter, r *http.Request) { + if !isTokenValid(r) { + challenge(w) + return + } + + if err := json.NewEncoder(w).Encode(tags); err != nil { + w.Write([]byte(err.Error())) + w.WriteHeader(http.StatusInternalServerError) + return + } + +} + +func isTokenValid(r *http.Request) bool { + valid := false + auth := r.Header.Get(http.CanonicalHeaderKey("Authorization")) + if len(auth) != 0 { + auth = strings.TrimSpace(auth) + index := strings.Index(auth, "Bearer") + token := auth[index+6:] + token = strings.TrimSpace(token) + if token == validToken { + valid = true + } + } + return valid +} + +func challenge(w http.ResponseWriter) { + challenge := "Bearer realm=\"" + tokenServer.URL + "/service/token\",service=\"token-service\"" + w.Header().Set("Www-Authenticate", challenge) + w.WriteHeader(http.StatusUnauthorized) + return +} + +func initTokenServer() *httptest.Server { + mux := http.NewServeMux() + mux.HandleFunc("/service/token", serveToken) + + return httptest.NewServer(mux) +} + +func serveToken(w http.ResponseWriter, r *http.Request) { + u, p, ok := r.BasicAuth() + if !ok || u != username || p != password { + w.WriteHeader(http.StatusUnauthorized) + return + } + + result := make(map[string]interface{}) + result["token"] = validToken + result["expires_in"] = 300 + result["issued_at"] = time.Now().Format(time.RFC3339) + + encoder := json.NewEncoder(w) + if err := encoder.Encode(result); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } +} + +func TestListTag(t *testing.T) { + client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) + if err != nil { + t.Error(err) + } + + list, err := client.ListTag() + if err != nil { + t.Error(err) + return + } + if len(list) != len(tags.Tags) { + t.Errorf("expected length: %d, actual length: %d", len(tags.Tags), len(list)) + return + } + +} + +func TestListTagWithInvalidUser(t *testing.T) { + credential := auth.NewBasicAuthCredential("user", "test") + client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) + if err != nil { + t.Error(err) + } + + _, err = client.ListTag() + if err != nil { + e, ok := errors.ParseError(err) + if ok && e.StatusCode == http.StatusUnauthorized { + return + } + t.Error(err) + return + } +} + +/*tokenHandler := func(w http.ResponseWriter, r *http.Request) { + username, _, ok := r.BasicAuth() + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + + service := r.FormValue("service") + scopes := r.URL.Query()["scope"] + access := token_util.GetResourceActions(scopes) + + token, _, issuedAt, err := token_util.MakeToken(username, service, access) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + t.Error(err) + return + } + + result := make(map[string]interface{}) + result["token"] = token + result["expires_in"] = "dfsd" + result["issued_at"] = issuedAt.Format(time.RFC3339) + + encoder := json.NewEncoder(w) + if err = encoder.Encode(result); err != nil { + w.WriteHeader(http.StatusInternalServerError) + t.Error(err) + return + } +} + +tokenServer := httptest.NewServer(http.HandlerFunc(tokenHandler)) +defer tokenServer.Close() +*/ From 3d2ec19ac19e827e35349daacf99fb1a41b5bdc2 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Fri, 13 May 2016 14:21:04 +0800 Subject: [PATCH 2/4] remove useless code --- utils/registry/repository_test.go | 43 +++---------------------------- 1 file changed, 4 insertions(+), 39 deletions(-) diff --git a/utils/registry/repository_test.go b/utils/registry/repository_test.go index 32332171a..5bc8a7d53 100644 --- a/utils/registry/repository_test.go +++ b/utils/registry/repository_test.go @@ -25,7 +25,7 @@ import ( "testing" "time" - "github.com/vmware/harbor/utils/log" + //"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/registry/auth" "github.com/vmware/harbor/utils/registry/errors" ) @@ -48,7 +48,7 @@ type tagResp struct { } func TestMain(m *testing.M) { - log.SetLevel(log.DebugLevel) + //log.SetLevel(log.DebugLevel) credential = auth.NewBasicAuthCredential(username, password) tokenServer = initTokenServer() @@ -157,8 +157,8 @@ func TestListTag(t *testing.T) { } -func TestListTagWithInvalidUser(t *testing.T) { - credential := auth.NewBasicAuthCredential("user", "test") +func TestListTagWithInvalidCredential(t *testing.T) { + credential := auth.NewBasicAuthCredential(username, "wrong_password") client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) if err != nil { t.Error(err) @@ -174,38 +174,3 @@ func TestListTagWithInvalidUser(t *testing.T) { return } } - -/*tokenHandler := func(w http.ResponseWriter, r *http.Request) { - username, _, ok := r.BasicAuth() - if !ok { - w.WriteHeader(http.StatusUnauthorized) - return - } - - service := r.FormValue("service") - scopes := r.URL.Query()["scope"] - access := token_util.GetResourceActions(scopes) - - token, _, issuedAt, err := token_util.MakeToken(username, service, access) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - t.Error(err) - return - } - - result := make(map[string]interface{}) - result["token"] = token - result["expires_in"] = "dfsd" - result["issued_at"] = issuedAt.Format(time.RFC3339) - - encoder := json.NewEncoder(w) - if err = encoder.Encode(result); err != nil { - w.WriteHeader(http.StatusInternalServerError) - t.Error(err) - return - } -} - -tokenServer := httptest.NewServer(http.HandlerFunc(tokenHandler)) -defer tokenServer.Close() -*/ From 3ff312892125a07fabb47751643e2b0d2b1e157b Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Fri, 13 May 2016 15:37:12 +0800 Subject: [PATCH 3/4] fix golint errors --- utils/registry/registry.go | 2 +- utils/registry/repository.go | 29 +++++++++++++++-------------- utils/registry/repository_test.go | 12 ++++++------ 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/utils/registry/registry.go b/utils/registry/registry.go index 845c8e876..baaf70e91 100644 --- a/utils/registry/registry.go +++ b/utils/registry/registry.go @@ -89,7 +89,7 @@ func (r *Registry) Catalog() ([]string, error) { resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { return repos, e } diff --git a/utils/registry/repository.go b/utils/registry/repository.go index 479d44408..ac49e04f8 100644 --- a/utils/registry/repository.go +++ b/utils/registry/repository.go @@ -111,14 +111,15 @@ func NewRepositoryWithUsername(name, endpoint, username string) (*Repository, er return repository, nil } -func isUnauthorizedError(err error) (error, bool) { +// try to convert err to errors.Error if it is +func isUnauthorizedError(err error) (bool, error) { if strings.Contains(err.Error(), http.StatusText(http.StatusUnauthorized)) { - return errors.Error{ + return true, errors.Error{ StatusCode: http.StatusUnauthorized, StatusText: http.StatusText(http.StatusUnauthorized), - }, true + } } - return err, false + return false, err } // ListTag ... @@ -131,7 +132,7 @@ func (r *Repository) ListTag() ([]string, error) { resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { return tags, e } @@ -178,7 +179,7 @@ func (r *Repository) ManifestExist(reference string) (digest string, exist bool, resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { err = e return @@ -224,7 +225,7 @@ func (r *Repository) PullManifest(reference string, acceptMediaTypes []string) ( resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { err = e return @@ -265,7 +266,7 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) ( resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { err = e return @@ -303,7 +304,7 @@ func (r *Repository) DeleteManifest(digest string) error { resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { return e } @@ -354,7 +355,7 @@ func (r *Repository) BlobExist(digest string) (bool, error) { resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { return false, e } @@ -392,7 +393,7 @@ func (r *Repository) PullBlob(digest string) (size int64, data []byte, err error resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { err = e return @@ -431,7 +432,7 @@ func (r *Repository) initiateBlobUpload(name string) (location, uploadUUID strin resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { err = e return @@ -469,7 +470,7 @@ func (r *Repository) monolithicBlobUpload(location, digest string, size int64, d resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { return e } @@ -523,7 +524,7 @@ func (r *Repository) DeleteBlob(digest string) error { resp, err := r.client.Do(req) if err != nil { - e, ok := isUnauthorizedError(err) + ok, e := isUnauthorizedError(err) if ok { return e } diff --git a/utils/registry/repository_test.go b/utils/registry/repository_test.go index 5bc8a7d53..07b46237b 100644 --- a/utils/registry/repository_test.go +++ b/utils/registry/repository_test.go @@ -31,12 +31,12 @@ import ( ) var ( - username string = "user" - password string = "P@ssw0rd" - repo string = "samalba/my-app" - tags tagResp = tagResp{Tags: []string{"1.0", "2.0", "3.0"}} - validToken string = "valid_token" - invalidToken string = "invalid_token" + username = "user" + password = "P@ssw0rd" + repo = "samalba/my-app" + tags = tagResp{Tags: []string{"1.0", "2.0", "3.0"}} + validToken = "valid_token" + invalidToken = "invalid_token" credential auth.Credential registryServer *httptest.Server tokenServer *httptest.Server From 4a823facadce6cd64348a92c457cdf529888bfc1 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Fri, 13 May 2016 16:09:43 +0800 Subject: [PATCH 4/4] guarantee the read-write operation for cache token is atomic --- utils/registry/auth/tokenhandler.go | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/utils/registry/auth/tokenhandler.go b/utils/registry/auth/tokenhandler.go index d734ee935..9d075b25d 100644 --- a/utils/registry/auth/tokenhandler.go +++ b/utils/registry/auth/tokenhandler.go @@ -22,6 +22,7 @@ import ( "net/http" "net/url" "strings" + "sync" "time" token_util "github.com/vmware/harbor/service/token" @@ -48,6 +49,7 @@ type tokenHandler struct { cache string // cached token expiresIn int // The duration in seconds since the token was issued that it will remain valid issuedAt *time.Time // The RFC3339-serialized UTC standard time at which a given token was issued + sync.Mutex } // Scheme returns the scheme that the handler can handle @@ -77,8 +79,10 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str expired := true - if t.expiresIn != 0 && t.issuedAt != nil { - expired = t.issuedAt.Add(time.Duration(t.expiresIn) * time.Second).Before(time.Now().UTC()) + cachedToken, cachedExpiredIn, cachedIssuedAt := t.getCachedToken() + + if len(cachedToken) != 0 && cachedExpiredIn != 0 && cachedIssuedAt != nil { + expired = cachedIssuedAt.Add(time.Duration(cachedExpiredIn) * time.Second).Before(time.Now().UTC()) } if expired || hasFrom { @@ -93,13 +97,11 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str token = to if !hasFrom { - t.cache = token - t.expiresIn = expiresIn - t.issuedAt = issuedAt + t.updateCachedToken(to, expiresIn, issuedAt) log.Debug("add token to cache") } } else { - token = t.cache + token = cachedToken log.Debug("get token from cache") } @@ -109,6 +111,20 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str return nil } +func (t *tokenHandler) getCachedToken() (string, int, *time.Time) { + t.Lock() + defer t.Unlock() + return t.cache, t.expiresIn, t.issuedAt +} + +func (t *tokenHandler) updateCachedToken(token string, expiresIn int, issuedAt *time.Time) { + t.Lock() + defer t.Unlock() + t.cache = token + t.expiresIn = expiresIn + t.issuedAt = issuedAt +} + // Implements interface Handler type standardTokenHandler struct { tokenHandler