♻️ Simplify error usage in auth package

This commit is contained in:
Maxim Lebedev 2022-01-30 00:31:52 +05:00
parent b60aab7be5
commit 6b05c5170f
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
4 changed files with 111 additions and 78 deletions

View File

@ -2,7 +2,7 @@ package http
import ( import (
"crypto/subtle" "crypto/subtle"
"fmt" "errors"
"path" "path"
"strings" "strings"
@ -11,7 +11,6 @@ import (
http "github.com/valyala/fasthttp" http "github.com/valyala/fasthttp"
"golang.org/x/text/language" "golang.org/x/text/language"
"golang.org/x/text/message" "golang.org/x/text/message"
"golang.org/x/xerrors"
"source.toby3d.me/toby3d/form" "source.toby3d.me/toby3d/form"
"source.toby3d.me/toby3d/middleware" "source.toby3d.me/toby3d/middleware"
@ -68,6 +67,7 @@ type (
Authorize string `form:"authorize"` Authorize string `form:"authorize"`
CodeChallenge string `form:"code_challenge"` CodeChallenge string `form:"code_challenge"`
State string `form:"state"` State string `form:"state"`
Provider string `form:"provider"`
} }
ExchangeRequest struct { ExchangeRequest struct {
@ -95,26 +95,29 @@ type (
} }
NewRequestHandlerOptions struct { NewRequestHandlerOptions struct {
Auth auth.UseCase Auth auth.UseCase
Clients client.UseCase Clients client.UseCase
Config *domain.Config Config *domain.Config
Matcher language.Matcher Matcher language.Matcher
Providers []*domain.Provider
} }
RequestHandler struct { RequestHandler struct {
clients client.UseCase clients client.UseCase
config *domain.Config config *domain.Config
matcher language.Matcher matcher language.Matcher
useCase auth.UseCase useCase auth.UseCase
providers []*domain.Provider
} }
) )
func NewRequestHandler(opts NewRequestHandlerOptions) *RequestHandler { func NewRequestHandler(opts NewRequestHandlerOptions) *RequestHandler {
return &RequestHandler{ return &RequestHandler{
clients: opts.Clients, clients: opts.Clients,
config: opts.Config, config: opts.Config,
matcher: opts.Matcher, matcher: opts.Matcher,
useCase: opts.Auth, useCase: opts.Auth,
providers: opts.Providers,
} }
} }
@ -135,8 +138,10 @@ func (h *RequestHandler) Register(r *router.Router) {
middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{ middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{
Skipper: func(ctx *http.RequestCtx) bool { Skipper: func(ctx *http.RequestCtx) bool {
matched, _ := path.Match("/api/*", string(ctx.Path())) matched, _ := path.Match("/api/*", string(ctx.Path()))
provider := string(ctx.QueryArgs().Peek("provider"))
return !matched return !ctx.IsPost() || !matched ||
(provider != "" && provider != domain.DefaultProviderDirect.UID)
}, },
Validator: func(ctx *http.RequestCtx, login, password string) (bool, error) { Validator: func(ctx *http.RequestCtx, login, password string) (bool, error) {
// TODO(toby3d): change this // TODO(toby3d): change this
@ -204,11 +209,7 @@ func (h *RequestHandler) handleVerify(ctx *http.RequestCtx) {
req := new(VerifyRequest) req := new(VerifyRequest)
if err := req.bind(ctx); err != nil { if err := req.bind(ctx); err != nil {
ctx.SetStatusCode(http.StatusBadRequest) ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(domain.Error{ encoder.Encode(err)
Code: "invalid_request",
Description: err.Error(),
Frame: xerrors.Caller(1),
})
return return
} }
@ -218,8 +219,8 @@ func (h *RequestHandler) handleVerify(ctx *http.RequestCtx) {
req.RedirectURI.CopyTo(u) req.RedirectURI.CopyTo(u)
if strings.EqualFold(req.Authorize, "deny") { if strings.EqualFold(req.Authorize, "deny") {
u.QueryArgs().Set("error", "access_denied") domain.NewError(domain.ErrorCodeAccessDenied, "user deny authorization request", "", req.State).
u.QueryArgs().Set("error_description", "user deny authorization request") SetReirectURI(u)
ctx.Redirect(u.String(), http.StatusFound) ctx.Redirect(u.String(), http.StatusFound)
return return
@ -235,10 +236,7 @@ func (h *RequestHandler) handleVerify(ctx *http.RequestCtx) {
}) })
if err != nil { if err != nil {
ctx.SetStatusCode(http.StatusInternalServerError) ctx.SetStatusCode(http.StatusInternalServerError)
encoder.Encode(domain.Error{ encoder.Encode(err)
Description: err.Error(),
Frame: xerrors.Caller(1),
})
return return
} }
@ -286,16 +284,32 @@ func (h *RequestHandler) handleExchange(ctx *http.RequestCtx) {
} }
func (r *AuthorizeRequest) bind(ctx *http.RequestCtx) error { func (r *AuthorizeRequest) bind(ctx *http.RequestCtx) error {
indieAuthError := new(domain.Error)
if err := form.Unmarshal(ctx.QueryArgs(), r); err != nil { if err := form.Unmarshal(ctx.QueryArgs(), r); err != nil {
return domain.Error{ if errors.As(err, indieAuthError) {
Code: "invalid_request", return indieAuthError
Description: err.Error(),
Frame: xerrors.Caller(1),
} }
return domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#authorization-request",
)
} }
r.Scope = make(domain.Scopes, 0) r.Scope = make(domain.Scopes, 0)
parseScope(r.Scope, ctx.QueryArgs().Peek("scope"))
if err := parseScope(r.Scope, ctx.QueryArgs().Peek("scope")); err != nil {
if errors.As(err, indieAuthError) {
return indieAuthError
}
return domain.NewError(
domain.ErrorCodeInvalidScope,
err.Error(),
"https://indieweb.org/scope",
)
}
if r.ResponseType == domain.ResponseTypeID { if r.ResponseType == domain.ResponseTypeID {
r.ResponseType = domain.ResponseTypeCode r.ResponseType = domain.ResponseTypeCode
@ -305,12 +319,18 @@ func (r *AuthorizeRequest) bind(ctx *http.RequestCtx) error {
} }
func (r *VerifyRequest) bind(ctx *http.RequestCtx) error { func (r *VerifyRequest) bind(ctx *http.RequestCtx) error {
indieAuthError := new(domain.Error)
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil { if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
return domain.Error{ if errors.As(err, indieAuthError) {
Code: "invalid_request", return indieAuthError
Description: err.Error(),
Frame: xerrors.Caller(1),
} }
return domain.NewError(
domain.ErrorCodeInvalidRequest,
err.Error(),
"https://indieauth.net/source/#authorization-request",
)
} }
r.Scope = make(domain.Scopes, 0) r.Scope = make(domain.Scopes, 0)
@ -320,24 +340,31 @@ func (r *VerifyRequest) bind(ctx *http.RequestCtx) error {
r.ResponseType = domain.ResponseTypeCode r.ResponseType = domain.ResponseTypeCode
} }
r.Provider = strings.ToLower(r.Provider)
if !strings.EqualFold(r.Authorize, "allow") && !strings.EqualFold(r.Authorize, "deny") { if !strings.EqualFold(r.Authorize, "allow") && !strings.EqualFold(r.Authorize, "deny") {
return domain.Error{ return domain.NewError(
Code: "invalid_request", domain.ErrorCodeInvalidRequest,
Description: "cannot validate verification request", "cannot validate verification request",
Frame: xerrors.Caller(1), "https://indieauth.net/source/#authorization-request",
} )
} }
return nil return nil
} }
func (r *ExchangeRequest) bind(ctx *http.RequestCtx) error { func (r *ExchangeRequest) bind(ctx *http.RequestCtx) error {
indieAuthError := new(domain.Error)
if err := form.Unmarshal(ctx.PostArgs(), r); err != nil { if err := form.Unmarshal(ctx.PostArgs(), r); err != nil {
return domain.Error{ if errors.As(err, indieAuthError) {
Code: "invalid_request", return indieAuthError
Description: err.Error(),
Frame: xerrors.Caller(1),
} }
return domain.NewError(
domain.ErrorCodeInvalidRequest,
"cannot validate verification request",
"https://indieauth.net/source/#redeeming-the-authorization-code",
)
} }
return nil return nil
@ -358,11 +385,11 @@ func parseScope(dst domain.Scopes, src ...[]byte) error {
for _, rawScope := range scopes { for _, rawScope := range scopes {
scope, err := domain.ParseScope(string(rawScope)) scope, err := domain.ParseScope(string(rawScope))
if err != nil { if err != nil {
return &domain.Error{ return domain.NewError(
Code: "invalid_request", domain.ErrorCodeInvalidScope,
Description: fmt.Sprintf("cannot parse scope: %v", err), err.Error(),
Frame: xerrors.Caller(1), "https://indieweb.org/scope#IndieAuth_Scopes",
} )
} }
dst = append(dst, scope) dst = append(dst, scope)

View File

@ -19,6 +19,7 @@ import (
clientrepo "source.toby3d.me/website/indieauth/internal/client/repository/memory" clientrepo "source.toby3d.me/website/indieauth/internal/client/repository/memory"
clientucase "source.toby3d.me/website/indieauth/internal/client/usecase" clientucase "source.toby3d.me/website/indieauth/internal/client/usecase"
"source.toby3d.me/website/indieauth/internal/domain" "source.toby3d.me/website/indieauth/internal/domain"
profilerepo "source.toby3d.me/website/indieauth/internal/profile/repository/memory"
sessionrepo "source.toby3d.me/website/indieauth/internal/session/repository/memory" sessionrepo "source.toby3d.me/website/indieauth/internal/session/repository/memory"
"source.toby3d.me/website/indieauth/internal/testing/httptest" "source.toby3d.me/website/indieauth/internal/testing/httptest"
userrepo "source.toby3d.me/website/indieauth/internal/user/repository/memory" userrepo "source.toby3d.me/website/indieauth/internal/user/repository/memory"
@ -33,19 +34,25 @@ func TestRender(t *testing.T) {
s := session.New(session.NewDefaultConfig()) s := session.New(session.NewDefaultConfig())
require.NoError(t, s.SetProvider(provider)) require.NoError(t, s.SetProvider(provider))
me := domain.TestMe(t) me := domain.TestMe(t, "https://user.example.net")
c := domain.TestClient(t) c := domain.TestClient(t)
config := domain.TestConfig(t) config := domain.TestConfig(t)
store := new(sync.Map) store := new(sync.Map)
store.Store(path.Join(userrepo.DefaultPathPrefix, me.String()), domain.TestUser(t)) user := domain.TestUser(t)
store.Store(path.Join(userrepo.DefaultPathPrefix, me.String()), user)
store.Store(path.Join(clientrepo.DefaultPathPrefix, c.ID.String()), c) store.Store(path.Join(clientrepo.DefaultPathPrefix, c.ID.String()), c)
store.Store(path.Join(profilerepo.DefaultPathPrefix, me.String()), user.Profile)
r := router.New() r := router.New()
delivery.NewRequestHandler(delivery.NewRequestHandlerOptions{ delivery.NewRequestHandler(delivery.NewRequestHandlerOptions{
Clients: clientucase.NewClientUseCase(clientrepo.NewMemoryClientRepository(store)), Clients: clientucase.NewClientUseCase(clientrepo.NewMemoryClientRepository(store)),
Config: config, Config: config,
Matcher: language.NewMatcher(message.DefaultCatalog.Languages()), Matcher: language.NewMatcher(message.DefaultCatalog.Languages()),
Auth: ucase.NewAuthUseCase(sessionrepo.NewMemorySessionRepository(config, store), config), Auth: ucase.NewAuthUseCase(
sessionrepo.NewMemorySessionRepository(config, store),
profilerepo.NewMemoryProfileRepository(store),
config,
),
}).Register(r) }).Register(r)
client, _, cleanup := httptest.New(t, r.Handler) client, _, cleanup := httptest.New(t, r.Handler)

View File

@ -9,17 +9,17 @@ import (
type ( type (
GenerateOptions struct { GenerateOptions struct {
ClientID *domain.ClientID ClientID *domain.ClientID
Me *domain.Me
RedirectURI *domain.URL RedirectURI *domain.URL
CodeChallenge string
CodeChallengeMethod domain.CodeChallengeMethod CodeChallengeMethod domain.CodeChallengeMethod
Scope domain.Scopes Scope domain.Scopes
Me *domain.Me CodeChallenge string
} }
ExchangeOptions struct { ExchangeOptions struct {
Code string
ClientID *domain.ClientID ClientID *domain.ClientID
RedirectURI *domain.URL RedirectURI *domain.URL
Code string
CodeVerifier string CodeVerifier string
} }

View File

@ -4,22 +4,24 @@ import (
"context" "context"
"fmt" "fmt"
"golang.org/x/xerrors"
"source.toby3d.me/website/indieauth/internal/auth" "source.toby3d.me/website/indieauth/internal/auth"
"source.toby3d.me/website/indieauth/internal/domain" "source.toby3d.me/website/indieauth/internal/domain"
"source.toby3d.me/website/indieauth/internal/profile"
"source.toby3d.me/website/indieauth/internal/random" "source.toby3d.me/website/indieauth/internal/random"
"source.toby3d.me/website/indieauth/internal/session" "source.toby3d.me/website/indieauth/internal/session"
) )
type authUseCase struct { type authUseCase struct {
config *domain.Config config *domain.Config
profiles profile.Repository
sessions session.Repository sessions session.Repository
} }
func NewAuthUseCase(sessions session.Repository, config *domain.Config) auth.UseCase { // NewAuthUseCase creates a new authentication use case.
func NewAuthUseCase(sessions session.Repository, profiles profile.Repository, config *domain.Config) auth.UseCase {
return &authUseCase{ return &authUseCase{
config: config, config: config,
profiles: profiles,
sessions: sessions, sessions: sessions,
} }
} }
@ -48,36 +50,33 @@ func (useCase *authUseCase) Generate(ctx context.Context, opts auth.GenerateOpti
func (useCase *authUseCase) Exchange(ctx context.Context, opts auth.ExchangeOptions) (*domain.Me, error) { func (useCase *authUseCase) Exchange(ctx context.Context, opts auth.ExchangeOptions) (*domain.Me, error) {
session, err := useCase.sessions.GetAndDelete(ctx, opts.Code) session, err := useCase.sessions.GetAndDelete(ctx, opts.Code)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("cannot find session in store: %w", err)
} }
if opts.ClientID.String() != session.ClientID.String() { if opts.ClientID.String() != session.ClientID.String() {
return nil, domain.Error{ return nil, domain.NewError(
Code: "invalid_request", domain.ErrorCodeInvalidRequest,
Description: "client's URL MUST match the client_id used in the authentication request", "client's URL MUST match the client_id used in the authentication request",
URI: "https://indieauth.net/source/#request", "https://indieauth.net/source/#request",
Frame: xerrors.Caller(1), )
}
} }
if opts.RedirectURI.String() != session.RedirectURI.String() { if opts.RedirectURI.String() != session.RedirectURI.String() {
return nil, domain.Error{ return nil, domain.NewError(
Code: "invalid_request", domain.ErrorCodeInvalidRequest,
Description: "client's redirect URL MUST match the initial authentication request", "client's redirect URL MUST match the initial authentication request",
URI: "https://indieauth.net/source/#request", "https://indieauth.net/source/#request",
Frame: xerrors.Caller(1), )
}
} }
if session.CodeChallenge != "" && if session.CodeChallenge != "" &&
!session.CodeChallengeMethod.Validate(session.CodeChallenge, opts.CodeVerifier) { !session.CodeChallengeMethod.Validate(session.CodeChallenge, opts.CodeVerifier) {
return nil, domain.Error{ return nil, domain.NewError(
Code: "invalid_request", domain.ErrorCodeInvalidRequest,
Description: "code_verifier is not hashes to the same value as given in " + "code_verifier is not hashes to the same value as given in the code_challenge in the original "+
"the code_challenge in the original authorization request", "authorization request",
URI: "https://indieauth.net/source/#request", "https://indieauth.net/source/#request",
Frame: xerrors.Caller(1), )
}
} }
return session.Me, nil return session.Me, nil