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

View File

@ -19,6 +19,7 @@ import (
clientrepo "source.toby3d.me/website/indieauth/internal/client/repository/memory"
clientucase "source.toby3d.me/website/indieauth/internal/client/usecase"
"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"
"source.toby3d.me/website/indieauth/internal/testing/httptest"
userrepo "source.toby3d.me/website/indieauth/internal/user/repository/memory"
@ -33,19 +34,25 @@ func TestRender(t *testing.T) {
s := session.New(session.NewDefaultConfig())
require.NoError(t, s.SetProvider(provider))
me := domain.TestMe(t)
me := domain.TestMe(t, "https://user.example.net")
c := domain.TestClient(t)
config := domain.TestConfig(t)
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(profilerepo.DefaultPathPrefix, me.String()), user.Profile)
r := router.New()
delivery.NewRequestHandler(delivery.NewRequestHandlerOptions{
Clients: clientucase.NewClientUseCase(clientrepo.NewMemoryClientRepository(store)),
Config: config,
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)
client, _, cleanup := httptest.New(t, r.Handler)

View File

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

View File

@ -4,22 +4,24 @@ import (
"context"
"fmt"
"golang.org/x/xerrors"
"source.toby3d.me/website/indieauth/internal/auth"
"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/session"
)
type authUseCase struct {
config *domain.Config
profiles profile.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{
config: config,
profiles: profiles,
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) {
session, err := useCase.sessions.GetAndDelete(ctx, opts.Code)
if err != nil {
return nil, err
return nil, fmt.Errorf("cannot find session in 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, domain.NewError(
domain.ErrorCodeInvalidRequest,
"client's URL MUST match the client_id used in the authentication request",
"https://indieauth.net/source/#request",
)
}
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, domain.NewError(
domain.ErrorCodeInvalidRequest,
"client's redirect URL MUST match the initial authentication request",
"https://indieauth.net/source/#request",
)
}
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, domain.NewError(
domain.ErrorCodeInvalidRequest,
"code_verifier is not hashes to the same value as given in the code_challenge in the original "+
"authorization request",
"https://indieauth.net/source/#request",
)
}
return session.Me, nil