diff --git a/backend/internal/common/errors.go b/backend/internal/common/errors.go index cab2ebb2..3bb802b5 100644 --- a/backend/internal/common/errors.go +++ b/backend/internal/common/errors.go @@ -8,21 +8,28 @@ import ( type AppError interface { error + HttpStatusCode() int } +type AppErrorDescription interface { + AppError + + Description() string +} + // Custom error types for various conditions type AlreadyInUseError struct { Property string } -func (e *AlreadyInUseError) Error() string { +func (e AlreadyInUseError) Error() string { return e.Property + " is already in use" } -func (e *AlreadyInUseError) HttpStatusCode() int { return http.StatusBadRequest } +func (e AlreadyInUseError) HttpStatusCode() int { return http.StatusBadRequest } -func (e *AlreadyInUseError) Is(target error) bool { +func (e AlreadyInUseError) Is(target error) bool { // Ignore the field property when checking if an error is of the type AlreadyInUseError x := &AlreadyInUseError{} return errors.As(target, &x) @@ -30,445 +37,357 @@ func (e *AlreadyInUseError) Is(target error) bool { type SetupNotAvailableError struct{} -func (e *SetupNotAvailableError) Error() string { return "not found" } -func (e *SetupNotAvailableError) HttpStatusCode() int { return http.StatusNotFound } +func (e SetupNotAvailableError) Error() string { return "not found" } +func (e SetupNotAvailableError) HttpStatusCode() int { return http.StatusNotFound } type TokenInvalidOrExpiredError struct{} -func (e *TokenInvalidOrExpiredError) Error() string { return "token is invalid or expired" } -func (e *TokenInvalidOrExpiredError) HttpStatusCode() int { return http.StatusUnauthorized } +func (e TokenInvalidOrExpiredError) Error() string { return "token is invalid or expired" } +func (e TokenInvalidOrExpiredError) HttpStatusCode() int { return http.StatusUnauthorized } type DeviceCodeInvalid struct{} -func (e *DeviceCodeInvalid) Error() string { +func (e DeviceCodeInvalid) Error() string { return "one time access code must be used on the device it was generated for" } -func (e *DeviceCodeInvalid) HttpStatusCode() int { return http.StatusUnauthorized } +func (e DeviceCodeInvalid) HttpStatusCode() int { return http.StatusUnauthorized } type TokenInvalidError struct{} -func (e *TokenInvalidError) Error() string { - return "Token is invalid" -} -func (e *TokenInvalidError) HttpStatusCode() int { return http.StatusUnauthorized } +func (e TokenInvalidError) Error() string { return "Token is invalid" } +func (e TokenInvalidError) HttpStatusCode() int { return http.StatusUnauthorized } type OidcMissingAuthorizationError struct{} -func (e *OidcMissingAuthorizationError) Error() string { return "missing authorization" } -func (e *OidcMissingAuthorizationError) HttpStatusCode() int { return http.StatusForbidden } +func (e OidcMissingAuthorizationError) Error() string { return "missing authorization" } +func (e OidcMissingAuthorizationError) HttpStatusCode() int { return http.StatusForbidden } type OidcGrantTypeNotSupportedError struct{} -func (e *OidcGrantTypeNotSupportedError) Error() string { return "grant type not supported" } -func (e *OidcGrantTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcGrantTypeNotSupportedError) Error() string { return "grant type not supported" } +func (e OidcGrantTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest } type OidcMissingClientCredentialsError struct{} -func (e *OidcMissingClientCredentialsError) Error() string { return "client id or secret not provided" } -func (e *OidcMissingClientCredentialsError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcMissingClientCredentialsError) Error() string { return "client id or secret not provided" } +func (e OidcMissingClientCredentialsError) HttpStatusCode() int { return http.StatusBadRequest } type OidcClientSecretInvalidError struct{} -func (e *OidcClientSecretInvalidError) Error() string { return "invalid client secret" } -func (e *OidcClientSecretInvalidError) HttpStatusCode() int { return http.StatusUnauthorized } +func (e OidcClientSecretInvalidError) Error() string { return "invalid client secret" } +func (e OidcClientSecretInvalidError) HttpStatusCode() int { return http.StatusUnauthorized } type OidcClientAssertionInvalidError struct{} -func (e *OidcClientAssertionInvalidError) Error() string { return "invalid client assertion" } -func (e *OidcClientAssertionInvalidError) HttpStatusCode() int { return http.StatusUnauthorized } +func (e OidcClientAssertionInvalidError) Error() string { return "invalid client assertion" } +func (e OidcClientAssertionInvalidError) HttpStatusCode() int { return http.StatusUnauthorized } type OidcInvalidAuthorizationCodeError struct{} -func (e *OidcInvalidAuthorizationCodeError) Error() string { return "invalid authorization code" } -func (e *OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcInvalidAuthorizationCodeError) Error() string { return "invalid authorization code" } +func (e OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return http.StatusBadRequest } type OidcClientNotFoundError struct{} -func (e *OidcClientNotFoundError) Error() string { return "client not found" } -func (e *OidcClientNotFoundError) HttpStatusCode() int { return http.StatusNotFound } +func (e OidcClientNotFoundError) Error() string { return "client not found" } +func (e OidcClientNotFoundError) HttpStatusCode() int { return http.StatusNotFound } type OidcMissingCallbackURLError struct{} -func (e *OidcMissingCallbackURLError) Error() string { +func (e OidcMissingCallbackURLError) Error() string { return "unable to detect callback url, it might be necessary for an admin to fix this" } -func (e *OidcMissingCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcMissingCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest } type OidcInvalidCallbackURLError struct{} -func (e *OidcInvalidCallbackURLError) Error() string { +func (e OidcInvalidCallbackURLError) Error() string { return "invalid callback URL, it might be necessary for an admin to fix this" } -func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcInvalidCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest } type FileTypeNotSupportedError struct{} -func (e *FileTypeNotSupportedError) Error() string { return "file type not supported" } -func (e *FileTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest } +func (e FileTypeNotSupportedError) Error() string { return "file type not supported" } +func (e FileTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest } type FileTooLargeError struct { MaxSize string } -func (e *FileTooLargeError) Error() string { +func (e FileTooLargeError) Error() string { return fmt.Sprintf("The file can't be larger than %s", e.MaxSize) } -func (e *FileTooLargeError) HttpStatusCode() int { return http.StatusRequestEntityTooLarge } +func (e FileTooLargeError) HttpStatusCode() int { return http.StatusRequestEntityTooLarge } type NotSignedInError struct{} -func (e *NotSignedInError) Error() string { return "You are not signed in" } -func (e *NotSignedInError) HttpStatusCode() int { return http.StatusUnauthorized } +func (e NotSignedInError) Error() string { return "You are not signed in" } +func (e NotSignedInError) HttpStatusCode() int { return http.StatusUnauthorized } type MissingAccessToken struct{} -func (e *MissingAccessToken) Error() string { return "Missing access token" } -func (e *MissingAccessToken) HttpStatusCode() int { return http.StatusUnauthorized } +func (e MissingAccessToken) Error() string { return "Missing access token" } +func (e MissingAccessToken) HttpStatusCode() int { return http.StatusUnauthorized } type MissingPermissionError struct{} -func (e *MissingPermissionError) Error() string { +func (e MissingPermissionError) Error() string { return "You don't have permission to perform this action" } -func (e *MissingPermissionError) HttpStatusCode() int { return http.StatusForbidden } +func (e MissingPermissionError) HttpStatusCode() int { return http.StatusForbidden } type TooManyRequestsError struct{} -func (e *TooManyRequestsError) Error() string { - return "Too many requests" -} -func (e *TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests } +func (e TooManyRequestsError) Error() string { return "Too many requests" } +func (e TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests } type UserIdNotProvidedError struct{} -func (e *UserIdNotProvidedError) Error() string { - return "User id not provided" -} -func (e *UserIdNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest } +func (e UserIdNotProvidedError) Error() string { return "User id not provided" } +func (e UserIdNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest } type UserNotFoundError struct{} -func (e *UserNotFoundError) Error() string { - return "User not found" -} -func (e *UserNotFoundError) HttpStatusCode() int { return http.StatusNotFound } +func (e UserNotFoundError) Error() string { return "User not found" } +func (e UserNotFoundError) HttpStatusCode() int { return http.StatusNotFound } type ClientIdOrSecretNotProvidedError struct{} -func (e *ClientIdOrSecretNotProvidedError) Error() string { - return "Client id or secret not provided" -} -func (e *ClientIdOrSecretNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest } +func (e ClientIdOrSecretNotProvidedError) Error() string { return "Client id or secret not provided" } +func (e ClientIdOrSecretNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest } type WrongFileTypeError struct { ExpectedFileType string } -func (e *WrongFileTypeError) Error() string { +func (e WrongFileTypeError) Error() string { return fmt.Sprintf("File must be of type %s", e.ExpectedFileType) } -func (e *WrongFileTypeError) HttpStatusCode() int { return http.StatusBadRequest } +func (e WrongFileTypeError) HttpStatusCode() int { return http.StatusBadRequest } type MissingSessionIdError struct{} -func (e *MissingSessionIdError) Error() string { - return "Missing session id" -} -func (e *MissingSessionIdError) HttpStatusCode() int { return http.StatusBadRequest } +func (e MissingSessionIdError) Error() string { return "Missing session id" } +func (e MissingSessionIdError) HttpStatusCode() int { return http.StatusBadRequest } type ReservedClaimError struct { Key string } -func (e *ReservedClaimError) Error() string { +func (e ReservedClaimError) Error() string { return fmt.Sprintf("Claim %s is reserved and can't be used", e.Key) } -func (e *ReservedClaimError) HttpStatusCode() int { return http.StatusBadRequest } +func (e ReservedClaimError) HttpStatusCode() int { return http.StatusBadRequest } type DuplicateClaimError struct { Key string } -func (e *DuplicateClaimError) Error() string { +func (e DuplicateClaimError) Error() string { return fmt.Sprintf("Claim %s is already defined", e.Key) } -func (e *DuplicateClaimError) HttpStatusCode() int { return http.StatusBadRequest } +func (e DuplicateClaimError) HttpStatusCode() int { return http.StatusBadRequest } type OidcInvalidCodeVerifierError struct{} -func (e *OidcInvalidCodeVerifierError) Error() string { - return "Invalid code verifier" -} -func (e *OidcInvalidCodeVerifierError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcInvalidCodeVerifierError) Error() string { return "Invalid code verifier" } +func (e OidcInvalidCodeVerifierError) HttpStatusCode() int { return http.StatusBadRequest } type OidcMissingCodeChallengeError struct{} -func (e *OidcMissingCodeChallengeError) Error() string { - return "Missing code challenge" -} -func (e *OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcMissingCodeChallengeError) Error() string { return "Missing code challenge" } +func (e OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest } type LdapUserUpdateError struct{} -func (e *LdapUserUpdateError) Error() string { - return "LDAP users can't be updated" -} -func (e *LdapUserUpdateError) HttpStatusCode() int { return http.StatusForbidden } +func (e LdapUserUpdateError) Error() string { return "LDAP users can't be updated" } +func (e LdapUserUpdateError) HttpStatusCode() int { return http.StatusForbidden } type LdapUserGroupUpdateError struct{} -func (e *LdapUserGroupUpdateError) Error() string { - return "LDAP user groups can't be updated" -} -func (e *LdapUserGroupUpdateError) HttpStatusCode() int { return http.StatusForbidden } +func (e LdapUserGroupUpdateError) Error() string { return "LDAP user groups can't be updated" } +func (e LdapUserGroupUpdateError) HttpStatusCode() int { return http.StatusForbidden } type OidcAccessDeniedError struct{} -func (e *OidcAccessDeniedError) Error() string { - return "You're not allowed to access this service" -} -func (e *OidcAccessDeniedError) HttpStatusCode() int { return http.StatusForbidden } +func (e OidcAccessDeniedError) Error() string { return "You're not allowed to access this service" } +func (e OidcAccessDeniedError) HttpStatusCode() int { return http.StatusForbidden } type OidcClientIdNotMatchingError struct{} -func (e *OidcClientIdNotMatchingError) Error() string { +func (e OidcClientIdNotMatchingError) Error() string { return "Client id in request doesn't match client id in token" } -func (e *OidcClientIdNotMatchingError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcClientIdNotMatchingError) HttpStatusCode() int { return http.StatusBadRequest } type OidcNoCallbackURLError struct{} -func (e *OidcNoCallbackURLError) Error() string { +func (e OidcNoCallbackURLError) Error() string { return "No callback URL provided" } -func (e *OidcNoCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcNoCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest } type UiConfigDisabledError struct{} -func (e *UiConfigDisabledError) Error() string { +func (e UiConfigDisabledError) Error() string { return "The configuration can't be changed since the UI configuration is disabled" } -func (e *UiConfigDisabledError) HttpStatusCode() int { return http.StatusForbidden } +func (e UiConfigDisabledError) HttpStatusCode() int { return http.StatusForbidden } type InvalidUUIDError struct{} -func (e *InvalidUUIDError) Error() string { - return "Invalid UUID" -} -func (e *InvalidUUIDError) HttpStatusCode() int { return http.StatusBadRequest } +func (e InvalidUUIDError) Error() string { return "Invalid UUID" } +func (e InvalidUUIDError) HttpStatusCode() int { return http.StatusBadRequest } type OneTimeAccessDisabledError struct{} -func (e *OneTimeAccessDisabledError) Error() string { - return "One-time access is disabled" -} -func (e *OneTimeAccessDisabledError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OneTimeAccessDisabledError) Error() string { return "One-time access is disabled" } +func (e OneTimeAccessDisabledError) HttpStatusCode() int { return http.StatusBadRequest } type InvalidAPIKeyError struct{} -func (e *InvalidAPIKeyError) Error() string { - return "Invalid Api Key" -} -func (e *InvalidAPIKeyError) HttpStatusCode() int { return http.StatusUnauthorized } +func (e InvalidAPIKeyError) Error() string { return "Invalid Api Key" } +func (e InvalidAPIKeyError) HttpStatusCode() int { return http.StatusUnauthorized } type NoAPIKeyProvidedError struct{} -func (e *NoAPIKeyProvidedError) Error() string { - return "No API Key Provided" -} -func (e *NoAPIKeyProvidedError) HttpStatusCode() int { return http.StatusUnauthorized } +func (e NoAPIKeyProvidedError) Error() string { return "No API Key Provided" } +func (e NoAPIKeyProvidedError) HttpStatusCode() int { return http.StatusUnauthorized } type APIKeyNotFoundError struct{} -func (e *APIKeyNotFoundError) Error() string { - return "API Key Not Found" -} -func (e *APIKeyNotFoundError) HttpStatusCode() int { return http.StatusUnauthorized } +func (e APIKeyNotFoundError) Error() string { return "API Key Not Found" } +func (e APIKeyNotFoundError) HttpStatusCode() int { return http.StatusUnauthorized } type APIKeyNotExpiredError struct{} -func (e *APIKeyNotExpiredError) Error() string { - return "API Key is not expired yet" -} -func (e *APIKeyNotExpiredError) HttpStatusCode() int { return http.StatusBadRequest } +func (e APIKeyNotExpiredError) Error() string { return "API Key is not expired yet" } +func (e APIKeyNotExpiredError) HttpStatusCode() int { return http.StatusBadRequest } type APIKeyExpirationDateError struct{} -func (e *APIKeyExpirationDateError) Error() string { +func (e APIKeyExpirationDateError) Error() string { return "API Key expiration time must be in the future" } -func (e *APIKeyExpirationDateError) HttpStatusCode() int { return http.StatusBadRequest } +func (e APIKeyExpirationDateError) HttpStatusCode() int { return http.StatusBadRequest } type APIKeyAuthNotAllowedError struct{} -func (e *APIKeyAuthNotAllowedError) Error() string { +func (e APIKeyAuthNotAllowedError) Error() string { return "API key authentication is not allowed for this endpoint" } -func (e *APIKeyAuthNotAllowedError) HttpStatusCode() int { return http.StatusForbidden } +func (e APIKeyAuthNotAllowedError) HttpStatusCode() int { return http.StatusForbidden } type OidcInvalidRefreshTokenError struct{} -func (e *OidcInvalidRefreshTokenError) Error() string { - return "refresh token is invalid or expired" -} -func (e *OidcInvalidRefreshTokenError) HttpStatusCode() int { - return http.StatusBadRequest -} +func (e OidcInvalidRefreshTokenError) Error() string { return "refresh token is invalid or expired" } +func (e OidcInvalidRefreshTokenError) HttpStatusCode() int { return http.StatusBadRequest } type OidcMissingRefreshTokenError struct{} -func (e *OidcMissingRefreshTokenError) Error() string { - return "refresh token is required" -} -func (e *OidcMissingRefreshTokenError) HttpStatusCode() int { - return http.StatusBadRequest -} +func (e OidcMissingRefreshTokenError) Error() string { return "refresh token is required" } +func (e OidcMissingRefreshTokenError) HttpStatusCode() int { return http.StatusBadRequest } type OidcMissingAuthorizationCodeError struct{} -func (e *OidcMissingAuthorizationCodeError) Error() string { - return "authorization code is required" -} -func (e *OidcMissingAuthorizationCodeError) HttpStatusCode() int { - return http.StatusBadRequest -} +func (e OidcMissingAuthorizationCodeError) Error() string { return "authorization code is required" } +func (e OidcMissingAuthorizationCodeError) HttpStatusCode() int { return http.StatusBadRequest } type UserDisabledError struct{} -func (e *UserDisabledError) Error() string { - return "User account is disabled" -} -func (e *UserDisabledError) HttpStatusCode() int { - return http.StatusForbidden -} +func (e UserDisabledError) Error() string { return "User account is disabled" } +func (e UserDisabledError) HttpStatusCode() int { return http.StatusForbidden } -type ValidationError struct { - Message string -} +type ValidationError struct{ Message string } -func (e *ValidationError) Error() string { - return e.Message -} +func (e ValidationError) Error() string { return e.Message } -func (e *ValidationError) HttpStatusCode() int { - return http.StatusBadRequest -} +func (e ValidationError) HttpStatusCode() int { return http.StatusBadRequest } type OidcDeviceCodeExpiredError struct{} -func (e *OidcDeviceCodeExpiredError) Error() string { - return "device code has expired" -} -func (e *OidcDeviceCodeExpiredError) HttpStatusCode() int { - return http.StatusBadRequest -} +func (e OidcDeviceCodeExpiredError) Error() string { return "device code has expired" } +func (e OidcDeviceCodeExpiredError) HttpStatusCode() int { return http.StatusBadRequest } type OidcInvalidDeviceCodeError struct{} -func (e *OidcInvalidDeviceCodeError) Error() string { - return "invalid device code" -} -func (e *OidcInvalidDeviceCodeError) HttpStatusCode() int { - return http.StatusBadRequest -} +func (e OidcInvalidDeviceCodeError) Error() string { return "invalid device code" } +func (e OidcInvalidDeviceCodeError) HttpStatusCode() int { return http.StatusBadRequest } type OidcSlowDownError struct{} -func (e *OidcSlowDownError) Error() string { - return "polling too frequently" -} -func (e *OidcSlowDownError) HttpStatusCode() int { - return http.StatusTooManyRequests -} +func (e OidcSlowDownError) Error() string { return "polling too frequently" } +func (e OidcSlowDownError) HttpStatusCode() int { return http.StatusTooManyRequests } type OidcAuthorizationPendingError struct{} -func (e *OidcAuthorizationPendingError) Error() string { - return "authorization is still pending" -} -func (e *OidcAuthorizationPendingError) HttpStatusCode() int { - return http.StatusBadRequest -} +func (e OidcAuthorizationPendingError) Error() string { return "authorization is still pending" } +func (e OidcAuthorizationPendingError) HttpStatusCode() int { return http.StatusBadRequest } type ReauthenticationRequiredError struct{} -func (e *ReauthenticationRequiredError) Error() string { - return "reauthentication required" -} -func (e *ReauthenticationRequiredError) HttpStatusCode() int { - return http.StatusUnauthorized -} +func (e ReauthenticationRequiredError) Error() string { return "reauthentication required" } +func (e ReauthenticationRequiredError) HttpStatusCode() int { return http.StatusUnauthorized } type OpenSignupDisabledError struct{} -func (e *OpenSignupDisabledError) Error() string { - return "Open user signup is not enabled" -} +func (e OpenSignupDisabledError) Error() string { return "Open user signup is not enabled" } -func (e *OpenSignupDisabledError) HttpStatusCode() int { - return http.StatusForbidden -} +func (e OpenSignupDisabledError) HttpStatusCode() int { return http.StatusForbidden } type ClientIdAlreadyExistsError struct{} -func (e *ClientIdAlreadyExistsError) Error() string { - return "Client ID already in use" -} +func (e ClientIdAlreadyExistsError) Error() string { return "Client ID already in use" } -func (e *ClientIdAlreadyExistsError) HttpStatusCode() int { - return http.StatusBadRequest -} +func (e ClientIdAlreadyExistsError) HttpStatusCode() int { return http.StatusBadRequest } type UserEmailNotSetError struct{} -func (e *UserEmailNotSetError) Error() string { - return "The user does not have an email address set" -} +func (e UserEmailNotSetError) Error() string { return "The user does not have an email address set" } -func (e *UserEmailNotSetError) HttpStatusCode() int { - return http.StatusBadRequest -} +func (e UserEmailNotSetError) HttpStatusCode() int { return http.StatusBadRequest } type ImageNotFoundError struct{} -func (e *ImageNotFoundError) Error() string { - return "Image not found" -} +func (e ImageNotFoundError) Error() string { return "Image not found" } -func (e *ImageNotFoundError) HttpStatusCode() int { - return http.StatusNotFound -} +func (e ImageNotFoundError) HttpStatusCode() int { return http.StatusNotFound } type InvalidEmailVerificationTokenError struct{} -func (e *InvalidEmailVerificationTokenError) Error() string { - return "Invalid email verification token" -} +func (e InvalidEmailVerificationTokenError) Error() string { return "Invalid email verification token" } -func (e *InvalidEmailVerificationTokenError) HttpStatusCode() int { - return http.StatusBadRequest -} +func (e InvalidEmailVerificationTokenError) HttpStatusCode() int { return http.StatusBadRequest } // OIDC prompt parameter errors - used for redirect error responses type OidcLoginRequiredError struct{} -func (e *OidcLoginRequiredError) Error() string { return "login_required" } -func (e *OidcLoginRequiredError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcLoginRequiredError) Error() string { return "login_required" } +func (e OidcLoginRequiredError) HttpStatusCode() int { return http.StatusBadRequest } type OidcConsentRequiredError struct{} -func (e *OidcConsentRequiredError) Error() string { return "consent_required" } -func (e *OidcConsentRequiredError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcConsentRequiredError) Error() string { return "consent_required" } +func (e OidcConsentRequiredError) HttpStatusCode() int { return http.StatusBadRequest } type OidcInteractionRequiredError struct{} -func (e *OidcInteractionRequiredError) Error() string { return "interaction_required" } -func (e *OidcInteractionRequiredError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcInteractionRequiredError) Error() string { return "interaction_required" } +func (e OidcInteractionRequiredError) HttpStatusCode() int { return http.StatusBadRequest } + +type OidcInvalidRequestError struct{ description string } + +func NewOidcInvalidRequestError(description string) *OidcInvalidRequestError { + return &OidcInvalidRequestError{description: description} +} + +func (e OidcInvalidRequestError) Error() string { return "invalid_request" } +func (e OidcInvalidRequestError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcInvalidRequestError) Description() string { return e.description } type OidcAccountSelectionRequiredError struct{} -func (e *OidcAccountSelectionRequiredError) Error() string { - return "account_selection_required" -} -func (e *OidcAccountSelectionRequiredError) HttpStatusCode() int { return http.StatusBadRequest } +func (e OidcAccountSelectionRequiredError) Error() string { return "account_selection_required" } +func (e OidcAccountSelectionRequiredError) HttpStatusCode() int { return http.StatusBadRequest } diff --git a/backend/internal/controller/well_known_controller.go b/backend/internal/controller/well_known_controller.go index 03ac2f56..2c9a9542 100644 --- a/backend/internal/controller/well_known_controller.go +++ b/backend/internal/controller/well_known_controller.go @@ -91,7 +91,7 @@ func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) { "id_token_signing_alg_values_supported": []string{alg.String()}, "authorization_response_iss_parameter_supported": true, "code_challenge_methods_supported": []string{"plain", "S256"}, - "prompt_values_supported": []string{"none", "login", "consent"}, + "prompt_values_supported": []string{"none", "login", "consent", "select_account"}, "token_endpoint_auth_methods_supported": []string{"client_secret_basic", "client_secret_post", "none"}, } return json.Marshal(config) diff --git a/backend/internal/middleware/error_handler.go b/backend/internal/middleware/error_handler.go index c565f403..7df172dc 100644 --- a/backend/internal/middleware/error_handler.go +++ b/backend/internal/middleware/error_handler.go @@ -23,7 +23,6 @@ func (m *ErrorHandlerMiddleware) Add() gin.HandlerFunc { return func(c *gin.Context) { c.Next() for _, err := range c.Errors { - // Check for record not found errors if errors.Is(err, gorm.ErrRecordNotFound) { errorResponse(c, http.StatusNotFound, "Record not found") @@ -39,30 +38,56 @@ func (m *ErrorHandlerMiddleware) Add() gin.HandlerFunc { } // Check for slice validation errors - var sliceValidationErrors binding.SliceValidationError - if errors.As(err, &sliceValidationErrors) { - if errors.As(sliceValidationErrors[0], &validationErrors) { + svErr, ok := errors.AsType[binding.SliceValidationError](err) + if ok { + if errors.As(svErr[0], &validationErrors) { message := handleValidationError(validationErrors) errorResponse(c, http.StatusBadRequest, message) return } } - var appErr common.AppError - if errors.As(err, &appErr) { + // AppError with description + appDescErr, ok := errors.AsType[common.AppErrorDescription](err) + if ok { + errorResponseWithDescription(c, appDescErr.HttpStatusCode(), appDescErr.Error(), appDescErr.Description()) + return + } + + // AppError (without description) + appErr, ok := errors.AsType[common.AppError](err) + if ok { errorResponse(c, appErr.HttpStatusCode(), appErr.Error()) return } - c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong"}) + c.JSON(http.StatusInternalServerError, errorResponseBody{ + Error: "Something went wrong", + }) } } } +type errorResponseBody struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description,omitempty"` +} + func errorResponse(c *gin.Context, statusCode int, message string) { // Capitalize the first letter of the message message = strings.ToUpper(message[:1]) + message[1:] - c.JSON(statusCode, gin.H{"error": message}) + c.JSON(statusCode, errorResponseBody{ + Error: message, + }) +} + +func errorResponseWithDescription(c *gin.Context, statusCode int, message string, description string) { + // Capitalize the first letter of the message + message = strings.ToUpper(message[:1]) + message[1:] + c.JSON(statusCode, errorResponseBody{ + Error: message, + ErrorDescription: description, + }) } func handleValidationError(validationErrors validator.ValidationErrors) string { diff --git a/backend/internal/service/oidc_service.go b/backend/internal/service/oidc_service.go index 845c24c8..e493a5b9 100644 --- a/backend/internal/service/oidc_service.go +++ b/backend/internal/service/oidc_service.go @@ -151,22 +151,20 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie // Parse prompt parameter (space-delimited list per OIDC spec) promptValues := parsePromptParameter(input.Prompt) - hasPromptNone := contains(promptValues, "none") - hasPromptLogin := contains(promptValues, "login") - hasPromptConsent := contains(promptValues, "consent") - hasPromptSelectAccount := contains(promptValues, "select_account") + hasPromptNone := slices.Contains(promptValues, "none") + hasPromptLogin := slices.Contains(promptValues, "login") + hasPromptConsent := slices.Contains(promptValues, "consent") + hasPromptSelectAccount := slices.Contains(promptValues, "select_account") // Validate prompt parameter conflicts early. // Per OIDC Core ยง3.1.2.6, prompt=none must not be combined with any // value that requires user interaction. if hasPromptNone && (hasPromptConsent || hasPromptLogin || hasPromptSelectAccount) { - return "", "", &common.OidcInteractionRequiredError{} + return "", "", common.NewOidcInvalidRequestError("prompt type 'none' cannot be combined with others") } - // Handle prompt=select_account early (not supported) - if hasPromptSelectAccount { - return "", "", &common.OidcInteractionRequiredError{} - } + // prompt=select_account is handled entirely in the UI + // Pocket ID holds one session per browser, so the frontend renders the current user as the sole selectable account and then calls Authorize normally. // If prompt=login is specified or the client requires reauthentication, check the reauthentication token if hasPromptLogin || client.RequiresReauthentication { @@ -219,14 +217,24 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie // Log the authorization event if hasAlreadyAuthorizedClient { - s.auditLogService.Create(ctx, model.AuditLogEventClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name}, tx) + s.auditLogService.Create( + ctx, model.AuditLogEventClientAuthorization, + ipAddress, userAgent, userID, + model.AuditLogData{"clientName": client.Name}, + tx, + ) } else { - s.auditLogService.Create(ctx, model.AuditLogEventNewClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name}, tx) + s.auditLogService.Create( + ctx, model.AuditLogEventNewClientAuthorization, + ipAddress, userAgent, userID, + model.AuditLogData{"clientName": client.Name}, + tx, + ) } err = tx.Commit().Error if err != nil { - return "", "", err + return "", "", fmt.Errorf("failed to commit transaction: %w", err) } return code, callbackURL, nil @@ -2219,13 +2227,3 @@ func parsePromptParameter(prompt string) []string { } return strings.Fields(prompt) } - -// contains checks if a string slice contains a specific value -func contains(slice []string, value string) bool { - for _, item := range slice { - if item == value { - return true - } - } - return false -} diff --git a/backend/internal/service/oidc_service_test.go b/backend/internal/service/oidc_service_test.go index 6d514578..40c7fcc7 100644 --- a/backend/internal/service/oidc_service_test.go +++ b/backend/internal/service/oidc_service_test.go @@ -10,6 +10,7 @@ import ( "encoding/json" "io" "net/http" + "slices" "strconv" "strings" "testing" @@ -1238,30 +1239,13 @@ func TestPromptParameterConflicts(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { values := parsePromptParameter(tt.prompt) - hasNone := contains(values, "none") - hasConsent := contains(values, "consent") - hasLogin := contains(values, "login") - hasSelectAccount := contains(values, "select_account") + hasNone := slices.Contains(values, "none") + hasConsent := slices.Contains(values, "consent") + hasLogin := slices.Contains(values, "login") + hasSelectAccount := slices.Contains(values, "select_account") conflict := hasNone && (hasConsent || hasLogin || hasSelectAccount) assert.Equal(t, tt.expectConflict, conflict) }) } } - -func TestContains(t *testing.T) { - t.Run("finds value in slice", func(t *testing.T) { - slice := []string{"none", "login", "consent"} - assert.True(t, contains(slice, "login")) - }) - - t.Run("returns false for missing value", func(t *testing.T) { - slice := []string{"none", "login"} - assert.False(t, contains(slice, "consent")) - }) - - t.Run("returns false for empty slice", func(t *testing.T) { - slice := []string{} - assert.False(t, contains(slice, "none")) - }) -} diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 1d958e12..43f249ad 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -53,6 +53,8 @@ "webauthn_not_supported_by_browser": "Passkeys are not supported by this browser. Please use an alternative sign in method.", "critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.", "sign_in_to": "Sign in to {name}", + "account_selection_signin_confirmation": "Do you want to use the following account to continue to {name}?", + "use_a_different_account": "Use a different account", "client_not_found": "Client not found", "client_wants_to_access_the_following_information": "{client} wants to access the following information:", "do_you_want_to_sign_in_to_client_with_your_app_name_account": "Do you want to sign in to {client} with your {appName} account?", diff --git a/frontend/src/lib/components/header/header.svelte b/frontend/src/lib/components/header/header.svelte index a07f426d..e3b0625a 100644 --- a/frontend/src/lib/components/header/header.svelte +++ b/frontend/src/lib/components/header/header.svelte @@ -22,7 +22,7 @@