♻️ Simplify error usage in token package
This commit is contained in:
parent
ed55c8cded
commit
2e30613089
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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", "")
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
"",
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue