diff --git a/internal/domain/client_id.go b/internal/domain/client_id.go index ddfaa67..993941f 100644 --- a/internal/domain/client_id.go +++ b/internal/domain/client_id.go @@ -30,59 +30,59 @@ func NewClientID(raw string) (*ClientID, error) { clientID := http.AcquireURI() if err := clientID.Parse(nil, []byte(raw)); err != nil { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: err.Error(), URI: "https://indieauth.net/source/#client-identifier", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } scheme := string(clientID.Scheme()) if scheme != "http" && scheme != "https" { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "client identifier URL MUST have either an https or http scheme", URI: "https://indieauth.net/source/#client-identifier", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } path := string(clientID.PathOriginal()) if path == "" || strings.Contains(path, "/.") || strings.Contains(path, "/..") { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "client identifier URL MUST contain a path component and MUST NOT contain " + "single-dot or double-dot path segments", URI: "https://indieauth.net/source/#client-identifier", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } if clientID.Hash() != nil { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "client identifier URL MUST NOT contain a fragment component", URI: "https://indieauth.net/source/#client-identifier", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } if clientID.Username() != nil || clientID.Password() != nil { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "client identifier URL MUST NOT contain a username or password component", URI: "https://indieauth.net/source/#client-identifier", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } domain := string(clientID.Host()) if domain == "" { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "client host name MUST be domain name or a loopback interface", URI: "https://indieauth.net/source/#client-identifier", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } @@ -98,11 +98,11 @@ func NewClientID(raw string) (*ClientID, error) { if !ip.IsLoopback() && ip.Compare(localhostIPv4) != 0 && ip.Compare(localhostIPv6) != 0 { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "client identifier URL MUST NOT be IPv4 or IPv6 addresses except for IPv4 " + "127.0.0.1 or IPv6 [::1]", URI: "https://indieauth.net/source/#client-identifier", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } diff --git a/internal/domain/email.go b/internal/domain/email.go index 43c6bba..313b0fa 100644 --- a/internal/domain/email.go +++ b/internal/domain/email.go @@ -11,8 +11,8 @@ type Email struct { } var ErrEmailInvalid error = Error{ - Code: "invalid_request", - Description: "cannot unmarshal email input", + Code: ErrorCodeInvalidRequest, + Description: "cannot parse email", } func NewEmail(src string) (*Email, error) { diff --git a/internal/domain/error.go b/internal/domain/error.go index 89f9f19..f7bc54e 100644 --- a/internal/domain/error.go +++ b/internal/domain/error.go @@ -2,39 +2,185 @@ package domain import ( "fmt" + "strconv" + http "github.com/valyala/fasthttp" "golang.org/x/xerrors" ) -// Error describes the data of a typical error. -//nolint: tagliatelle -type Error struct { - Code string `json:"error"` - Description string `json:"error_description,omitempty"` - URI string `json:"error_uri,omitempty"` - Frame xerrors.Frame `json:"-"` +type ( + // Error describes the format of a typical IndieAuth error. + Error struct { + // A single error code. + Code ErrorCode `json:"error"` + + // Human-readable ASCII text providing additional information, used to + // assist the client developer in understanding the error that occurred. + Description string `json:"error_description,omitempty"` //nolint: tagliatelle + + // A URI identifying a human-readable web page with information about + // the error, used to provide the client developer with additional + // information about the error. + URI string `json:"error_uri,omitempty"` //nolint: tagliatelle + + // REQUIRED if a "state" parameter was present in the client + // authorization request. The exact value received from the + // client. + State string `json:"-"` + + frame xerrors.Frame `json:"-"` + } + + // ErrorCode represent error code described in RFC 6749. + ErrorCode struct { + uid string + } +) + +var ( + ErrorCodeUndefined ErrorCode = ErrorCode{uid: ""} + + // RFC 6749 section 4.1.2.1: The resource owner or authorization server + // denied the request. + ErrorCodeAccessDenied ErrorCode = ErrorCode{uid: "access_denied"} + + // RFC 6749 section 5.2: Client authentication failed (e.g., unknown + // client, no client authentication included, or unsupported + // authentication method). + // + // The authorization server MAY return an HTTP 401 (Unauthorized) status + // code to indicate which HTTP authentication schemes are supported. + // + // If the client attempted to authenticate via the "Authorization" + // request header field, the authorization server MUST respond with an + // HTTP 401 (Unauthorized) status code and include the + // "WWW-Authenticate" response header field matching the authentication + // scheme used by the client. + ErrorCodeInvalidClient ErrorCode = ErrorCode{uid: "invalid_client"} + + // RFC 6749 section 5.2: The provided authorization grant (e.g., + // authorization code, resource owner credentials) or refresh token is + // invalid, expired, revoked, does not match the redirection URI used in + // the authorization request, or was issued to another client. + ErrorCodeInvalidGrant ErrorCode = ErrorCode{uid: "invalid_grant"} + + // RFC 6749 section 4.1.2.1: The request is missing a required + // parameter, includes an invalid parameter value, includes a parameter + // more than once, or is otherwise malformed. + // + // RFC 6749 section 5.2: The request is missing a required parameter, + // includes an unsupported parameter value (other than grant type), + // repeats a parameter, includes multiple credentials, utilizes more + // than one mechanism for authenticating the client, or is otherwise + // malformed. + ErrorCodeInvalidRequest ErrorCode = ErrorCode{uid: "invalid_request"} + + // RFC 6749 section 4.1.2.1: The requested scope is invalid, unknown, or + // malformed. + // + // RFC 6749 section 5.2: The requested scope is invalid, unknown, + // malformed, or exceeds the scope granted by the resource owner. + ErrorCodeInvalidScope ErrorCode = ErrorCode{uid: "invalid_scope"} + + // RFC 6749 section 4.1.2.1: The authorization server encountered an + // unexpected condition that prevented it from fulfilling the request. + // (This error code is needed because a 500 Internal Server Error HTTP + // status code cannot be returned to the client via an HTTP redirect.) + ErrorCodeServerError ErrorCode = ErrorCode{uid: "server_error"} + + // RFC 6749 section 4.1.2.1: The authorization server is currently + // unable to handle the request due to a temporary overloading or + // maintenance of the server. (This error code is needed because a 503 + // Service Unavailable HTTP status code cannot be returned to the client + // via an HTTP redirect.) + ErrorCodeTemporarilyUnavailable ErrorCode = ErrorCode{uid: "temporarily_unavailable"} + + // RFC 6749 section 4.1.2.1: The client is not authorized to request an + // authorization code using this method. + // + // RFC 6749 section 5.2: The authenticated client is not authorized to + // use this authorization grant type. + ErrorCodeUnauthorizedClient ErrorCode = ErrorCode{uid: "unauthorized_client"} + + // RFC 6749 section 5.2: The authorization grant type is not supported + // by the authorization server. + ErrorCodeUnsupportedGrantType ErrorCode = ErrorCode{uid: "unsupported_grant_type"} + + // RFC 6749 section 4.1.2.1: The authorization server does not support + // obtaining an authorization code using this method. + ErrorCodeUnsupportedResponseType ErrorCode = ErrorCode{uid: "unsupported_response_type"} +) + +// String returns a string representation of the error code. +func (ec ErrorCode) String() string { + return ec.uid } +// MarshalJSON encodes the error code into its string representation in JSON. +func (ec ErrorCode) MarshalJSON() ([]byte, error) { + return []byte(strconv.QuoteToASCII(ec.uid)), nil +} + +// Error returns a string representation of the error, satisfying the error +// interface. func (e Error) Error() string { return fmt.Sprint(e) } +// Format prints the stack as error detail. func (e Error) Format(s fmt.State, r rune) { xerrors.FormatError(e, s, r) } +// FormatError prints the receiver's error, if any. func (e Error) FormatError(p xerrors.Printer) error { - p.Print(e.Description) + p.Print(e.Code) - if e.URI != "" { - p.Print(" (", e.URI, ")") + if e.Description != "" { + p.Print(": ", e.Description) } - if !p.Detail() { - return e - } - - e.Frame.Format(p) + e.frame.Format(p) return nil } + +// SetReirectURI sets fasthttp.QueryArgs with the request state, code, +// description and error URI in the provided fasthttp.URI. +func (e Error) SetReirectURI(u *http.URI) { + if u == nil { + return + } + + for k, v := range map[string]string{ + "error": e.Code.String(), + "error_description": e.Description, + "error_uri": e.URI, + "state": e.State, + } { + if v == "" { + continue + } + + u.QueryArgs().Set(k, v) + } +} + +// NewError creates a new Error with the stack pointing to the function call +// line. +// +// If no code or ErrorCodeUndefined is provided, ErrorCodeAccessDenied will be +// used instead. +func NewError(code ErrorCode, description string) *Error { + if code == ErrorCodeUndefined { + code = ErrorCodeAccessDenied + } + + return &Error{ + Code: code, + Description: description, + URI: "", + State: "", + frame: xerrors.Caller(1), + } +} diff --git a/internal/domain/error_test.go b/internal/domain/error_test.go new file mode 100644 index 0000000..42b9d22 --- /dev/null +++ b/internal/domain/error_test.go @@ -0,0 +1,12 @@ +package domain_test + +import ( + "fmt" + + "source.toby3d.me/website/indieauth/internal/domain" +) + +func ExampleNewError() { + fmt.Printf("%v", domain.NewError(domain.ErrorCodeInvalidRequest, "client_id MUST be provided")) + // Output: invalid_request: client_id MUST be provided +} diff --git a/internal/domain/me.go b/internal/domain/me.go index 9670149..9185ddf 100644 --- a/internal/domain/me.go +++ b/internal/domain/me.go @@ -23,77 +23,77 @@ func NewMe(raw string) (*Me, error) { me := http.AcquireURI() if err := me.Parse(nil, []byte(raw)); err != nil { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: err.Error(), URI: "https://indieauth.net/source/#user-profile-url", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } scheme := string(me.Scheme()) if scheme != "http" && scheme != "https" { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "profile URL MUST have either an https or http scheme", URI: "https://indieauth.net/source/#user-profile-url", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } path := string(me.PathOriginal()) if path == "" || strings.Contains(path, "/.") || strings.Contains(path, "/..") { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "profile URL MUST contain a path component (/ is a valid path), MUST NOT " + "contain single-dot or double-dot path segments", URI: "https://indieauth.net/source/#user-profile-url", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } if me.Hash() != nil { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "profile URL MUST NOT contain a fragment component", URI: "https://indieauth.net/source/#user-profile-url", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } if me.Username() != nil || me.Password() != nil { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "profile URL MUST NOT contain a username or password component", URI: "https://indieauth.net/source/#user-profile-url", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } domain := string(me.Host()) if domain == "" { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "profile host name MUST be a domain name", URI: "https://indieauth.net/source/#user-profile-url", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } if _, port, _ := net.SplitHostPort(domain); port != "" { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "profile MUST NOT contain a port", URI: "https://indieauth.net/source/#user-profile-url", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } } if net.ParseIP(domain) != nil { return nil, Error{ - Code: "invalid_request", + Code: ErrorCodeInvalidRequest, Description: "profile MUST NOT be ipv4 or ipv6 addresses", URI: "https://indieauth.net/source/#user-profile-url", - Frame: xerrors.Caller(1), + frame: xerrors.Caller(1), } }