408 lines
13 KiB
Go
408 lines
13 KiB
Go
package shared
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// ErrorCodeInvalidRequest reports malformed or semantically invalid service
|
|
// input.
|
|
ErrorCodeInvalidRequest = "invalid_request"
|
|
|
|
// ErrorCodeChallengeNotFound reports that the requested challenge does not
|
|
// exist.
|
|
ErrorCodeChallengeNotFound = "challenge_not_found"
|
|
|
|
// ErrorCodeChallengeExpired reports that the requested challenge may no
|
|
// longer be confirmed.
|
|
ErrorCodeChallengeExpired = "challenge_expired"
|
|
|
|
// ErrorCodeInvalidCode reports that the submitted confirmation code does not
|
|
// match the stored challenge.
|
|
ErrorCodeInvalidCode = "invalid_code"
|
|
|
|
// ErrorCodeInvalidClientPublicKey reports that the submitted client public
|
|
// key does not satisfy the Ed25519/base64 contract.
|
|
ErrorCodeInvalidClientPublicKey = "invalid_client_public_key"
|
|
|
|
// ErrorCodeBlockedByPolicy reports that the auth flow is denied by current
|
|
// user or registration policy.
|
|
ErrorCodeBlockedByPolicy = "blocked_by_policy"
|
|
|
|
// ErrorCodeSessionLimitExceeded reports that creating another active session
|
|
// would violate the configured limit.
|
|
ErrorCodeSessionLimitExceeded = "session_limit_exceeded"
|
|
|
|
// ErrorCodeSessionNotFound reports that the requested device session does
|
|
// not exist.
|
|
ErrorCodeSessionNotFound = "session_not_found"
|
|
|
|
// ErrorCodeSubjectNotFound reports that the requested trusted internal
|
|
// subject does not exist.
|
|
ErrorCodeSubjectNotFound = "subject_not_found"
|
|
|
|
// ErrorCodeServiceUnavailable reports that a required dependency or
|
|
// propagation step is temporarily unavailable.
|
|
ErrorCodeServiceUnavailable = "service_unavailable"
|
|
|
|
// ErrorCodeInternalError reports that local state is inconsistent or an
|
|
// invariant was broken unexpectedly.
|
|
ErrorCodeInternalError = "internal_error"
|
|
)
|
|
|
|
const genericInvalidRequestMessage = "request is invalid"
|
|
|
|
var publicErrorStatusCodes = map[string]int{
|
|
ErrorCodeInvalidRequest: http.StatusBadRequest,
|
|
ErrorCodeInvalidClientPublicKey: http.StatusBadRequest,
|
|
ErrorCodeInvalidCode: http.StatusBadRequest,
|
|
ErrorCodeChallengeNotFound: http.StatusNotFound,
|
|
ErrorCodeChallengeExpired: http.StatusGone,
|
|
ErrorCodeBlockedByPolicy: http.StatusForbidden,
|
|
ErrorCodeSessionLimitExceeded: http.StatusConflict,
|
|
ErrorCodeServiceUnavailable: http.StatusServiceUnavailable,
|
|
}
|
|
|
|
var publicStableMessages = map[string]string{
|
|
ErrorCodeChallengeNotFound: "challenge not found",
|
|
ErrorCodeChallengeExpired: "challenge expired",
|
|
ErrorCodeInvalidCode: "confirmation code is invalid",
|
|
ErrorCodeInvalidClientPublicKey: "client_public_key is not a valid base64-encoded raw 32-byte Ed25519 public key",
|
|
ErrorCodeBlockedByPolicy: "authentication is blocked by policy",
|
|
ErrorCodeSessionLimitExceeded: "active session limit would be exceeded",
|
|
ErrorCodeServiceUnavailable: "service is unavailable",
|
|
}
|
|
|
|
var internalErrorStatusCodes = map[string]int{
|
|
ErrorCodeInvalidRequest: http.StatusBadRequest,
|
|
ErrorCodeSessionNotFound: http.StatusNotFound,
|
|
ErrorCodeSubjectNotFound: http.StatusNotFound,
|
|
ErrorCodeServiceUnavailable: http.StatusServiceUnavailable,
|
|
ErrorCodeInternalError: http.StatusInternalServerError,
|
|
}
|
|
|
|
var internalStableMessages = map[string]string{
|
|
ErrorCodeSessionNotFound: "session not found",
|
|
ErrorCodeSubjectNotFound: "subject not found",
|
|
ErrorCodeServiceUnavailable: "service is unavailable",
|
|
ErrorCodeInternalError: "internal server error",
|
|
}
|
|
|
|
// PublicErrorProjection describes one transport-ready public auth error after
|
|
// internal service errors have been normalized to the frozen client-safe
|
|
// surface.
|
|
type PublicErrorProjection struct {
|
|
// StatusCode is the HTTP status that should be returned to the public auth
|
|
// caller.
|
|
StatusCode int
|
|
|
|
// Code is the stable client-safe error code written into the public JSON
|
|
// envelope.
|
|
Code string
|
|
|
|
// Message is the client-safe error description exposed to the public auth
|
|
// caller.
|
|
Message string
|
|
}
|
|
|
|
// InternalErrorProjection describes one transport-ready internal API error
|
|
// after service-layer failures have been normalized to the frozen trusted
|
|
// caller surface.
|
|
type InternalErrorProjection struct {
|
|
// StatusCode is the HTTP status that should be returned to the trusted
|
|
// caller.
|
|
StatusCode int
|
|
|
|
// Code is the stable error code written into the internal JSON envelope.
|
|
Code string
|
|
|
|
// Message is the trusted-caller-safe error description exposed by the
|
|
// internal HTTP API.
|
|
Message string
|
|
}
|
|
|
|
// ServiceError projects one stable application-layer failure with a service
|
|
// error code and a caller-safe message.
|
|
type ServiceError struct {
|
|
// Code is the stable error code expected by later transport mapping.
|
|
Code string
|
|
|
|
// Message is the caller-safe error description.
|
|
Message string
|
|
|
|
// Err optionally stores the wrapped underlying cause.
|
|
Err error
|
|
}
|
|
|
|
// Error returns the caller-safe error description.
|
|
func (e *ServiceError) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
|
|
switch {
|
|
case strings.TrimSpace(e.Message) != "":
|
|
return e.Message
|
|
case strings.TrimSpace(e.Code) != "":
|
|
return e.Code
|
|
case e.Err != nil:
|
|
return e.Err.Error()
|
|
default:
|
|
return ErrorCodeInternalError
|
|
}
|
|
}
|
|
|
|
// Unwrap returns the wrapped cause, if any.
|
|
func (e *ServiceError) Unwrap() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
|
|
return e.Err
|
|
}
|
|
|
|
// NewServiceError returns a new typed application-layer error.
|
|
func NewServiceError(code string, message string, err error) *ServiceError {
|
|
return &ServiceError{
|
|
Code: strings.TrimSpace(code),
|
|
Message: strings.TrimSpace(message),
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
// IsPublicErrorCode reports whether code belongs to the frozen public auth
|
|
// error surface.
|
|
func IsPublicErrorCode(code string) bool {
|
|
_, ok := publicErrorStatusCodes[strings.TrimSpace(code)]
|
|
return ok
|
|
}
|
|
|
|
// IsInternalOnlyErrorCode reports whether code is intentionally excluded from
|
|
// the public auth transport surface.
|
|
func IsInternalOnlyErrorCode(code string) bool {
|
|
switch strings.TrimSpace(code) {
|
|
case ErrorCodeSessionNotFound, ErrorCodeSubjectNotFound, ErrorCodeInternalError:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// IsSendEmailCodePublicErrorCode reports whether code may be exposed by the
|
|
// public send-email-code route after public projection.
|
|
func IsSendEmailCodePublicErrorCode(code string) bool {
|
|
switch strings.TrimSpace(code) {
|
|
case ErrorCodeInvalidRequest, ErrorCodeServiceUnavailable:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// IsConfirmEmailCodePublicErrorCode reports whether code may be exposed by the
|
|
// public confirm-email-code route after public projection.
|
|
func IsConfirmEmailCodePublicErrorCode(code string) bool {
|
|
switch strings.TrimSpace(code) {
|
|
case ErrorCodeInvalidRequest,
|
|
ErrorCodeChallengeNotFound,
|
|
ErrorCodeChallengeExpired,
|
|
ErrorCodeInvalidCode,
|
|
ErrorCodeInvalidClientPublicKey,
|
|
ErrorCodeBlockedByPolicy,
|
|
ErrorCodeSessionLimitExceeded,
|
|
ErrorCodeServiceUnavailable:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// PublicHTTPStatusCode reports the frozen public HTTP status for code. Unknown
|
|
// or internal-only codes are normalized to 503 service_unavailable.
|
|
func PublicHTTPStatusCode(code string) int {
|
|
if statusCode, ok := publicErrorStatusCodes[strings.TrimSpace(code)]; ok {
|
|
return statusCode
|
|
}
|
|
|
|
return http.StatusServiceUnavailable
|
|
}
|
|
|
|
// ProjectPublicError normalizes err to the frozen public-auth error surface.
|
|
// Unknown and internal-only service failures are intentionally projected as
|
|
// 503 service_unavailable so internal invariants do not leak to public callers.
|
|
func ProjectPublicError(err error) PublicErrorProjection {
|
|
serviceErr, ok := errors.AsType[*ServiceError](err)
|
|
code := CodeOf(err)
|
|
if !IsPublicErrorCode(code) {
|
|
return PublicErrorProjection{
|
|
StatusCode: http.StatusServiceUnavailable,
|
|
Code: ErrorCodeServiceUnavailable,
|
|
Message: publicMessageForCode(ErrorCodeServiceUnavailable, ""),
|
|
}
|
|
}
|
|
|
|
message := ""
|
|
if ok && serviceErr != nil {
|
|
message = serviceErr.Message
|
|
}
|
|
|
|
return PublicErrorProjection{
|
|
StatusCode: PublicHTTPStatusCode(code),
|
|
Code: code,
|
|
Message: publicMessageForCode(code, message),
|
|
}
|
|
}
|
|
|
|
// InternalHTTPStatusCode reports the frozen internal HTTP status for code.
|
|
// Unknown codes are normalized to 500 internal_error.
|
|
func InternalHTTPStatusCode(code string) int {
|
|
if statusCode, ok := internalErrorStatusCodes[strings.TrimSpace(code)]; ok {
|
|
return statusCode
|
|
}
|
|
|
|
return http.StatusInternalServerError
|
|
}
|
|
|
|
// ProjectInternalError normalizes err to the frozen internal trusted HTTP
|
|
// error surface. Unknown failures are intentionally projected as
|
|
// 500 internal_error so transport callers do not depend on unclassified local
|
|
// failures.
|
|
func ProjectInternalError(err error) InternalErrorProjection {
|
|
serviceErr, ok := errors.AsType[*ServiceError](err)
|
|
code := CodeOf(err)
|
|
if _, known := internalErrorStatusCodes[code]; !known {
|
|
return InternalErrorProjection{
|
|
StatusCode: http.StatusInternalServerError,
|
|
Code: ErrorCodeInternalError,
|
|
Message: internalMessageForCode(ErrorCodeInternalError, ""),
|
|
}
|
|
}
|
|
|
|
message := ""
|
|
if ok && serviceErr != nil {
|
|
message = serviceErr.Message
|
|
}
|
|
|
|
return InternalErrorProjection{
|
|
StatusCode: InternalHTTPStatusCode(code),
|
|
Code: code,
|
|
Message: internalMessageForCode(code, message),
|
|
}
|
|
}
|
|
|
|
// InvalidRequest reports one malformed or semantically invalid caller input.
|
|
func InvalidRequest(message string) *ServiceError {
|
|
return NewServiceError(ErrorCodeInvalidRequest, message, nil)
|
|
}
|
|
|
|
// ChallengeNotFound reports that the requested challenge does not exist.
|
|
func ChallengeNotFound() *ServiceError {
|
|
return NewServiceError(ErrorCodeChallengeNotFound, "challenge not found", nil)
|
|
}
|
|
|
|
// ChallengeExpired reports that the requested challenge is expired.
|
|
func ChallengeExpired() *ServiceError {
|
|
return NewServiceError(ErrorCodeChallengeExpired, "challenge expired", nil)
|
|
}
|
|
|
|
// InvalidCode reports that the submitted confirmation code is invalid.
|
|
func InvalidCode() *ServiceError {
|
|
return NewServiceError(ErrorCodeInvalidCode, "confirmation code is invalid", nil)
|
|
}
|
|
|
|
// InvalidClientPublicKey reports that the submitted client public key does not
|
|
// satisfy the frozen contract.
|
|
func InvalidClientPublicKey() *ServiceError {
|
|
return NewServiceError(
|
|
ErrorCodeInvalidClientPublicKey,
|
|
"client_public_key is not a valid base64-encoded raw 32-byte Ed25519 public key",
|
|
nil,
|
|
)
|
|
}
|
|
|
|
// BlockedByPolicy reports that the current auth flow is denied by policy.
|
|
func BlockedByPolicy() *ServiceError {
|
|
return NewServiceError(ErrorCodeBlockedByPolicy, "authentication is blocked by policy", nil)
|
|
}
|
|
|
|
// SessionLimitExceeded reports that creating another active session would
|
|
// exceed the current configured limit.
|
|
func SessionLimitExceeded() *ServiceError {
|
|
return NewServiceError(ErrorCodeSessionLimitExceeded, "active session limit would be exceeded", nil)
|
|
}
|
|
|
|
// SessionNotFound reports that the requested session does not exist.
|
|
func SessionNotFound() *ServiceError {
|
|
return NewServiceError(ErrorCodeSessionNotFound, "session not found", nil)
|
|
}
|
|
|
|
// SubjectNotFound reports that the requested internal subject does not exist.
|
|
func SubjectNotFound() *ServiceError {
|
|
return NewServiceError(ErrorCodeSubjectNotFound, "subject not found", nil)
|
|
}
|
|
|
|
// ServiceUnavailable reports that a required dependency or propagation step is
|
|
// temporarily unavailable.
|
|
func ServiceUnavailable(err error) *ServiceError {
|
|
return NewServiceError(ErrorCodeServiceUnavailable, "service is unavailable", err)
|
|
}
|
|
|
|
// InternalError reports an invariant-breaking local failure.
|
|
func InternalError(err error) *ServiceError {
|
|
return NewServiceError(ErrorCodeInternalError, "internal error", err)
|
|
}
|
|
|
|
// CodeOf returns the stable service error code of err when err wraps a
|
|
// ServiceError. Otherwise it returns ErrorCodeInternalError.
|
|
func CodeOf(err error) string {
|
|
serviceErr, ok := errors.AsType[*ServiceError](err)
|
|
if !ok || serviceErr == nil || strings.TrimSpace(serviceErr.Code) == "" {
|
|
return ErrorCodeInternalError
|
|
}
|
|
|
|
return serviceErr.Code
|
|
}
|
|
|
|
func publicMessageForCode(code string, message string) string {
|
|
trimmedMessage := strings.TrimSpace(message)
|
|
|
|
switch strings.TrimSpace(code) {
|
|
case ErrorCodeInvalidRequest:
|
|
if trimmedMessage != "" {
|
|
return trimmedMessage
|
|
}
|
|
return genericInvalidRequestMessage
|
|
case ErrorCodeServiceUnavailable:
|
|
return publicStableMessages[ErrorCodeServiceUnavailable]
|
|
default:
|
|
if stableMessage, ok := publicStableMessages[strings.TrimSpace(code)]; ok {
|
|
return stableMessage
|
|
}
|
|
return publicStableMessages[ErrorCodeServiceUnavailable]
|
|
}
|
|
}
|
|
|
|
func internalMessageForCode(code string, message string) string {
|
|
trimmedMessage := strings.TrimSpace(message)
|
|
|
|
switch strings.TrimSpace(code) {
|
|
case ErrorCodeInvalidRequest:
|
|
if trimmedMessage != "" {
|
|
return trimmedMessage
|
|
}
|
|
return genericInvalidRequestMessage
|
|
case ErrorCodeSessionNotFound,
|
|
ErrorCodeSubjectNotFound,
|
|
ErrorCodeServiceUnavailable,
|
|
ErrorCodeInternalError:
|
|
if stableMessage, ok := internalStableMessages[strings.TrimSpace(code)]; ok {
|
|
return stableMessage
|
|
}
|
|
return internalStableMessages[ErrorCodeInternalError]
|
|
default:
|
|
return internalStableMessages[ErrorCodeInternalError]
|
|
}
|
|
}
|