auth/internal/domain/error.go

275 lines
9.3 KiB
Go

package domain
import (
"fmt"
"net/url"
"strconv"
"strings"
"golang.org/x/xerrors"
"source.toby3d.me/toby3d/auth/internal/common"
)
type (
// Error describes the format of a typical IndieAuth error.
//nolint:tagliatelle // RFC 6749 section 5.2
Error struct {
frame xerrors.Frame `json:"-"`
// 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"`
// 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"`
// REQUIRED if a "state" parameter was present in the client
// authorization request. The exact value received from the
// client.
State string `json:"-"`
}
// ErrorCode represent error code described in RFC 6749.
ErrorCode struct {
errorCode string
}
)
var (
// ErrorCodeUnd describes an unrecognized error code.
ErrorCodeUnd = ErrorCode{errorCode: ""} // "und"
// ErrorCodeAccessDenied describes the access_denied error code.
//
// RFC 6749 section 4.1.2.1: The resource owner or authorization server
// denied the request.
ErrorCodeAccessDenied = ErrorCode{errorCode: "access_denied"} // "access_denied"
// ErrorCodeInvalidClient describes the invalid_client error code.
//
// 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: "invalid_client"} // "invalid_client"
// ErrorCodeInvalidGrant describes the invalid_grant error code.
//
// 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: "invalid_grant"} // "invalid_grant"
// ErrorCodeInvalidRequest describes the invalid_request error code.
//
// IndieAuth: The request is not valid.
//
// 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: "invalid_request"} // "invalid_request"
// ErrorCodeInvalidScope describes the invalid_scope error code.
//
// 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: "invalid_scope"} // "invalid_scope"
// ErrorCodeServerError describes the server_error error code.
//
// 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: "server_error"} // "server_error"
// ErrorCodeTemporarilyUnavailable describes the temporarily_unavailable error code.
//
// 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: "temporarily_unavailable"} // "temporarily_unavailable"
// ErrorCodeUnauthorizedClient describes the unauthorized_client error code.
//
// 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: "unauthorized_client"} // "unauthorized_client"
// ErrorCodeUnsupportedGrantType describes the unsupported_grant_type error code.
//
// RFC 6749 section 5.2: The authorization grant type is not supported
// by the authorization server.
ErrorCodeUnsupportedGrantType = ErrorCode{errorCode: "unsupported_grant_type"} // "unsupported_grant_type"
// ErrorCodeUnsupportedResponseType describes the unsupported_response_type error code.
//
// RFC 6749 section 4.1.2.1: The authorization server does not support
// obtaining an authorization code using this method.
ErrorCodeUnsupportedResponseType = ErrorCode{errorCode: "unsupported_response_type"} // "unsupported_response_type"
// ErrorCodeInvalidToken describes the invalid_token error code.
//
// IndieAuth: The access token provided is expired, revoked, or invalid.
ErrorCodeInvalidToken = ErrorCode{errorCode: "invalid_token"} // "invalid_token"
// ErrorCodeInsufficientScope describes the insufficient_scope error code.
//
// IndieAuth: The request requires higher privileges than provided.
ErrorCodeInsufficientScope = ErrorCode{errorCode: "insufficient_scope"} // "insufficient_scope"
)
var ErrErrorCodeUnknown error = NewError(ErrorCodeInvalidRequest, "unknown error code", "")
//nolint:gochecknoglobals // maps cannot be constants
var uidsErrorCodes = map[string]ErrorCode{
ErrorCodeAccessDenied.errorCode: ErrorCodeAccessDenied,
ErrorCodeInsufficientScope.errorCode: ErrorCodeInsufficientScope,
ErrorCodeInvalidClient.errorCode: ErrorCodeInvalidClient,
ErrorCodeInvalidGrant.errorCode: ErrorCodeInvalidGrant,
ErrorCodeInvalidRequest.errorCode: ErrorCodeInvalidRequest,
ErrorCodeInvalidScope.errorCode: ErrorCodeInvalidScope,
ErrorCodeInvalidToken.errorCode: ErrorCodeInvalidToken,
ErrorCodeServerError.errorCode: ErrorCodeServerError,
ErrorCodeTemporarilyUnavailable.errorCode: ErrorCodeTemporarilyUnavailable,
ErrorCodeUnauthorizedClient.errorCode: ErrorCodeUnauthorizedClient,
ErrorCodeUnsupportedGrantType.errorCode: ErrorCodeUnsupportedGrantType,
ErrorCodeUnsupportedResponseType.errorCode: ErrorCodeUnsupportedResponseType,
}
// String returns a string representation of the error code.
func (ec ErrorCode) String() string {
if ec.errorCode != "" {
return ec.errorCode
}
return common.Und
}
func (ec ErrorCode) GoString() string {
return "domain.ErrorCode(" + ec.String() + ")"
}
// UnmarshalForm implements custom unmarshler for form values.
func (ec *ErrorCode) UnmarshalForm(v []byte) error {
code, ok := uidsErrorCodes[strings.ToLower(string(v))]
if !ok {
return fmt.Errorf("UnmarshalForm: %w", ErrErrorCodeUnknown)
}
*ec = code
return nil
}
// MarshalJSON encodes the error code into its string representation in JSON.
func (ec ErrorCode) MarshalJSON() ([]byte, error) {
return []byte(strconv.QuoteToASCII(ec.errorCode)), 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(state fmt.State, r rune) {
xerrors.FormatError(e, state, r)
}
// FormatError prints the receiver's error, if any.
func (e Error) FormatError(printer xerrors.Printer) error {
printer.Print(e.Code)
if e.Description != "" {
printer.Print(": ", e.Description)
}
if !printer.Detail() {
return nil
}
e.frame.Format(printer)
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 *url.URL) {
if u == nil {
return
}
q := u.Query()
for key, val := range map[string]string{
"error": e.Code.String(),
"error_description": e.Description,
"error_uri": e.URI,
"state": e.State,
} {
if val == "" {
continue
}
q.Set(key, val)
}
u.RawQuery = q.Encode()
}
// NewError creates a new Error with the stack pointing to the function call
// line number.
//
// If no code or ErrorCodeUnd is provided, ErrorCodeAccessDenied will be
// used instead.
func NewError(code ErrorCode, description, uri string, requestState ...string) *Error {
if code == ErrorCodeUnd {
code = ErrorCodeAccessDenied
}
var state string
if len(requestState) > 0 {
state = requestState[0]
}
return &Error{
Code: code,
Description: description,
URI: uri,
State: state,
frame: xerrors.Caller(1),
}
}