From 451da06bc64f3d7d92597832317d69911cf07710 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Mon, 20 Sep 2021 20:45:54 +0500 Subject: [PATCH 1/6] :recycle: Refactored token revokation --- internal/common/common.go | 7 + internal/model/token.go | 14 +- internal/token/delivery/http/token_http.go | 181 +++++------------- .../token/delivery/http/token_http_test.go | 55 ++++++ internal/token/repository.go | 2 +- internal/token/repository/bolt/bolt_token.go | 57 ------ .../token/repository/memory/memory_token.go | 61 ++++-- internal/token/usecase.go | 4 - internal/token/usecase/token_usecase.go | 83 +------- internal/token/usecase/token_usecase_test.go | 42 ++++ internal/util/util.go | 27 +++ 11 files changed, 238 insertions(+), 295 deletions(-) create mode 100644 internal/common/common.go create mode 100644 internal/token/delivery/http/token_http_test.go delete mode 100644 internal/token/repository/bolt/bolt_token.go create mode 100644 internal/token/usecase/token_usecase_test.go create mode 100644 internal/util/util.go diff --git a/internal/common/common.go b/internal/common/common.go new file mode 100644 index 0000000..ad29957 --- /dev/null +++ b/internal/common/common.go @@ -0,0 +1,7 @@ +package common + +const ( + MIMEApplicationJSON string = "application/json" + MIMETextHTML string = "text/html" + MIMEApplicationXWWWFormUrlencoded string = "application/x-www-form-urlencoded" +) diff --git a/internal/model/token.go b/internal/model/token.go index cb00b5f..4ecbca4 100644 --- a/internal/model/token.go +++ b/internal/model/token.go @@ -1,12 +1,10 @@ package model type Token struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - Scope string `json:"scope"` - Me string `json:"me"` - ClientID string `json:"client_id"` - Profile *Profile `json:"profile,omitempty"` + AccessToken string + ClientID string + Me string + Profile *Profile + Scopes []string + Type string } - -func (Token) Bucket() []byte { return []byte("tokens") } diff --git a/internal/token/delivery/http/token_http.go b/internal/token/delivery/http/token_http.go index 19ffccb..ec5a903 100644 --- a/internal/token/delivery/http/token_http.go +++ b/internal/token/delivery/http/token_http.go @@ -4,183 +4,96 @@ import ( "strings" "github.com/fasthttp/router" - json "github.com/goccy/go-json" + "github.com/goccy/go-json" http "github.com/valyala/fasthttp" - "gitlab.com/toby3d/indieauth/internal/model" - "gitlab.com/toby3d/indieauth/internal/token" + + "source.toby3d.me/website/oauth/internal/common" + "source.toby3d.me/website/oauth/internal/model" + "source.toby3d.me/website/oauth/internal/token" ) type ( - Handler struct { + RequestHandler struct { useCase token.UseCase } - ExchangeRequest struct { - GrantType string - Code string - ClientID string - RedirectURI string - CodeVerifier string - } - - RevokeRequest struct { + RevocationRequest struct { Action string Token string } - ExchangeResponse struct { - AccessToken string `json:"access_token"` - Me string `json:"me"` - Scope string `json:"scope"` - TokenType string `json:"token_type"` - } - - VerificationResponse struct { - Me string `json:"me"` - ClientID string `json:"client_id"` - Scope string `json:"scope"` - } + RevocationResponse struct{} ) -func NewTokenHandler(useCase token.UseCase) *Handler { - return &Handler{ +const ( + Action string = "action" + ActionRevoke string = "revoke" +) + +func NewRequestHandler(useCase token.UseCase) *RequestHandler { + return &RequestHandler{ useCase: useCase, } } -func (h *Handler) Register(r *router.Router) { - r.GET("/token", h.Verification) +func (h *RequestHandler) Register(r *router.Router) { r.POST("/token", h.Update) } -func (h *Handler) Verification(ctx *http.RequestCtx) { +func (h *RequestHandler) Update(ctx *http.RequestCtx) { + ctx.SetStatusCode(http.StatusBadRequest) + + switch string(ctx.FormValue(Action)) { + case ActionRevoke: + h.Revocation(ctx) + } +} + +func (h *RequestHandler) Revocation(ctx *http.RequestCtx) { + ctx.SetContentType(common.MIMEApplicationJSON) + ctx.SetStatusCode(http.StatusOK) + encoder := json.NewEncoder(ctx) - token, err := h.useCase.Verify(ctx, - strings.TrimPrefix(string(ctx.Request.Header.Peek(http.HeaderAuthorization)), "Bearer "), - ) - if err != nil { - ctx.Error(err.Error(), http.StatusBadRequest) - - return - } - - ctx.SetContentType("application/json") - _ = encoder.Encode(&VerificationResponse{ - Me: token.Me, - ClientID: token.ClientID, - Scope: token.Scope, - }) -} - -func (h *Handler) Update(ctx *http.RequestCtx) { - if ctx.PostArgs().Has("action") { - h.Revoke(ctx) - - return - } - - h.Exchange(ctx) -} - -func (r *ExchangeRequest) bind(ctx *http.RequestCtx) error { - if r.GrantType = string(ctx.PostArgs().Peek("grant_type")); r.GrantType != "authorization_code" { - return model.Error{ - Code: model.ErrInvalidRequest.Code, - Description: "'grant_type' must be 'authorization_code'", - } - } - - if r.Code = string(ctx.PostArgs().Peek("code")); r.Code == "" { - return model.Error{ - Code: model.ErrInvalidRequest.Code, - Description: "'code' query is required", - } - } - - if r.ClientID = string(ctx.PostArgs().Peek("client_id")); r.ClientID == "" { - return model.Error{ - Code: model.ErrInvalidRequest.Code, - Description: "'client_id' query is required", - } - } - - if r.RedirectURI = string(ctx.PostArgs().Peek("redirect_uri")); r.RedirectURI == "" { - return model.Error{ - Code: model.ErrInvalidRequest.Code, - Description: "'redirect_uri' query is required", - } - } - - r.CodeVerifier = string(ctx.PostArgs().Peek("code_verifier")) - - return nil -} - -func (h *Handler) Exchange(ctx *http.RequestCtx) { - encoder := json.NewEncoder(ctx) - req := new(ExchangeRequest) - + req := new(RevocationRequest) if err := req.bind(ctx); err != nil { - ctx.Error(err.Error(), http.StatusBadRequest) + ctx.SetStatusCode(http.StatusBadRequest) + encoder.Encode(err) return } - token, err := h.useCase.Exchange(ctx, &model.ExchangeRequest{ - ClientID: req.ClientID, - Code: req.Code, - CodeVerifier: req.CodeVerifier, - RedirectURI: req.RedirectURI, - }) - if err != nil { - ctx.Error(err.Error(), http.StatusInternalServerError) + if err := h.useCase.Revoke(ctx, req.Token); err != nil { + ctx.SetStatusCode(http.StatusBadRequest) + encoder.Encode(err) return } - if token == nil { - ctx.Error(model.ErrUnauthorizedClient.Error(), http.StatusUnauthorized) + if err := encoder.Encode(RevocationResponse{}); err != nil { + ctx.SetStatusCode(http.StatusInternalServerError) + encoder.Encode(err) return } - - ctx.SetContentType("application/json") - _ = encoder.Encode(&ExchangeResponse{ - AccessToken: token.AccessToken, - Me: token.Me, - Scope: token.Scope, - TokenType: token.TokenType, - }) } -func (r *RevokeRequest) bind(ctx *http.RequestCtx) error { - if r.Action = string(ctx.PostArgs().Peek("action")); r.Action != "revoke" { +func (r *RevocationRequest) bind(ctx *http.RequestCtx) error { + if r.Action = string(ctx.FormValue(Action)); !strings.EqualFold(r.Action, ActionRevoke) { return model.Error{ - Code: model.ErrInvalidRequest.Code, - Description: "'action' must be 'revoke'", + Code: model.ErrInvalidRequest, + Description: "request MUST contain 'action' key with value 'revoke'", + URI: "https://indieauth.spec.indieweb.org/#token-revocation-request", } } - if r.Token = string(ctx.PostArgs().Peek("token")); r.Token == "" { + if r.Token = string(ctx.FormValue("token")); r.Token == "" { return model.Error{ - Code: model.ErrInvalidRequest.Code, - Description: "'token' query is required", + Code: model.ErrInvalidRequest, + Description: "request MUST contain the 'token' key with the valid access token as its value", + URI: "https://indieauth.spec.indieweb.org/#token-revocation-request", } } return nil } - -func (h *Handler) Revoke(ctx *http.RequestCtx) { - req := new(RevokeRequest) - if err := req.bind(ctx); err != nil { - ctx.Error(err.Error(), http.StatusBadRequest) - - return - } - - _ = h.useCase.Revoke(ctx, string(ctx.PostArgs().Peek("token"))) - - ctx.SuccessString("application/json", "{}") -} diff --git a/internal/token/delivery/http/token_http_test.go b/internal/token/delivery/http/token_http_test.go new file mode 100644 index 0000000..234747d --- /dev/null +++ b/internal/token/delivery/http/token_http_test.go @@ -0,0 +1,55 @@ +package http_test + +import ( + "context" + "strings" + "testing" + + "github.com/brianvoe/gofakeit" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + http "github.com/valyala/fasthttp" + + "source.toby3d.me/website/oauth/internal/common" + "source.toby3d.me/website/oauth/internal/model" + delivery "source.toby3d.me/website/oauth/internal/token/delivery/http" + repository "source.toby3d.me/website/oauth/internal/token/repository/memory" + "source.toby3d.me/website/oauth/internal/token/usecase" + "source.toby3d.me/website/oauth/internal/util" +) + +func TestRevocation(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + repo := repository.NewMemoryTokenRepository() + accessToken := gofakeit.Password(true, true, true, true, false, 32) + + require.NoError(repo.Create(context.TODO(), &model.Token{ + AccessToken: accessToken, + Type: "Bearer", + ClientID: "https://app.example.com/", + Me: "https://user.example.net/", + Scopes: []string{"create", "update", "delete"}, + })) + + req := http.AcquireRequest() + defer http.ReleaseRequest(req) + + req.Header.SetMethod(http.MethodPost) + req.SetRequestURI("http://localhost/token") + req.Header.SetContentType(common.MIMEApplicationXWWWFormUrlencoded) + req.Header.Set(http.HeaderAccept, common.MIMEApplicationJSON) + req.PostArgs().Set("action", "revoke") + req.PostArgs().Set("token", accessToken) + + resp := http.AcquireResponse() + defer http.ReleaseResponse(resp) + + require.NoError(util.Serve(delivery.NewRequestHandler(usecase.NewTokenUseCase(repo)).Revocation, req, resp)) + assert.Equal(http.StatusOK, resp.StatusCode()) + assert.Equal(`{}`, strings.TrimSpace(string(resp.Body()))) + + token, err := repo.Get(context.TODO(), accessToken) + require.NoError(err) + assert.Nil(token) +} diff --git a/internal/token/repository.go b/internal/token/repository.go index d67cce6..c110b37 100644 --- a/internal/token/repository.go +++ b/internal/token/repository.go @@ -3,7 +3,7 @@ package token import ( "context" - "gitlab.com/toby3d/indieauth/internal/model" + "source.toby3d.me/website/oauth/internal/model" ) type Repository interface { diff --git a/internal/token/repository/bolt/bolt_token.go b/internal/token/repository/bolt/bolt_token.go deleted file mode 100644 index 53d592e..0000000 --- a/internal/token/repository/bolt/bolt_token.go +++ /dev/null @@ -1,57 +0,0 @@ -package bolt - -import ( - "context" - - json "github.com/goccy/go-json" - "gitlab.com/toby3d/indieauth/internal/model" - "gitlab.com/toby3d/indieauth/internal/token" - bolt "go.etcd.io/bbolt" -) - -type boltTokenRepository struct { - db *bolt.DB -} - -func NewBoltTokenRepository(db *bolt.DB) (token.Repository, error) { - if err := db.Update(func(tx *bolt.Tx) error { - _, err := tx.CreateBucketIfNotExists(model.Token{}.Bucket()) - - return err - }); err != nil { - return nil, err - } - - return &boltTokenRepository{ - db: db, - }, nil -} - -func (repo *boltTokenRepository) Create(ctx context.Context, token *model.Token) error { - jsonToken, err := json.Marshal(token) - if err != nil { - return err - } - - return repo.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(model.Token{}.Bucket()).Put([]byte(token.AccessToken), jsonToken) - }) -} - -func (repo *boltTokenRepository) Get(ctx context.Context, token string) (*model.Token, error) { - t := new(model.Token) - - if err := repo.db.View(func(tx *bolt.Tx) error { - return json.Unmarshal(tx.Bucket(model.Token{}.Bucket()).Get([]byte(token)), t) - }); err != nil { - return nil, err - } - - return t, nil -} - -func (repo *boltTokenRepository) Delete(ctx context.Context, token string) error { - return repo.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(model.Token{}.Bucket()).Delete([]byte(token)) - }) -} diff --git a/internal/token/repository/memory/memory_token.go b/internal/token/repository/memory/memory_token.go index 608c235..34e3323 100644 --- a/internal/token/repository/memory/memory_token.go +++ b/internal/token/repository/memory/memory_token.go @@ -4,37 +4,72 @@ import ( "context" "sync" - "gitlab.com/toby3d/indieauth/internal/model" - "gitlab.com/toby3d/indieauth/internal/token" + "source.toby3d.me/website/oauth/internal/model" + "source.toby3d.me/website/oauth/internal/token" ) type memoryTokenRepository struct { - tokens *sync.Map + mutex *sync.RWMutex + tokens []*model.Token } func NewMemoryTokenRepository() token.Repository { return &memoryTokenRepository{ - tokens: new(sync.Map), + mutex: new(sync.RWMutex), + tokens: make([]*model.Token, 0), } } func (repo *memoryTokenRepository) Create(ctx context.Context, token *model.Token) error { - repo.tokens.Store(token.AccessToken, token) + repo.mutex.Lock() - return nil -} + repo.tokens = append(repo.tokens, token) -func (repo *memoryTokenRepository) Delete(ctx context.Context, token string) error { - repo.tokens.Delete(token) + repo.mutex.Unlock() return nil } func (repo *memoryTokenRepository) Get(ctx context.Context, token string) (*model.Token, error) { - t, ok := repo.tokens.Load(token) - if !ok { - return nil, nil + repo.mutex.RLock() + defer repo.mutex.RUnlock() + + for i := range repo.tokens { + if repo.tokens[i].AccessToken != token { + continue + } + + return repo.tokens[i], nil } - return t.(*model.Token), nil + return nil, nil +} + +func (repo *memoryTokenRepository) Delete(ctx context.Context, token string) error { + repo.mutex.RLock() + + for i := range repo.tokens { + if repo.tokens[i].AccessToken != token { + continue + } + + repo.mutex.RUnlock() + repo.mutex.Lock() + + if i < len(repo.tokens)-1 { + copy(repo.tokens[i:], repo.tokens[i+1:]) + } + + repo.tokens[len(repo.tokens)-1] = nil + repo.tokens = repo.tokens[:len(repo.tokens)-1] + + repo.mutex.Unlock() + repo.mutex.RLock() + + break + } + + repo.mutex.RUnlock() + + return nil } diff --git a/internal/token/usecase.go b/internal/token/usecase.go index 02b0cac..f7e4ae7 100644 --- a/internal/token/usecase.go +++ b/internal/token/usecase.go @@ -2,12 +2,8 @@ package token import ( "context" - - "gitlab.com/toby3d/indieauth/internal/model" ) type UseCase interface { - Exchange(ctx context.Context, req *model.ExchangeRequest) (*model.Token, error) - Verify(ctx context.Context, token string) (*model.Token, error) Revoke(ctx context.Context, token string) error } diff --git a/internal/token/usecase/token_usecase.go b/internal/token/usecase/token_usecase.go index de4a0aa..e266447 100644 --- a/internal/token/usecase/token_usecase.go +++ b/internal/token/usecase/token_usecase.go @@ -2,93 +2,20 @@ package usecase import ( "context" - "time" - "gitlab.com/toby3d/indieauth/internal/auth" - "gitlab.com/toby3d/indieauth/internal/model" - "gitlab.com/toby3d/indieauth/internal/pkce" - "gitlab.com/toby3d/indieauth/internal/random" - "gitlab.com/toby3d/indieauth/internal/token" + "source.toby3d.me/website/oauth/internal/token" ) type tokenUseCase struct { - authRepo auth.Repository - tokenRepo token.Repository + tokens token.Repository } -func NewTokenUseCase(authRepo auth.Repository, tokenRepo token.Repository) token.UseCase { +func NewTokenUseCase(tokens token.Repository) token.UseCase { return &tokenUseCase{ - authRepo: authRepo, - tokenRepo: tokenRepo, + tokens: tokens, } } -func (useCase *tokenUseCase) Exchange(ctx context.Context, req *model.ExchangeRequest) (*model.Token, error) { - login, err := useCase.authRepo.Get(ctx, req.Code) - if err != nil { - return nil, err - } - - if login == nil { - return nil, nil - } - - _ = useCase.authRepo.Delete(ctx, login.Code) - - if time.Now().UTC().After(time.Unix(login.CreatedAt, 0).Add(10 * time.Minute)) { - return nil, nil - } - - if login.ClientID != req.ClientID { - return nil, model.Error{ - Code: model.ErrInvalidRequest.Code, - Description: "'client_id' value must be identical to the value at the start of the authorization flow", - } - } - - if login.RedirectURI != req.RedirectURI { - return nil, model.Error{ - Code: model.ErrInvalidRequest.Code, - Description: "'client_id' value must be identical to the value at the start of the authorization flow", - } - } - - if login.CodeChallenge != "" { - code, err := pkce.New(login.CodeChallengeMethod) - if err != nil { - return nil, err - } - - code.Verifier = req.CodeVerifier - - code.Generate() - - if login.CodeChallenge != code.Challenge { - return nil, model.Error{ - Code: model.ErrInvalidRequest.Code, - Description: "PKCE validation failed, please go through the authorization flow again", - } - } - } - - token := new(model.Token) - token.AccessToken = random.New().String(64) - token.ClientID = login.ClientID - token.Me = login.Me - token.Scope = login.Scope - token.TokenType = "Bearer" - - if err := useCase.tokenRepo.Create(ctx, token); err != nil { - return nil, err - } - - return token, nil -} - -func (useCase *tokenUseCase) Verify(ctx context.Context, token string) (*model.Token, error) { - return useCase.tokenRepo.Get(ctx, token) -} - func (useCase *tokenUseCase) Revoke(ctx context.Context, token string) error { - return useCase.tokenRepo.Delete(ctx, token) + return useCase.tokens.Delete(ctx, token) } diff --git a/internal/token/usecase/token_usecase_test.go b/internal/token/usecase/token_usecase_test.go new file mode 100644 index 0000000..e50f595 --- /dev/null +++ b/internal/token/usecase/token_usecase_test.go @@ -0,0 +1,42 @@ +package usecase_test + +import ( + "context" + "testing" + + "github.com/brianvoe/gofakeit" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "source.toby3d.me/website/oauth/internal/model" + repository "source.toby3d.me/website/oauth/internal/token/repository/memory" + "source.toby3d.me/website/oauth/internal/token/usecase" +) + +func TestRevoke(t *testing.T) { + t.Parallel() + + require := require.New(t) + assert := assert.New(t) + repo := repository.NewMemoryTokenRepository() + ucase := usecase.NewTokenUseCase(repo) + accessToken := gofakeit.Password(true, true, true, true, false, 32) + + require.NoError(repo.Create(context.TODO(), &model.Token{ + AccessToken: accessToken, + Type: "Bearer", + ClientID: "https://app.example.com/", + Me: "https://user.example.net/", + Scopes: []string{"create", "update", "delete"}, + })) + + token, err := repo.Get(context.TODO(), accessToken) + require.NoError(err) + assert.NotNil(token) + + require.NoError(ucase.Revoke(context.TODO(), token.AccessToken)) + + token, err = repo.Get(context.TODO(), token.AccessToken) + require.NoError(err) + assert.Nil(token) +} diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..6e4c467 --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,27 @@ +package util + +import ( + "net" + + http "github.com/valyala/fasthttp" + httputil "github.com/valyala/fasthttp/fasthttputil" +) + +func Serve(handler http.RequestHandler, req *http.Request, res *http.Response) error { + ln := httputil.NewInmemoryListener() + defer ln.Close() + + go func() { + if err := http.Serve(ln, handler); err != nil { + panic(err) + } + }() + + client := http.Client{ + Dial: func(addr string) (net.Conn, error) { + return ln.Dial() + }, + } + + return client.Do(req, res) +} From 53098497a51574a74c9d106f77c4c0ffce1d2a93 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Mon, 20 Sep 2021 23:25:08 +0500 Subject: [PATCH 2/6] :recycle: Refactored token verification --- internal/token/delivery/http/token_http.go | 45 ++++++++++++++++--- .../token/delivery/http/token_http_test.go | 44 +++++++++++++++++- internal/token/usecase.go | 3 ++ internal/token/usecase/token_usecase.go | 5 +++ internal/token/usecase/token_usecase_test.go | 22 +++++++++ 5 files changed, 113 insertions(+), 6 deletions(-) diff --git a/internal/token/delivery/http/token_http.go b/internal/token/delivery/http/token_http.go index ec5a903..94d3a2e 100644 --- a/internal/token/delivery/http/token_http.go +++ b/internal/token/delivery/http/token_http.go @@ -1,6 +1,7 @@ package http import ( + "bytes" "strings" "github.com/fasthttp/router" @@ -22,6 +23,12 @@ type ( Token string } + VerificationResponse struct { + Me string `json:"me"` + ClientID string `json:"client_id"` + Scope string `json:"scope"` + } + RevocationResponse struct{} ) @@ -37,12 +44,42 @@ func NewRequestHandler(useCase token.UseCase) *RequestHandler { } func (h *RequestHandler) Register(r *router.Router) { + r.GET("/token", h.Read) r.POST("/token", h.Update) } -func (h *RequestHandler) Update(ctx *http.RequestCtx) { - ctx.SetStatusCode(http.StatusBadRequest) +func (h *RequestHandler) Read(ctx *http.RequestCtx) { + ctx.SetContentType(common.MIMEApplicationJSON) + ctx.SetStatusCode(http.StatusOK) + encoder := json.NewEncoder(ctx) + rawToken := ctx.Request.Header.Peek(http.HeaderAuthorization) + + token, err := h.useCase.Verify(ctx, string(bytes.TrimSpace(bytes.TrimPrefix(rawToken, []byte("Bearer"))))) + if err != nil { + ctx.SetStatusCode(http.StatusBadRequest) + encoder.Encode(err) + + return + } + + if token == nil { + ctx.SetStatusCode(http.StatusUnauthorized) + + return + } + + if err := encoder.Encode(&VerificationResponse{ + Me: token.Me, + ClientID: token.ClientID, + Scope: strings.Join(token.Scopes, " "), + }); err != nil { + ctx.SetStatusCode(http.StatusInternalServerError) + encoder.Encode(err) + } +} + +func (h *RequestHandler) Update(ctx *http.RequestCtx) { switch string(ctx.FormValue(Action)) { case ActionRevoke: h.Revocation(ctx) @@ -70,11 +107,9 @@ func (h *RequestHandler) Revocation(ctx *http.RequestCtx) { return } - if err := encoder.Encode(RevocationResponse{}); err != nil { + if err := encoder.Encode(&RevocationResponse{}); err != nil { ctx.SetStatusCode(http.StatusInternalServerError) encoder.Encode(err) - - return } } diff --git a/internal/token/delivery/http/token_http_test.go b/internal/token/delivery/http/token_http_test.go index 234747d..21acbec 100644 --- a/internal/token/delivery/http/token_http_test.go +++ b/internal/token/delivery/http/token_http_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/brianvoe/gofakeit" + "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" http "github.com/valyala/fasthttp" @@ -18,7 +19,48 @@ import ( "source.toby3d.me/website/oauth/internal/util" ) +func TestVerification(t *testing.T) { + t.Parallel() + + require := require.New(t) + assert := assert.New(t) + repo := repository.NewMemoryTokenRepository() + accessToken := model.Token{ + AccessToken: gofakeit.Password(true, true, true, true, false, 32), + Type: "Bearer", + ClientID: "https://app.example.com/", + Me: "https://user.example.net/", + Scopes: []string{"create", "update", "delete"}, + } + + require.NoError(repo.Create(context.TODO(), &accessToken)) + + req := http.AcquireRequest() + defer http.ReleaseRequest(req) + + req.Header.SetMethod(http.MethodGet) + req.SetRequestURI("http://localhost/token") + req.Header.Set(http.HeaderAccept, common.MIMEApplicationJSON) + req.Header.Set(http.HeaderAuthorization, "Bearer "+accessToken.AccessToken) + + resp := http.AcquireResponse() + defer http.ReleaseResponse(resp) + + require.NoError(util.Serve(delivery.NewRequestHandler(usecase.NewTokenUseCase(repo)).Read, req, resp)) + assert.Equal(http.StatusOK, resp.StatusCode()) + + token := new(delivery.VerificationResponse) + require.NoError(json.Unmarshal(resp.Body(), token)) + assert.Equal(&delivery.VerificationResponse{ + Me: accessToken.Me, + ClientID: accessToken.ClientID, + Scope: strings.Join(accessToken.Scopes, " "), + }, token) +} + func TestRevocation(t *testing.T) { + t.Parallel() + require := require.New(t) assert := assert.New(t) repo := repository.NewMemoryTokenRepository() @@ -45,7 +87,7 @@ func TestRevocation(t *testing.T) { resp := http.AcquireResponse() defer http.ReleaseResponse(resp) - require.NoError(util.Serve(delivery.NewRequestHandler(usecase.NewTokenUseCase(repo)).Revocation, req, resp)) + require.NoError(util.Serve(delivery.NewRequestHandler(usecase.NewTokenUseCase(repo)).Update, req, resp)) assert.Equal(http.StatusOK, resp.StatusCode()) assert.Equal(`{}`, strings.TrimSpace(string(resp.Body()))) diff --git a/internal/token/usecase.go b/internal/token/usecase.go index f7e4ae7..12ae9fb 100644 --- a/internal/token/usecase.go +++ b/internal/token/usecase.go @@ -2,8 +2,11 @@ package token import ( "context" + + "source.toby3d.me/website/oauth/internal/model" ) type UseCase interface { + Verify(ctx context.Context, token string) (*model.Token, error) Revoke(ctx context.Context, token string) error } diff --git a/internal/token/usecase/token_usecase.go b/internal/token/usecase/token_usecase.go index e266447..4a00030 100644 --- a/internal/token/usecase/token_usecase.go +++ b/internal/token/usecase/token_usecase.go @@ -3,6 +3,7 @@ package usecase import ( "context" + "source.toby3d.me/website/oauth/internal/model" "source.toby3d.me/website/oauth/internal/token" ) @@ -16,6 +17,10 @@ func NewTokenUseCase(tokens token.Repository) token.UseCase { } } +func (useCase *tokenUseCase) Verify(ctx context.Context, token string) (*model.Token, error) { + return useCase.tokens.Get(ctx, token) +} + func (useCase *tokenUseCase) Revoke(ctx context.Context, token string) error { return useCase.tokens.Delete(ctx, token) } diff --git a/internal/token/usecase/token_usecase_test.go b/internal/token/usecase/token_usecase_test.go index e50f595..54a7d68 100644 --- a/internal/token/usecase/token_usecase_test.go +++ b/internal/token/usecase/token_usecase_test.go @@ -13,6 +13,28 @@ import ( "source.toby3d.me/website/oauth/internal/token/usecase" ) +func TestVerify(t *testing.T) { + t.Parallel() + + require := require.New(t) + assert := assert.New(t) + repo := repository.NewMemoryTokenRepository() + ucase := usecase.NewTokenUseCase(repo) + accessToken := &model.Token{ + AccessToken: gofakeit.Password(true, true, true, true, false, 32), + Type: "Bearer", + ClientID: "https://app.example.com/", + Me: "https://user.example.net/", + Scopes: []string{"create", "update", "delete"}, + } + + require.NoError(repo.Create(context.TODO(), accessToken)) + + token, err := ucase.Verify(context.TODO(), accessToken.AccessToken) + require.NoError(err) + assert.Equal(accessToken, token) +} + func TestRevoke(t *testing.T) { t.Parallel() From 22187f309ba99c09496750d6301ef1867b5682e6 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Thu, 23 Sep 2021 02:32:40 +0500 Subject: [PATCH 3/6] :art: Format token package usage --- internal/model/error.go | 67 +++++----- internal/token/repository.go | 14 +- .../token/repository/memory/memory_token.go | 81 +++++------ .../repository/memory/memory_token_test.go | 126 ++++++++++++++++++ internal/token/usecase/token_usecase.go | 2 +- internal/token/usecase/token_usecase_test.go | 7 +- 6 files changed, 205 insertions(+), 92 deletions(-) create mode 100644 internal/token/repository/memory/memory_token_test.go diff --git a/internal/model/error.go b/internal/model/error.go index 75eb59a..c397d7a 100644 --- a/internal/model/error.go +++ b/internal/model/error.go @@ -6,8 +6,6 @@ import ( "golang.org/x/xerrors" ) -// TODO(toby3d): make more informative errors. -// See https://indieauth.spec.indieweb.org/#authorization-request type Error struct { Code string `json:"error"` Description string `json:"error_description,omitempty"` @@ -15,48 +13,43 @@ type Error struct { Frame xerrors.Frame `json:"-"` } -var ( - ErrInvalidRequest Error = Error{ - Code: "invalid_request", - Description: "the request is missing a required parameter, includes an invalid parameter value, or is otherwise malformed", - } - ErrUnauthorizedClient Error = Error{ - Code: "unauthorized_client", - Description: "the client is not authorized to request an authorization code using this method", - } - ErrAccessDenied Error = Error{ - Code: "access_denied", - Description: "", - } - ErrUnsupportedResponseType Error = Error{ - Code: "unsupported_response_type", - Description: "the authorization server does not support obtaining an authorization code using this method", - } - ErrInvalidScope Error = Error{ - Code: "invalid_scope", - Description: "the requested scope is invalid, unknown, or malformed", - } - ErrServerError Error = Error{ - Code: "server_error", - Description: "the authorization server encountered an unexpected condition which prevented it from fulfilling the request", - } - ErrTemporarilyUnavailable Error = Error{ - Code: "temporarily_unavailable", - Description: "the authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server", - } +const ( + ErrAccessDenied string = "access_denied" + ErrInvalidClient string = "invalid_client" + ErrInvalidGrant string = "invalid_grant" + ErrInvalidRequest string = "invalid_request" + ErrInvalidScope string = "invalid_scope" + ErrInvalidToken string = "invalid_token" + ErrServerError string = "server_error" + ErrTemporarilyUnavailable string = "temporarily_unavailable" + ErrUnauthorizedClient string = "unauthorized_client" + ErrUnsupportedResponseType string = "unsupported_response_type" ) -func (e Error) FormatError(p xerrors.Printer) error { - p.Printf("%s: %s", e.Code, e.Description) - e.Frame.Format(p) +const errorColor string = "\033[31m" - return nil +func (e Error) Error() string { + return fmt.Sprint(e) } func (e Error) Format(s fmt.State, r rune) { xerrors.FormatError(e, s, r) } -func (e Error) Error() string { - return fmt.Sprint(e) +func (e Error) FormatError(p xerrors.Printer) error { + p.Print(errorColor, e.Code) + + if e.Description != "" { + p.Printf(": %s", e.Description) + } + + if e.URI != "" { + p.Printf("%4s", e.URI) + } + + if p.Detail() { + e.Frame.Format(p) + } + + return nil } diff --git a/internal/token/repository.go b/internal/token/repository.go index c110b37..5e89450 100644 --- a/internal/token/repository.go +++ b/internal/token/repository.go @@ -3,11 +3,19 @@ package token import ( "context" + "golang.org/x/xerrors" "source.toby3d.me/website/oauth/internal/model" ) type Repository interface { - Create(ctx context.Context, token *model.Token) error - Get(ctx context.Context, token string) (*model.Token, error) - Delete(ctx context.Context, token string) error + Get(ctx context.Context, accessToken string) (*model.Token, error) + Create(ctx context.Context, accessToken *model.Token) error + Update(ctx context.Context, accessToken *model.Token) error + Remove(ctx context.Context, accessToken string) error +} + +var ErrExist error = model.Error{ + Code: model.ErrInvalidRequest, + Description: "this token is already exists", + Frame: xerrors.Caller(1), } diff --git a/internal/token/repository/memory/memory_token.go b/internal/token/repository/memory/memory_token.go index 34e3323..aa0d248 100644 --- a/internal/token/repository/memory/memory_token.go +++ b/internal/token/repository/memory/memory_token.go @@ -9,67 +9,50 @@ import ( ) type memoryTokenRepository struct { - mutex *sync.RWMutex - tokens []*model.Token + tokens *sync.Map } -func NewMemoryTokenRepository() token.Repository { +func NewMemoryTokenRepository(tokens *sync.Map) token.Repository { return &memoryTokenRepository{ - mutex: new(sync.RWMutex), - tokens: make([]*model.Token, 0), + tokens: tokens, } } -func (repo *memoryTokenRepository) Create(ctx context.Context, token *model.Token) error { - repo.mutex.Lock() +func (repo *memoryTokenRepository) Get(ctx context.Context, accessToken string) (*model.Token, error) { + src, ok := repo.tokens.Load(accessToken) + if !ok { + return nil, nil + } - repo.tokens = append(repo.tokens, token) + result, ok := src.(*model.Token) + if !ok { + return nil, nil + } - repo.mutex.Unlock() + return result, nil +} + +func (repo *memoryTokenRepository) Create(ctx context.Context, accessToken *model.Token) error { + t, err := repo.Get(ctx, accessToken.AccessToken) + if err != nil { + return err + } + + if t != nil { + return token.ErrExist + } + + return repo.Update(ctx, accessToken) +} + +func (repo *memoryTokenRepository) Update(ctx context.Context, accessToken *model.Token) error { + repo.tokens.Store(accessToken.AccessToken, accessToken) return nil } -func (repo *memoryTokenRepository) Get(ctx context.Context, token string) (*model.Token, error) { - repo.mutex.RLock() - defer repo.mutex.RUnlock() - - for i := range repo.tokens { - if repo.tokens[i].AccessToken != token { - continue - } - - return repo.tokens[i], nil - } - - return nil, nil -} - -func (repo *memoryTokenRepository) Delete(ctx context.Context, token string) error { - repo.mutex.RLock() - - for i := range repo.tokens { - if repo.tokens[i].AccessToken != token { - continue - } - - repo.mutex.RUnlock() - repo.mutex.Lock() - - if i < len(repo.tokens)-1 { - copy(repo.tokens[i:], repo.tokens[i+1:]) - } - - repo.tokens[len(repo.tokens)-1] = nil - repo.tokens = repo.tokens[:len(repo.tokens)-1] - - repo.mutex.Unlock() - repo.mutex.RLock() - - break - } - - repo.mutex.RUnlock() +func (repo *memoryTokenRepository) Remove(ctx context.Context, accessToken string) error { + repo.tokens.Delete(accessToken) return nil } diff --git a/internal/token/repository/memory/memory_token_test.go b/internal/token/repository/memory/memory_token_test.go new file mode 100644 index 0000000..4db6692 --- /dev/null +++ b/internal/token/repository/memory/memory_token_test.go @@ -0,0 +1,126 @@ +package memory_test + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "source.toby3d.me/website/oauth/internal/model" + "source.toby3d.me/website/oauth/internal/random" + "source.toby3d.me/website/oauth/internal/token" + "source.toby3d.me/website/oauth/internal/token/repository/memory" +) + +func TestGet(t *testing.T) { + t.Parallel() + + store := new(sync.Map) + accessToken := &model.Token{ + AccessToken: random.New().String(32), + ClientID: "https://app.example.com/", + Me: "https://toby3d.me/", + Profile: &model.Profile{ + Name: "Maxim Lebedev", + URL: "https://toby3d.me/", + Photo: "https://toby3d.me/photo.jpg", + Email: "hey@toby3d.me", + }, + Scopes: []string{"read", "update", "delete"}, + Type: "Bearer", + } + + store.Store(accessToken.AccessToken, accessToken) + + result, err := memory.NewMemoryTokenRepository(store).Get(context.TODO(), accessToken.AccessToken) + require.NoError(t, err) + assert.Equal(t, accessToken, result) +} + +func TestCreate(t *testing.T) { + t.Parallel() + + store := new(sync.Map) + accessToken := &model.Token{ + AccessToken: random.New().String(32), + ClientID: "https://app.example.com/", + Me: "https://toby3d.me/", + Profile: &model.Profile{ + Name: "Maxim Lebedev", + URL: "https://toby3d.me/", + Photo: "https://toby3d.me/photo.jpg", + Email: "hey@toby3d.me", + }, + Scopes: []string{"read", "update", "delete"}, + Type: "Bearer", + } + + repo := memory.NewMemoryTokenRepository(store) + require.NoError(t, repo.Create(context.TODO(), accessToken)) + + result, ok := store.Load(accessToken.AccessToken) + assert.True(t, ok) + assert.Equal(t, accessToken, result) + + assert.EqualError(t, repo.Create(context.TODO(), accessToken), token.ErrExist.Error()) +} + +func TestUpdate(t *testing.T) { + t.Parallel() + + store := new(sync.Map) + accessToken := &model.Token{ + AccessToken: random.New().String(32), + ClientID: "https://app.example.com/", + Me: "https://toby3d.me/", + Profile: &model.Profile{ + Name: "Maxim Lebedev", + URL: "https://toby3d.me/", + Photo: "https://toby3d.me/photo.jpg", + Email: "hey@toby3d.me", + }, + Scopes: []string{"read", "update", "delete"}, + Type: "Bearer", + } + + store.Store(accessToken.AccessToken, accessToken) + + tokenCopy := *accessToken + tokenCopy.ClientID = "https://client.example.com/" + tokenCopy.Me = "https://toby3d.ru/" + + require.NoError(t, memory.NewMemoryTokenRepository(store).Update(context.TODO(), &tokenCopy)) + + result, ok := store.Load(accessToken.AccessToken) + assert.True(t, ok) + assert.NotEqual(t, accessToken, result) + assert.Equal(t, &tokenCopy, result) +} + +func TestDelete(t *testing.T) { + t.Parallel() + + store := new(sync.Map) + accessToken := &model.Token{ + AccessToken: random.New().String(32), + ClientID: "https://app.example.com/", + Me: "https://toby3d.me/", + Profile: &model.Profile{ + Name: "Maxim Lebedev", + URL: "https://toby3d.me/", + Photo: "https://toby3d.me/photo.jpg", + Email: "hey@toby3d.me", + }, + Scopes: []string{"read", "update", "delete"}, + Type: "Bearer", + } + + store.Store(accessToken.AccessToken, accessToken) + + require.NoError(t, memory.NewMemoryTokenRepository(store).Remove(context.TODO(), accessToken.AccessToken)) + + result, ok := store.Load(accessToken.AccessToken) + assert.False(t, ok) + assert.Nil(t, result) +} diff --git a/internal/token/usecase/token_usecase.go b/internal/token/usecase/token_usecase.go index 4a00030..507eaf4 100644 --- a/internal/token/usecase/token_usecase.go +++ b/internal/token/usecase/token_usecase.go @@ -22,5 +22,5 @@ func (useCase *tokenUseCase) Verify(ctx context.Context, token string) (*model.T } func (useCase *tokenUseCase) Revoke(ctx context.Context, token string) error { - return useCase.tokens.Delete(ctx, token) + return useCase.tokens.Remove(ctx, token) } diff --git a/internal/token/usecase/token_usecase_test.go b/internal/token/usecase/token_usecase_test.go index 54a7d68..8232b01 100644 --- a/internal/token/usecase/token_usecase_test.go +++ b/internal/token/usecase/token_usecase_test.go @@ -2,6 +2,7 @@ package usecase_test import ( "context" + "sync" "testing" "github.com/brianvoe/gofakeit" @@ -18,7 +19,8 @@ func TestVerify(t *testing.T) { require := require.New(t) assert := assert.New(t) - repo := repository.NewMemoryTokenRepository() + store := new(sync.Map) + repo := repository.NewMemoryTokenRepository(store) ucase := usecase.NewTokenUseCase(repo) accessToken := &model.Token{ AccessToken: gofakeit.Password(true, true, true, true, false, 32), @@ -40,7 +42,8 @@ func TestRevoke(t *testing.T) { require := require.New(t) assert := assert.New(t) - repo := repository.NewMemoryTokenRepository() + store := new(sync.Map) + repo := repository.NewMemoryTokenRepository(store) ucase := usecase.NewTokenUseCase(repo) accessToken := gofakeit.Password(true, true, true, true, false, 32) From fafb4bbc9415ec95a7c0c19639c0fd2555042b31 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Thu, 23 Sep 2021 04:26:13 +0500 Subject: [PATCH 4/6] :card_file_box: Created bbolt repository for tokens --- internal/token/repository/bolt/bolt_token.go | 125 ++++++++++++ .../token/repository/bolt/bolt_token_test.go | 185 ++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 internal/token/repository/bolt/bolt_token.go create mode 100644 internal/token/repository/bolt/bolt_token_test.go diff --git a/internal/token/repository/bolt/bolt_token.go b/internal/token/repository/bolt/bolt_token.go new file mode 100644 index 0000000..ed6aefa --- /dev/null +++ b/internal/token/repository/bolt/bolt_token.go @@ -0,0 +1,125 @@ +package bolt + +import ( + "context" + "strings" + + json "github.com/goccy/go-json" + "github.com/pkg/errors" + "go.etcd.io/bbolt" + bolt "go.etcd.io/bbolt" + "golang.org/x/xerrors" + "source.toby3d.me/website/oauth/internal/model" + "source.toby3d.me/website/oauth/internal/token" +) + +type ( + Token struct { + AccessToken string `json:"access_token"` + ClientID string `json:"client_id"` + Me string `json:"me"` + Scope string `json:"scope"` + Type string `json:"type"` + } + + boltTokenRepository struct { + db *bolt.DB + } +) + +var ErrNotExist error = xerrors.New("key not exist") + +func NewBoltTokenRepository(db *bolt.DB) (token.Repository, error) { + if err := db.Update(func(tx *bbolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(Token{}.Bucket()) + + return err + }); err != nil { + return nil, err + } + + return &boltTokenRepository{ + db: db, + }, nil +} + +func (repo *boltTokenRepository) Get(ctx context.Context, accessToken string) (*model.Token, error) { + result := new(model.Token) + err := repo.db.View(func(tx *bolt.Tx) error { + if src := tx.Bucket(Token{}.Bucket()).Get([]byte(accessToken)); src != nil { + return NewToken().Bind(src, result) + } + + return ErrNotExist + }) + if err != nil && !xerrors.Is(err, ErrNotExist) { + return nil, err + } + + if xerrors.Is(err, ErrNotExist) { + return nil, nil + } + + return result, nil +} + +func (repo *boltTokenRepository) Create(ctx context.Context, accessToken *model.Token) error { + t, err := repo.Get(ctx, accessToken.AccessToken) + if err != nil { + return errors.Wrap(err, "failed to verify the existence of the token") + } + + if t != nil { + return token.ErrExist + } + + return repo.Update(ctx, accessToken) +} + +func (repo *boltTokenRepository) Update(ctx context.Context, accessToken *model.Token) error { + dto := NewToken() + dto.Populate(accessToken) + + src, err := json.Marshal(dto) + if err != nil { + return errors.Wrap(err, "failed to marshal token") + } + + return repo.db.Update(func(tx *bolt.Tx) error { + return tx.Bucket(dto.Bucket()).Put([]byte(dto.AccessToken), src) + }) +} + +func (repo *boltTokenRepository) Remove(ctx context.Context, accessToken string) error { + return repo.db.Update(func(tx *bolt.Tx) error { + return tx.Bucket(Token{}.Bucket()).Delete([]byte(accessToken)) + }) +} + +func NewToken() *Token { + return new(Token) +} + +func (Token) Bucket() []byte { return []byte("tokens") } + +func (t *Token) Populate(src *model.Token) { + t.AccessToken = src.AccessToken + t.ClientID = src.ClientID + t.Me = src.Me + t.Scope = strings.Join(src.Scopes, " ") + t.Type = src.Type +} + +func (t *Token) Bind(src []byte, dst *model.Token) error { + if err := json.Unmarshal(src, t); err != nil { + return err + } + + dst.AccessToken = t.AccessToken + dst.ClientID = t.ClientID + dst.Me = t.Me + dst.Scopes = strings.Fields(t.Scope) + dst.Type = t.Type + + return nil +} diff --git a/internal/token/repository/bolt/bolt_token_test.go b/internal/token/repository/bolt/bolt_token_test.go new file mode 100644 index 0000000..f709477 --- /dev/null +++ b/internal/token/repository/bolt/bolt_token_test.go @@ -0,0 +1,185 @@ +package bolt_test + +import ( + "context" + "log" + "os" + "path/filepath" + "testing" + + json "github.com/goccy/go-json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.etcd.io/bbolt" + "source.toby3d.me/website/oauth/internal/model" + "source.toby3d.me/website/oauth/internal/random" + "source.toby3d.me/website/oauth/internal/token" + "source.toby3d.me/website/oauth/internal/token/repository/bolt" +) + +var ( + db *bbolt.DB + repo token.Repository +) + +func TestMain(m *testing.M) { + var err error + + dbPath := filepath.Join("..", "..", "..", "..", "test", "testing.db") + if db, err = bbolt.Open(dbPath, os.ModePerm, nil); err != nil { + log.Fatalln(err) + } + + if repo, err = bolt.NewBoltTokenRepository(db); err != nil { + _ = db.Close() + + log.Fatalln(err) + } + + code := m.Run() + _ = db.Close() + _ = os.RemoveAll(dbPath) + + os.Exit(code) +} + +func TestGet(t *testing.T) { + t.Parallel() + + accessToken := random.New().String(32) + + t.Cleanup(func() { + _ = db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken)) + }) + }) + + src, err := json.Marshal(&bolt.Token{ + AccessToken: accessToken, + ClientID: "https://app.example.com/", + Me: "https://toby3d.me/", + Scope: "read update delete", + Type: "Bearer", + }) + require.NoError(t, err) + + require.NoError(t, db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket(bolt.Token{}.Bucket()).Put([]byte(accessToken), src) + })) + + tkn, err := repo.Get(context.TODO(), accessToken) + require.NoError(t, err) + assert.Equal(t, &model.Token{ + AccessToken: accessToken, + ClientID: "https://app.example.com/", + Me: "https://toby3d.me/", + Scopes: []string{"read", "update", "delete"}, + Type: "Bearer", + }, tkn) +} + +func TestCreate(t *testing.T) { + t.Parallel() + + accessToken := &model.Token{ + AccessToken: random.New().String(32), + ClientID: "https://app.example.com/", + Me: "https://toby3d.me/", + Scopes: []string{"read", "update", "delete"}, + Type: "Bearer", + } + + t.Cleanup(func() { + _ = db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken.AccessToken)) + }) + }) + + require.NoError(t, repo.Create(context.TODO(), accessToken)) + + result := new(model.Token) + require.NoError(t, db.View(func(tx *bbolt.Tx) error { + return bolt.NewToken().Bind(tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(accessToken.AccessToken)), result) + })) + assert.Equal(t, accessToken, result) + + assert.EqualError(t, repo.Create(context.TODO(), accessToken), token.ErrExist.Error()) +} + +func TestUpdate(t *testing.T) { + t.Parallel() + + accessToken := random.New().String(32) + + t.Cleanup(func() { + _ = db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken)) + }) + }) + + src, err := json.Marshal(&bolt.Token{ + AccessToken: accessToken, + ClientID: "https://app.example.com/", + Me: "https://toby3d.me/", + Scope: "read update delete", + Type: "Bearer", + }) + require.NoError(t, err) + + require.NoError(t, db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket(bolt.Token{}.Bucket()).Put([]byte(accessToken), src) + })) + + require.NoError(t, repo.Update(context.TODO(), &model.Token{ + AccessToken: accessToken, + ClientID: "https://client.example.com/", + Me: "https://toby3d.ru/", + Scopes: []string{"read"}, + Type: "Bearer", + })) + + result := new(model.Token) + require.NoError(t, db.View(func(tx *bbolt.Tx) error { + return bolt.NewToken().Bind(tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(accessToken)), result) + })) + assert.Equal(t, &model.Token{ + AccessToken: accessToken, + ClientID: "https://client.example.com/", + Me: "https://toby3d.ru/", + Scopes: []string{"read"}, + Type: "Bearer", + }, result) +} + +func TestDelete(t *testing.T) { + t.Parallel() + + accessToken := random.New().String(32) + + t.Cleanup(func() { + _ = db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken)) + }) + }) + + src, err := json.Marshal(&bolt.Token{ + AccessToken: accessToken, + ClientID: "https://app.example.com/", + Me: "https://toby3d.me/", + Scope: "read update delete", + Type: "Bearer", + }) + require.NoError(t, err) + + require.NoError(t, db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket(bolt.Token{}.Bucket()).Put([]byte(accessToken), src) + })) + + require.NoError(t, repo.Remove(context.TODO(), accessToken)) + + require.NoError(t, db.View(func(tx *bbolt.Tx) error { + assert.Nil(t, tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(accessToken))) + + return nil + })) +} From bb1d908c9ab89ab526b49892f4f7fc8a22098383 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Thu, 23 Sep 2021 22:46:44 +0500 Subject: [PATCH 5/6] :art: Format token package --- internal/model/profile.go | 12 ++++--- internal/model/token.go | 13 ++++++-- internal/model/url.go | 3 ++ internal/token/delivery/http/token_http.go | 2 +- .../token/delivery/http/token_http_test.go | 7 +++-- internal/token/repository.go | 3 +- internal/token/repository/bolt/bolt_token.go | 11 ++++--- .../token/repository/bolt/bolt_token_test.go | 31 ++++++++++--------- .../repository/memory/memory_token_test.go | 1 + internal/token/usecase.go | 4 +-- internal/token/usecase/token_usecase.go | 8 ++--- 11 files changed, 59 insertions(+), 36 deletions(-) create mode 100644 internal/model/url.go diff --git a/internal/model/profile.go b/internal/model/profile.go index 33e376a..a04b0f6 100644 --- a/internal/model/profile.go +++ b/internal/model/profile.go @@ -1,8 +1,12 @@ package model type Profile struct { - Name string `json:"name"` - URL string `json:"url"` - Photo string `json:"photo"` - Email string `json:"email,omitempty"` + Name string + URL URL + Photo URL + Email string +} + +func NewProfile() *Profile { + return new(Profile) } diff --git a/internal/model/token.go b/internal/model/token.go index 4ecbca4..1e101d4 100644 --- a/internal/model/token.go +++ b/internal/model/token.go @@ -1,10 +1,17 @@ package model type Token struct { - AccessToken string - ClientID string - Me string Profile *Profile Scopes []string + AccessToken string Type string + Me URL + ClientID URL +} + +func NewToken() *Token { + t := new(Token) + t.Scopes = make([]string, 0) + + return t } diff --git a/internal/model/url.go b/internal/model/url.go new file mode 100644 index 0000000..55a4b36 --- /dev/null +++ b/internal/model/url.go @@ -0,0 +1,3 @@ +package model + +type URL string diff --git a/internal/token/delivery/http/token_http.go b/internal/token/delivery/http/token_http.go index 94d3a2e..ae34eff 100644 --- a/internal/token/delivery/http/token_http.go +++ b/internal/token/delivery/http/token_http.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/fasthttp/router" - "github.com/goccy/go-json" + json "github.com/goccy/go-json" http "github.com/valyala/fasthttp" "source.toby3d.me/website/oauth/internal/common" diff --git a/internal/token/delivery/http/token_http_test.go b/internal/token/delivery/http/token_http_test.go index 21acbec..d8d152a 100644 --- a/internal/token/delivery/http/token_http_test.go +++ b/internal/token/delivery/http/token_http_test.go @@ -3,6 +3,7 @@ package http_test import ( "context" "strings" + "sync" "testing" "github.com/brianvoe/gofakeit" @@ -24,7 +25,8 @@ func TestVerification(t *testing.T) { require := require.New(t) assert := assert.New(t) - repo := repository.NewMemoryTokenRepository() + store := new(sync.Map) + repo := repository.NewMemoryTokenRepository(store) accessToken := model.Token{ AccessToken: gofakeit.Password(true, true, true, true, false, 32), Type: "Bearer", @@ -63,7 +65,8 @@ func TestRevocation(t *testing.T) { require := require.New(t) assert := assert.New(t) - repo := repository.NewMemoryTokenRepository() + store := new(sync.Map) + repo := repository.NewMemoryTokenRepository(store) accessToken := gofakeit.Password(true, true, true, true, false, 32) require.NoError(repo.Create(context.TODO(), &model.Token{ diff --git a/internal/token/repository.go b/internal/token/repository.go index 5e89450..c689d6d 100644 --- a/internal/token/repository.go +++ b/internal/token/repository.go @@ -4,6 +4,7 @@ import ( "context" "golang.org/x/xerrors" + "source.toby3d.me/website/oauth/internal/model" ) @@ -15,7 +16,7 @@ type Repository interface { } var ErrExist error = model.Error{ - Code: model.ErrInvalidRequest, + Code: "invalid_request", Description: "this token is already exists", Frame: xerrors.Caller(1), } diff --git a/internal/token/repository/bolt/bolt_token.go b/internal/token/repository/bolt/bolt_token.go index ed6aefa..3897e63 100644 --- a/internal/token/repository/bolt/bolt_token.go +++ b/internal/token/repository/bolt/bolt_token.go @@ -9,6 +9,7 @@ import ( "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt" "golang.org/x/xerrors" + "source.toby3d.me/website/oauth/internal/model" "source.toby3d.me/website/oauth/internal/token" ) @@ -44,7 +45,7 @@ func NewBoltTokenRepository(db *bolt.DB) (token.Repository, error) { } func (repo *boltTokenRepository) Get(ctx context.Context, accessToken string) (*model.Token, error) { - result := new(model.Token) + result := model.NewToken() err := repo.db.View(func(tx *bolt.Tx) error { if src := tx.Bucket(Token{}.Bucket()).Get([]byte(accessToken)); src != nil { return NewToken().Bind(src, result) @@ -104,8 +105,8 @@ func (Token) Bucket() []byte { return []byte("tokens") } func (t *Token) Populate(src *model.Token) { t.AccessToken = src.AccessToken - t.ClientID = src.ClientID - t.Me = src.Me + t.ClientID = string(src.ClientID) + t.Me = string(src.Me) t.Scope = strings.Join(src.Scopes, " ") t.Type = src.Type } @@ -116,10 +117,10 @@ func (t *Token) Bind(src []byte, dst *model.Token) error { } dst.AccessToken = t.AccessToken - dst.ClientID = t.ClientID - dst.Me = t.Me dst.Scopes = strings.Fields(t.Scope) dst.Type = t.Type + dst.ClientID = model.URL(t.ClientID) + dst.Me = model.URL(t.Me) return nil } diff --git a/internal/token/repository/bolt/bolt_token_test.go b/internal/token/repository/bolt/bolt_token_test.go index f709477..c4e31a8 100644 --- a/internal/token/repository/bolt/bolt_token_test.go +++ b/internal/token/repository/bolt/bolt_token_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" + "source.toby3d.me/website/oauth/internal/model" "source.toby3d.me/website/oauth/internal/random" "source.toby3d.me/website/oauth/internal/token" @@ -81,29 +82,31 @@ func TestGet(t *testing.T) { func TestCreate(t *testing.T) { t.Parallel() - accessToken := &model.Token{ - AccessToken: random.New().String(32), + accessToken := random.New().String(32) + + t.Cleanup(func() { + _ = db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken)) + }) + }) + + tkn := &model.Token{ + AccessToken: accessToken, ClientID: "https://app.example.com/", Me: "https://toby3d.me/", Scopes: []string{"read", "update", "delete"}, Type: "Bearer", } - t.Cleanup(func() { - _ = db.Update(func(tx *bbolt.Tx) error { - return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken.AccessToken)) - }) - }) + require.NoError(t, repo.Create(context.TODO(), tkn)) - require.NoError(t, repo.Create(context.TODO(), accessToken)) - - result := new(model.Token) + result := model.NewToken() require.NoError(t, db.View(func(tx *bbolt.Tx) error { - return bolt.NewToken().Bind(tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(accessToken.AccessToken)), result) + return bolt.NewToken().Bind(tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(tkn.AccessToken)), result) })) - assert.Equal(t, accessToken, result) + assert.Equal(t, tkn, result) - assert.EqualError(t, repo.Create(context.TODO(), accessToken), token.ErrExist.Error()) + assert.EqualError(t, repo.Create(context.TODO(), tkn), token.ErrExist.Error()) } func TestUpdate(t *testing.T) { @@ -138,7 +141,7 @@ func TestUpdate(t *testing.T) { Type: "Bearer", })) - result := new(model.Token) + result := model.NewToken() require.NoError(t, db.View(func(tx *bbolt.Tx) error { return bolt.NewToken().Bind(tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(accessToken)), result) })) diff --git a/internal/token/repository/memory/memory_token_test.go b/internal/token/repository/memory/memory_token_test.go index 4db6692..455db3c 100644 --- a/internal/token/repository/memory/memory_token_test.go +++ b/internal/token/repository/memory/memory_token_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "source.toby3d.me/website/oauth/internal/model" "source.toby3d.me/website/oauth/internal/random" "source.toby3d.me/website/oauth/internal/token" diff --git a/internal/token/usecase.go b/internal/token/usecase.go index 12ae9fb..c1bb153 100644 --- a/internal/token/usecase.go +++ b/internal/token/usecase.go @@ -7,6 +7,6 @@ import ( ) type UseCase interface { - Verify(ctx context.Context, token string) (*model.Token, error) - Revoke(ctx context.Context, token string) error + Verify(ctx context.Context, accessToken string) (*model.Token, error) + Revoke(ctx context.Context, accessToken string) error } diff --git a/internal/token/usecase/token_usecase.go b/internal/token/usecase/token_usecase.go index 507eaf4..2bbf196 100644 --- a/internal/token/usecase/token_usecase.go +++ b/internal/token/usecase/token_usecase.go @@ -17,10 +17,10 @@ func NewTokenUseCase(tokens token.Repository) token.UseCase { } } -func (useCase *tokenUseCase) Verify(ctx context.Context, token string) (*model.Token, error) { - return useCase.tokens.Get(ctx, token) +func (useCase *tokenUseCase) Verify(ctx context.Context, accessToken string) (*model.Token, error) { + return useCase.tokens.Get(ctx, accessToken) } -func (useCase *tokenUseCase) Revoke(ctx context.Context, token string) error { - return useCase.tokens.Remove(ctx, token) +func (useCase *tokenUseCase) Revoke(ctx context.Context, accessToken string) error { + return useCase.tokens.Remove(ctx, accessToken) } From a8bc4587ed217ca061fc11348e8fddd80ee332fe Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Thu, 23 Sep 2021 23:27:59 +0500 Subject: [PATCH 6/6] :rotating_light: Removed linter warnings for token --- internal/token/delivery/http/token_http.go | 43 +++++++++----- .../token/delivery/http/token_http_test.go | 3 +- internal/token/repository.go | 2 +- internal/token/repository/bolt/bolt_token.go | 57 ++++++++++++------- .../token/repository/bolt/bolt_token_test.go | 19 ++++++- .../repository/memory/memory_token_test.go | 1 - internal/token/usecase/token_usecase.go | 14 ++++- internal/token/usecase/token_usecase_test.go | 3 +- 8 files changed, 102 insertions(+), 40 deletions(-) diff --git a/internal/token/delivery/http/token_http.go b/internal/token/delivery/http/token_http.go index ae34eff..99d314a 100644 --- a/internal/token/delivery/http/token_http.go +++ b/internal/token/delivery/http/token_http.go @@ -7,7 +7,7 @@ import ( "github.com/fasthttp/router" json "github.com/goccy/go-json" http "github.com/valyala/fasthttp" - + "golang.org/x/xerrors" "source.toby3d.me/website/oauth/internal/common" "source.toby3d.me/website/oauth/internal/model" "source.toby3d.me/website/oauth/internal/token" @@ -23,10 +23,11 @@ type ( Token string } + //nolint: tagliatelle VerificationResponse struct { - Me string `json:"me"` - ClientID string `json:"client_id"` - Scope string `json:"scope"` + Me model.URL `json:"me"` + ClientID model.URL `json:"client_id"` + Scope string `json:"scope"` } RevocationResponse struct{} @@ -58,7 +59,10 @@ func (h *RequestHandler) Read(ctx *http.RequestCtx) { token, err := h.useCase.Verify(ctx, string(bytes.TrimSpace(bytes.TrimPrefix(rawToken, []byte("Bearer"))))) if err != nil { ctx.SetStatusCode(http.StatusBadRequest) - encoder.Encode(err) + + if err = encoder.Encode(err); err != nil { + ctx.Error(err.Error(), http.StatusInternalServerError) + } return } @@ -75,13 +79,15 @@ func (h *RequestHandler) Read(ctx *http.RequestCtx) { Scope: strings.Join(token.Scopes, " "), }); err != nil { ctx.SetStatusCode(http.StatusInternalServerError) - encoder.Encode(err) + + if err = encoder.Encode(err); err != nil { + ctx.Error(err.Error(), http.StatusInternalServerError) + } } } func (h *RequestHandler) Update(ctx *http.RequestCtx) { - switch string(ctx.FormValue(Action)) { - case ActionRevoke: + if strings.EqualFold(string(ctx.FormValue(Action)), ActionRevoke) { h.Revocation(ctx) } } @@ -95,38 +101,49 @@ func (h *RequestHandler) Revocation(ctx *http.RequestCtx) { req := new(RevocationRequest) if err := req.bind(ctx); err != nil { ctx.SetStatusCode(http.StatusBadRequest) - encoder.Encode(err) + + if err = encoder.Encode(err); err != nil { + ctx.Error(err.Error(), http.StatusInternalServerError) + } return } if err := h.useCase.Revoke(ctx, req.Token); err != nil { ctx.SetStatusCode(http.StatusBadRequest) - encoder.Encode(err) + + if err = encoder.Encode(err); err != nil { + ctx.Error(err.Error(), http.StatusInternalServerError) + } return } if err := encoder.Encode(&RevocationResponse{}); err != nil { ctx.SetStatusCode(http.StatusInternalServerError) - encoder.Encode(err) + + if err = encoder.Encode(err); err != nil { + ctx.Error(err.Error(), http.StatusInternalServerError) + } } } func (r *RevocationRequest) bind(ctx *http.RequestCtx) error { if r.Action = string(ctx.FormValue(Action)); !strings.EqualFold(r.Action, ActionRevoke) { return model.Error{ - Code: model.ErrInvalidRequest, + Code: "invalid_request", Description: "request MUST contain 'action' key with value 'revoke'", URI: "https://indieauth.spec.indieweb.org/#token-revocation-request", + Frame: xerrors.Caller(1), } } if r.Token = string(ctx.FormValue("token")); r.Token == "" { return model.Error{ - Code: model.ErrInvalidRequest, + Code: "invalid_request", Description: "request MUST contain the 'token' key with the valid access token as its value", URI: "https://indieauth.spec.indieweb.org/#token-revocation-request", + Frame: xerrors.Caller(1), } } diff --git a/internal/token/delivery/http/token_http_test.go b/internal/token/delivery/http/token_http_test.go index d8d152a..e121655 100644 --- a/internal/token/delivery/http/token_http_test.go +++ b/internal/token/delivery/http/token_http_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" http "github.com/valyala/fasthttp" - "source.toby3d.me/website/oauth/internal/common" "source.toby3d.me/website/oauth/internal/model" delivery "source.toby3d.me/website/oauth/internal/token/delivery/http" @@ -33,6 +32,7 @@ func TestVerification(t *testing.T) { ClientID: "https://app.example.com/", Me: "https://user.example.net/", Scopes: []string{"create", "update", "delete"}, + Profile: nil, } require.NoError(repo.Create(context.TODO(), &accessToken)) @@ -75,6 +75,7 @@ func TestRevocation(t *testing.T) { ClientID: "https://app.example.com/", Me: "https://user.example.net/", Scopes: []string{"create", "update", "delete"}, + Profile: nil, })) req := http.AcquireRequest() diff --git a/internal/token/repository.go b/internal/token/repository.go index c689d6d..b203362 100644 --- a/internal/token/repository.go +++ b/internal/token/repository.go @@ -4,7 +4,6 @@ import ( "context" "golang.org/x/xerrors" - "source.toby3d.me/website/oauth/internal/model" ) @@ -18,5 +17,6 @@ type Repository interface { var ErrExist error = model.Error{ Code: "invalid_request", Description: "this token is already exists", + URI: "", Frame: xerrors.Caller(1), } diff --git a/internal/token/repository/bolt/bolt_token.go b/internal/token/repository/bolt/bolt_token.go index 3897e63..32530e8 100644 --- a/internal/token/repository/bolt/bolt_token.go +++ b/internal/token/repository/bolt/bolt_token.go @@ -6,18 +6,16 @@ import ( json "github.com/goccy/go-json" "github.com/pkg/errors" - "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt" "golang.org/x/xerrors" - "source.toby3d.me/website/oauth/internal/model" "source.toby3d.me/website/oauth/internal/token" ) type ( Token struct { - AccessToken string `json:"access_token"` - ClientID string `json:"client_id"` + AccessToken string `json:"accessToken"` + ClientID string `json:"clientId"` Me string `json:"me"` Scope string `json:"scope"` Type string `json:"type"` @@ -31,12 +29,13 @@ type ( var ErrNotExist error = xerrors.New("key not exist") func NewBoltTokenRepository(db *bolt.DB) (token.Repository, error) { - if err := db.Update(func(tx *bbolt.Tx) error { + if err := db.Update(func(tx *bolt.Tx) error { + //nolint: exhaustivestruct _, err := tx.CreateBucketIfNotExists(Token{}.Bucket()) - return err + return errors.Wrap(err, "failed to create a bucket") }); err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to update the storage structure") } return &boltTokenRepository{ @@ -46,18 +45,19 @@ func NewBoltTokenRepository(db *bolt.DB) (token.Repository, error) { func (repo *boltTokenRepository) Get(ctx context.Context, accessToken string) (*model.Token, error) { result := model.NewToken() - err := repo.db.View(func(tx *bolt.Tx) error { + + if err := repo.db.View(func(tx *bolt.Tx) error { + //nolint: exhaustivestruct if src := tx.Bucket(Token{}.Bucket()).Get([]byte(accessToken)); src != nil { return NewToken().Bind(src, result) } return ErrNotExist - }) - if err != nil && !xerrors.Is(err, ErrNotExist) { - return nil, err - } + }); err != nil { + if !xerrors.Is(err, ErrNotExist) { + return nil, errors.Wrap(err, "failed to retrieve token from storage") + } - if xerrors.Is(err, ErrNotExist) { return nil, nil } @@ -86,15 +86,32 @@ func (repo *boltTokenRepository) Update(ctx context.Context, accessToken *model. return errors.Wrap(err, "failed to marshal token") } - return repo.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(dto.Bucket()).Put([]byte(dto.AccessToken), src) - }) + if err = repo.db.Update(func(tx *bolt.Tx) error { + if err := tx.Bucket(dto.Bucket()).Put([]byte(dto.AccessToken), src); err != nil { + return errors.Wrap(err, "failed to overwrite the token in the bucket") + } + + return nil + }); err != nil { + return errors.Wrap(err, "failed to update the token in the repository") + } + + return nil } func (repo *boltTokenRepository) Remove(ctx context.Context, accessToken string) error { - return repo.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(Token{}.Bucket()).Delete([]byte(accessToken)) - }) + if err := repo.db.Update(func(tx *bolt.Tx) error { + //nolint: exhaustivestruct + if err := tx.Bucket(Token{}.Bucket()).Delete([]byte(accessToken)); err != nil { + return errors.Wrap(err, "failed to remove token in bucket") + } + + return nil + }); err != nil { + return errors.Wrap(err, "failed to remove token from storage") + } + + return nil } func NewToken() *Token { @@ -113,7 +130,7 @@ func (t *Token) Populate(src *model.Token) { func (t *Token) Bind(src []byte, dst *model.Token) error { if err := json.Unmarshal(src, t); err != nil { - return err + return errors.Wrap(err, "cannot unmarshal token source") } dst.AccessToken = t.AccessToken diff --git a/internal/token/repository/bolt/bolt_token_test.go b/internal/token/repository/bolt/bolt_token_test.go index c4e31a8..7ae62ae 100644 --- a/internal/token/repository/bolt/bolt_token_test.go +++ b/internal/token/repository/bolt/bolt_token_test.go @@ -1,3 +1,4 @@ +//nolint: wrapcheck package bolt_test import ( @@ -11,13 +12,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" - "source.toby3d.me/website/oauth/internal/model" "source.toby3d.me/website/oauth/internal/random" "source.toby3d.me/website/oauth/internal/token" "source.toby3d.me/website/oauth/internal/token/repository/bolt" ) +//nolint: gochecknoglobals var ( db *bbolt.DB repo token.Repository @@ -51,6 +52,7 @@ func TestGet(t *testing.T) { t.Cleanup(func() { _ = db.Update(func(tx *bbolt.Tx) error { + //nolint: exhaustivestruct return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken)) }) }) @@ -65,6 +67,7 @@ func TestGet(t *testing.T) { require.NoError(t, err) require.NoError(t, db.Update(func(tx *bbolt.Tx) error { + //nolint: exhaustivestruct return tx.Bucket(bolt.Token{}.Bucket()).Put([]byte(accessToken), src) })) @@ -76,6 +79,7 @@ func TestGet(t *testing.T) { Me: "https://toby3d.me/", Scopes: []string{"read", "update", "delete"}, Type: "Bearer", + Profile: nil, }, tkn) } @@ -86,6 +90,7 @@ func TestCreate(t *testing.T) { t.Cleanup(func() { _ = db.Update(func(tx *bbolt.Tx) error { + //nolint: exhaustivestruct return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken)) }) }) @@ -96,12 +101,15 @@ func TestCreate(t *testing.T) { Me: "https://toby3d.me/", Scopes: []string{"read", "update", "delete"}, Type: "Bearer", + Profile: nil, } require.NoError(t, repo.Create(context.TODO(), tkn)) result := model.NewToken() + require.NoError(t, db.View(func(tx *bbolt.Tx) error { + //nolint: exhaustivestruct return bolt.NewToken().Bind(tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(tkn.AccessToken)), result) })) assert.Equal(t, tkn, result) @@ -116,6 +124,7 @@ func TestUpdate(t *testing.T) { t.Cleanup(func() { _ = db.Update(func(tx *bbolt.Tx) error { + //nolint: exhaustivestruct return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken)) }) }) @@ -130,6 +139,7 @@ func TestUpdate(t *testing.T) { require.NoError(t, err) require.NoError(t, db.Update(func(tx *bbolt.Tx) error { + //nolint: exhaustivestruct return tx.Bucket(bolt.Token{}.Bucket()).Put([]byte(accessToken), src) })) @@ -139,10 +149,13 @@ func TestUpdate(t *testing.T) { Me: "https://toby3d.ru/", Scopes: []string{"read"}, Type: "Bearer", + Profile: nil, })) result := model.NewToken() + require.NoError(t, db.View(func(tx *bbolt.Tx) error { + //nolint: exhaustivestruct return bolt.NewToken().Bind(tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(accessToken)), result) })) assert.Equal(t, &model.Token{ @@ -151,6 +164,7 @@ func TestUpdate(t *testing.T) { Me: "https://toby3d.ru/", Scopes: []string{"read"}, Type: "Bearer", + Profile: nil, }, result) } @@ -161,6 +175,7 @@ func TestDelete(t *testing.T) { t.Cleanup(func() { _ = db.Update(func(tx *bbolt.Tx) error { + //nolint: exhaustivestruct return tx.Bucket(bolt.Token{}.Bucket()).Delete([]byte(accessToken)) }) }) @@ -175,12 +190,14 @@ func TestDelete(t *testing.T) { require.NoError(t, err) require.NoError(t, db.Update(func(tx *bbolt.Tx) error { + //nolint: exhaustivestruct return tx.Bucket(bolt.Token{}.Bucket()).Put([]byte(accessToken), src) })) require.NoError(t, repo.Remove(context.TODO(), accessToken)) require.NoError(t, db.View(func(tx *bbolt.Tx) error { + //nolint: exhaustivestruct assert.Nil(t, tx.Bucket(bolt.Token{}.Bucket()).Get([]byte(accessToken))) return nil diff --git a/internal/token/repository/memory/memory_token_test.go b/internal/token/repository/memory/memory_token_test.go index 455db3c..4db6692 100644 --- a/internal/token/repository/memory/memory_token_test.go +++ b/internal/token/repository/memory/memory_token_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "source.toby3d.me/website/oauth/internal/model" "source.toby3d.me/website/oauth/internal/random" "source.toby3d.me/website/oauth/internal/token" diff --git a/internal/token/usecase/token_usecase.go b/internal/token/usecase/token_usecase.go index 2bbf196..81fb76d 100644 --- a/internal/token/usecase/token_usecase.go +++ b/internal/token/usecase/token_usecase.go @@ -3,6 +3,7 @@ package usecase import ( "context" + "github.com/pkg/errors" "source.toby3d.me/website/oauth/internal/model" "source.toby3d.me/website/oauth/internal/token" ) @@ -18,9 +19,18 @@ func NewTokenUseCase(tokens token.Repository) token.UseCase { } func (useCase *tokenUseCase) Verify(ctx context.Context, accessToken string) (*model.Token, error) { - return useCase.tokens.Get(ctx, accessToken) + t, err := useCase.tokens.Get(ctx, accessToken) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve token from storage") + } + + return t, nil } func (useCase *tokenUseCase) Revoke(ctx context.Context, accessToken string) error { - return useCase.tokens.Remove(ctx, accessToken) + if err := useCase.tokens.Remove(ctx, accessToken); err != nil { + return errors.Wrap(err, "failed to delete a token in the vault") + } + + return nil } diff --git a/internal/token/usecase/token_usecase_test.go b/internal/token/usecase/token_usecase_test.go index 8232b01..961fa31 100644 --- a/internal/token/usecase/token_usecase_test.go +++ b/internal/token/usecase/token_usecase_test.go @@ -8,7 +8,6 @@ import ( "github.com/brianvoe/gofakeit" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "source.toby3d.me/website/oauth/internal/model" repository "source.toby3d.me/website/oauth/internal/token/repository/memory" "source.toby3d.me/website/oauth/internal/token/usecase" @@ -28,6 +27,7 @@ func TestVerify(t *testing.T) { ClientID: "https://app.example.com/", Me: "https://user.example.net/", Scopes: []string{"create", "update", "delete"}, + Profile: nil, } require.NoError(repo.Create(context.TODO(), accessToken)) @@ -53,6 +53,7 @@ func TestRevoke(t *testing.T) { ClientID: "https://app.example.com/", Me: "https://user.example.net/", Scopes: []string{"create", "update", "delete"}, + Profile: nil, })) token, err := repo.Get(context.TODO(), accessToken)