♻️ Simplify error usage in token package

This commit is contained in:
Maxim Lebedev 2022-01-30 01:43:53 +05:00
parent ed55c8cded
commit 2e30613089
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
6 changed files with 202 additions and 91 deletions

View File

@ -1,17 +1,18 @@
package http
import (
"errors"
"strings"
"github.com/fasthttp/router"
json "github.com/goccy/go-json"
http "github.com/valyala/fasthttp"
"golang.org/x/xerrors"
"source.toby3d.me/toby3d/form"
"source.toby3d.me/toby3d/middleware"
"source.toby3d.me/website/indieauth/internal/common"
"source.toby3d.me/website/indieauth/internal/domain"
"source.toby3d.me/website/indieauth/internal/ticket"
"source.toby3d.me/website/indieauth/internal/token"
)
@ -36,10 +37,18 @@ type (
//nolint: tagliatelle
ExchangeResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
Me string `json:"me"`
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
Me string `json:"me"`
Profile *ProfileResponse `json:"profile,omitempty"`
}
ProfileResponse struct {
Name string `json:"name,omitempty"`
URL *domain.URL `json:"url,omitempty"`
Photo *domain.URL `json:"photo,omitempty"`
Email *domain.Email `json:"email,omitempty"`
}
//nolint: tagliatelle
@ -52,15 +61,15 @@ type (
RevocationResponse struct{}
RequestHandler struct {
tokens token.UseCase
// TODO(toby3d): tickets ticket.UseCase
tokens token.UseCase
tickets ticket.UseCase
}
)
func NewRequestHandler(tokens token.UseCase /*, tickets ticket.UseCase*/) *RequestHandler {
func NewRequestHandler(tokens token.UseCase, tickets ticket.UseCase) *RequestHandler {
return &RequestHandler{
tokens: tokens,
// tickets: tickets,
tokens: tokens,
tickets: tickets,
}
}
@ -83,16 +92,16 @@ func (h *RequestHandler) handleValidate(ctx *http.RequestCtx) {
"Bearer "))
if err != nil || t == nil {
ctx.SetStatusCode(http.StatusUnauthorized)
encoder.Encode(&domain.Error{
Code: "unauthorized_client",
Description: err.Error(),
Frame: xerrors.Caller(1),
})
encoder.Encode(domain.NewError(
domain.ErrorCodeUnauthorizedClient,
err.Error(),
"https://indieauth.net/source/#access-token-verification",
))
return
}
encoder.Encode(&VerificationResponse{
_ = encoder.Encode(&VerificationResponse{
ClientID: t.ClientID,
Me: t.Me,
Scope: t.Scope,
@ -111,11 +120,11 @@ func (h *RequestHandler) handleAction(ctx *http.RequestCtx) {
action, err := domain.ParseAction(string(ctx.PostArgs().Peek("action")))
if err != nil {
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(domain.Error{
Code: "invalid_request",
Description: err.Error(),
Frame: xerrors.Caller(1),
})
encoder.Encode(domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"",
))
return
}
@ -142,7 +151,7 @@ func (h *RequestHandler) handleExchange(ctx *http.RequestCtx) {
return
}
token, err := h.tokens.Exchange(ctx, token.ExchangeOptions{
token, profile, err := h.tokens.Exchange(ctx, token.ExchangeOptions{
ClientID: req.ClientID,
RedirectURI: req.RedirectURI,
Code: req.Code,
@ -150,20 +159,47 @@ func (h *RequestHandler) handleExchange(ctx *http.RequestCtx) {
})
if err != nil {
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(&domain.Error{
Description: err.Error(),
Frame: xerrors.Caller(1),
})
encoder.Encode(domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#request",
))
return
}
encoder.Encode(&ExchangeResponse{
resp := &ExchangeResponse{
AccessToken: token.AccessToken,
TokenType: "Bearer",
Scope: token.Scope.String(),
Me: token.Me.String(),
})
}
if profile == nil {
encoder.Encode(resp)
return
}
resp.Profile = new(ProfileResponse)
if len(profile.Name) > 0 {
resp.Profile.Name = profile.Name[0]
}
if len(profile.URL) > 0 {
resp.Profile.URL = profile.URL[0]
}
if len(profile.Photo) > 0 {
resp.Profile.Photo = profile.Photo[0]
}
if len(profile.Email) > 0 {
resp.Profile.Email = profile.Email[0]
}
_ = encoder.Encode(resp)
}
func (h *RequestHandler) handleRevoke(ctx *http.RequestCtx) {
@ -174,20 +210,24 @@ func (h *RequestHandler) handleRevoke(ctx *http.RequestCtx) {
req := new(RevokeRequest)
if err := req.bind(ctx); err != nil {
ctx.Error(err.Error(), http.StatusBadRequest)
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(err)
return
}
if err := h.tokens.Revoke(ctx, req.Token); err != nil {
ctx.Error(err.Error(), http.StatusBadRequest)
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"",
))
return
}
if err := encoder.Encode(&RevocationResponse{}); err != nil {
ctx.Error(err.Error(), http.StatusInternalServerError)
}
_ = encoder.Encode(&RevocationResponse{})
}
func (h *RequestHandler) handleTicket(ctx *http.RequestCtx) {
@ -204,53 +244,72 @@ func (h *RequestHandler) handleTicket(ctx *http.RequestCtx) {
return
}
/* TODO(toby3d)
token, err := h.tickets.Redeem(ctx, req.Ticket)
t, err := h.tickets.Exchange(ctx, req.Ticket)
if err != nil {
ctx.SetStatusCode(http.StatusInternalServerError)
encoder.Encode(domain.Error{
Description: err.Error(),
Frame: xerrors.Caller(1),
})
encoder.Encode(domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#request",
))
return
}
*/
encoder.Encode(ExchangeResponse{})
encoder.Encode(ExchangeResponse{
AccessToken: t.AccessToken,
TokenType: "Bearer",
Scope: t.Scope.String(),
Me: t.Me.String(),
})
}
func (r *ExchangeRequest) bind(ctx *http.RequestCtx) error {
indieAuthError := new(domain.Error)
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
return domain.Error{
Code: "invalid_request",
Description: err.Error(),
Frame: xerrors.Caller(1),
if errors.As(err, indieAuthError) {
return indieAuthError
}
return domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#request",
)
}
return nil
}
func (r *RevokeRequest) bind(ctx *http.RequestCtx) error {
indieAuthError := new(domain.Error)
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
return domain.Error{
Code: "invalid_request",
Description: err.Error(),
Frame: xerrors.Caller(1),
if errors.As(err, indieAuthError) {
return indieAuthError
}
return domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#request",
)
}
return nil
}
func (r *TicketRequest) bind(ctx *http.RequestCtx) error {
indieAuthError := new(domain.Error)
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
return domain.Error{
Code: "invalid_request",
Description: err.Error(),
Frame: xerrors.Caller(1),
if errors.As(err, indieAuthError) {
return indieAuthError
}
return domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#request",
)
}
return nil

View File

@ -16,11 +16,19 @@ import (
"source.toby3d.me/website/indieauth/internal/domain"
sessionrepo "source.toby3d.me/website/indieauth/internal/session/repository/memory"
"source.toby3d.me/website/indieauth/internal/testing/httptest"
ticketrepo "source.toby3d.me/website/indieauth/internal/ticket/repository/memory"
ticketucase "source.toby3d.me/website/indieauth/internal/ticket/usecase"
delivery "source.toby3d.me/website/indieauth/internal/token/delivery/http"
tokenrepo "source.toby3d.me/website/indieauth/internal/token/repository/memory"
tokenucase "source.toby3d.me/website/indieauth/internal/token/usecase"
)
/* TODO(toby3d)
func TestExchange(t *testing.T) {
t.Parallel()
}
*/
func TestVerification(t *testing.T) {
t.Parallel()
@ -30,8 +38,18 @@ func TestVerification(t *testing.T) {
r := router.New()
// TODO(toby3d): provide tickets
delivery.NewRequestHandler(tokenucase.NewTokenUseCase(tokenrepo.NewMemoryTokenRepository(store),
sessionrepo.NewMemorySessionRepository(config, store), config)).Register(r)
delivery.NewRequestHandler(
tokenucase.NewTokenUseCase(
tokenrepo.NewMemoryTokenRepository(store),
sessionrepo.NewMemorySessionRepository(config, store),
config,
),
ticketucase.NewTicketUseCase(
ticketrepo.NewMemoryTicketRepository(store, config),
new(http.Client),
config,
),
).Register(r)
client, _, cleanup := httptest.New(t, r.Handler)
t.Cleanup(cleanup)
@ -63,8 +81,18 @@ func TestRevocation(t *testing.T) {
accessToken := domain.TestToken(t)
r := router.New()
delivery.NewRequestHandler(tokenucase.NewTokenUseCase(tokens, sessionrepo.NewMemorySessionRepository(config,
store), config)).Register(r)
delivery.NewRequestHandler(
tokenucase.NewTokenUseCase(
tokens,
sessionrepo.NewMemorySessionRepository(config, store),
config,
),
ticketucase.NewTicketUseCase(
ticketrepo.NewMemoryTicketRepository(store, config),
new(http.Client),
config,
),
).Register(r)
client, _, cleanup := httptest.New(t, r.Handler)
t.Cleanup(cleanup)

View File

@ -2,7 +2,6 @@ package token
import (
"context"
"errors"
"source.toby3d.me/website/indieauth/internal/domain"
)
@ -13,6 +12,6 @@ type Repository interface {
}
var (
ErrExist = errors.New("token already exist")
ErrNotExist = errors.New("token not exist")
ErrExist error = domain.NewError(domain.ErrorCodeServerError, "token already exist", "")
ErrNotExist error = domain.NewError(domain.ErrorCodeServerError, "token not exist", "")
)

View File

@ -2,7 +2,6 @@ package token
import (
"context"
"errors"
"source.toby3d.me/website/indieauth/internal/domain"
)
@ -16,7 +15,7 @@ type (
}
UseCase interface {
Exchange(ctx context.Context, opts ExchangeOptions) (*domain.Token, error)
Exchange(ctx context.Context, opts ExchangeOptions) (*domain.Token, *domain.Profile, error)
// Verify checks the AccessToken and returns the associated information.
Verify(ctx context.Context, accessToken string) (*domain.Token, error)
@ -26,4 +25,31 @@ type (
}
)
var ErrRevoke = errors.New("this token has been revoked")
var (
ErrRevoke error = domain.NewError(
domain.ErrorCodeAccessDenied,
"this token has been revoked",
"",
)
ErrMismatchClientID error = domain.NewError(
domain.ErrorCodeInvalidRequest,
"client's URL MUST match the client_id used in the authentication request",
"",
)
ErrMismatchRedirectURI error = domain.NewError(
domain.ErrorCodeInvalidRequest,
"client's redirect URL MUST match the initial authentication request",
"",
)
ErrEmptyScope error = domain.NewError(
domain.ErrorCodeInvalidScope,
"empty scopes are invalid",
"",
)
ErrMismatchPKCE error = domain.NewError(
domain.ErrorCodeInvalidRequest,
"code_verifier is not hashes to the same value as given in the code_challenge in the original "+
" authorization request",
"",
)
)

View File

@ -19,12 +19,9 @@ type tokenUseCase struct {
tokens token.Repository
}
//nolint: gochecknoinits
func init() {
jwt.RegisterCustomField("scope", make(domain.Scopes, 0))
}
func NewTokenUseCase(tokens token.Repository, sessions session.Repository, config *domain.Config) token.UseCase {
jwt.RegisterCustomField("scope", make(domain.Scopes, 0))
return &tokenUseCase{
sessions: sessions,
config: config,
@ -32,39 +29,31 @@ func NewTokenUseCase(tokens token.Repository, sessions session.Repository, confi
}
}
func (useCase *tokenUseCase) Exchange(ctx context.Context, opts token.ExchangeOptions) (*domain.Token, error) {
func (useCase *tokenUseCase) Exchange(ctx context.Context, opts token.ExchangeOptions) (*domain.Token, *domain.Profile,
error) {
session, err := useCase.sessions.GetAndDelete(ctx, opts.Code)
if err != nil {
return nil, fmt.Errorf("cannot get session from store: %w", err)
return nil, nil, fmt.Errorf("cannot get session from store: %w", err)
}
if opts.ClientID.String() != session.ClientID.String() {
return nil, domain.Error{
Code: "invalid_request",
Description: "client's URL MUST match the client_id used in the authentication request",
URI: "https://indieauth.net/source/#request",
Frame: xerrors.Caller(1),
}
return nil, nil, token.ErrMismatchClientID
}
if opts.RedirectURI.String() != session.RedirectURI.String() {
return nil, domain.Error{
Code: "invalid_request",
Description: "client's redirect URL MUST match the initial authentication request",
URI: "https://indieauth.net/source/#request",
Frame: xerrors.Caller(1),
}
return nil, nil, token.ErrMismatchRedirectURI
}
if session.CodeChallenge != "" &&
!session.CodeChallengeMethod.Validate(session.CodeChallenge, opts.CodeVerifier) {
return nil, domain.Error{
Code: "invalid_request",
Description: "code_verifier is not hashes to the same value as given in " +
"the code_challenge in the original authorization request",
URI: "https://indieauth.net/source/#request",
Frame: xerrors.Caller(1),
}
return nil, nil, token.ErrMismatchPKCE
}
// NOTE(toby3d): If the authorization code was issued with no scope, the
// token endpoint MUST NOT issue an access token, as empty scopes are
// invalid per Section 3.3 of OAuth 2.0 RFC6749.
if session.Scope.IsEmpty() {
return nil, nil, token.ErrEmptyScope
}
t, err := domain.NewToken(domain.NewTokenOptions{
@ -77,10 +66,18 @@ func (useCase *tokenUseCase) Exchange(ctx context.Context, opts token.ExchangeOp
Subject: session.Me,
})
if err != nil {
return nil, fmt.Errorf("cannot generate a new access token: %w", err)
return nil, nil, fmt.Errorf("cannot generate a new access token: %w", err)
}
return t, nil
if !session.Scope.Has(domain.ScopeProfile) {
return t, nil, nil
}
p := new(domain.Profile)
// TODO(toby3d): if session.Scope.Has(domain.ScopeEmail) {}
return t, p, nil
}
func (useCase *tokenUseCase) Verify(ctx context.Context, accessToken string) (*domain.Token, error) {

View File

@ -14,9 +14,11 @@ import (
usecase "source.toby3d.me/website/indieauth/internal/token/usecase"
)
/* TODO(toby3d)
func TestExchange(t *testing.T) {
t.Parallel()
}
*/
func TestVerify(t *testing.T) {
t.Parallel()