feat: authsession service

This commit is contained in:
Ilia Denisov
2026-04-08 16:23:07 +02:00
committed by GitHub
parent 28f04916af
commit 86a68ed9d0
174 changed files with 31732 additions and 112 deletions
@@ -0,0 +1,407 @@
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]
}
}