diff --git a/internal/auth/delivery/http/auth_http.go b/internal/auth/delivery/http/auth_http.go index 975c67a..bac3d4b 100644 --- a/internal/auth/delivery/http/auth_http.go +++ b/internal/auth/delivery/http/auth_http.go @@ -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) diff --git a/internal/auth/delivery/http/auth_http_test.go b/internal/auth/delivery/http/auth_http_test.go index 05c25fb..de7d023 100644 --- a/internal/auth/delivery/http/auth_http_test.go +++ b/internal/auth/delivery/http/auth_http_test.go @@ -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) diff --git a/internal/auth/usecase.go b/internal/auth/usecase.go index 7927f4b..c2b0a11 100644 --- a/internal/auth/usecase.go +++ b/internal/auth/usecase.go @@ -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 } diff --git a/internal/auth/usecase/auth_ucase.go b/internal/auth/usecase/auth_ucase.go index aa4246f..9d65310 100644 --- a/internal/auth/usecase/auth_ucase.go +++ b/internal/auth/usecase/auth_ucase.go @@ -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