♻️ Simplify error usage in auth package
This commit is contained in:
parent
b60aab7be5
commit
6b05c5170f
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue