Files
nhost/services/auth/go/controller/errors.go

568 lines
15 KiB
Go

package controller
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"github.com/nhost/nhost/services/auth/go/api"
)
type APIError struct {
t api.ErrorResponseError
}
func (e *APIError) Error() string {
return fmt.Sprintf("API error: %s", e.t)
}
var (
ErrJWTConfiguration = errors.New("jwt-configuration")
ErrAnonymousUsersDisabled = &APIError{api.DisabledEndpoint}
ErrUserEmailNotFound = &APIError{api.InvalidEmailPassword}
ErrUserPhoneNumberNotFound = &APIError{api.InvalidRequest}
ErrInvalidOTP = &APIError{api.InvalidRequest}
ErrUserProviderNotFound = &APIError{api.InvalidRequest}
ErrSecurityKeyNotFound = &APIError{api.InvalidRequest}
ErrUserProviderAlreadyLinked = &APIError{api.InvalidRequest}
ErrEmailAlreadyInUse = &APIError{api.EmailAlreadyInUse}
ErrForbiddenAnonymous = &APIError{api.ForbiddenAnonymous}
ErrInternalServerError = &APIError{api.InternalServerError}
ErrInvalidEmailPassword = &APIError{api.InvalidEmailPassword}
ErrPasswordTooShort = &APIError{api.PasswordTooShort}
ErrPasswordInHibpDatabase = &APIError{api.PasswordInHibpDatabase}
ErrRoleNotAllowed = &APIError{api.RoleNotAllowed}
ErrDefaultRoleMustBeInAllowedRoles = &APIError{api.DefaultRoleMustBeInAllowedRoles}
ErrRedirecToNotAllowed = &APIError{api.RedirectToNotAllowed}
ErrDisabledUser = &APIError{api.DisabledUser}
ErrUnverifiedUser = &APIError{api.UnverifiedUser}
ErrUserNotAnonymous = &APIError{api.UserNotAnonymous}
ErrInvalidPat = &APIError{api.InvalidPat}
ErrInvalidTicket = &APIError{api.InvalidTicket}
ErrInvalidRequest = &APIError{api.InvalidRequest}
ErrSignupDisabled = &APIError{api.SignupDisabled}
ErrUnauthenticatedUser = &APIError{api.InvalidRequest}
ErrDisabledEndpoint = &APIError{api.DisabledEndpoint}
ErrEmailAlreadyVerified = &APIError{api.EmailAlreadyVerified}
ErrInvalidRefreshToken = &APIError{api.InvalidRefreshToken}
ErrDisabledMfaTotp = &APIError{api.DisabledMfaTotp}
ErrNoTotpSecret = &APIError{api.NoTotpSecret}
ErrInvalidTotp = &APIError{api.InvalidTotp}
ErrMfaTypeNotFound = &APIError{api.MfaTypeNotFound}
ErrTotpAlreadyActive = &APIError{api.TotpAlreadyActive}
ErrInvalidState = &APIError{api.InvalidState}
ErrOauthTokenExchangeFailed = &APIError{api.OauthTokenEchangeFailed}
ErrOauthProfileFetchFailed = &APIError{api.OauthProfileFetchFailed}
ErrOauthProviderError = &APIError{api.OauthProviderError}
ErrCannotSendSMS = &APIError{api.CannotSendSms}
)
func logError(err error) slog.Attr {
return slog.String("error", err.Error())
}
type ErrorResponse api.ErrorResponse
func (response ErrorResponse) visit(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.Status)
return json.NewEncoder(w).Encode(response) //nolint:wrapcheck
}
func (response ErrorResponse) VisitSignUpEmailPasswordResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignInAnonymousResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifySignInMfaTotpResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifyChangeUserMfaResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitChangeUserMfaResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignInEmailPasswordResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignInPasswordlessEmailResponse(
w http.ResponseWriter,
) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignInPATResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignInProviderResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignInProviderCallbackGetResponse(
w http.ResponseWriter,
) error {
return response.visit(w)
}
func (response ErrorResponse) VisitChangeUserEmailResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitChangeUserPasswordResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSendPasswordResetEmailResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSendVerificationEmailResponse(
w http.ResponseWriter,
) error {
return response.visit(w)
}
func (response ErrorResponse) VisitCreatePATResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitDeanonymizeUserResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignUpWebauthnResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignInWebauthnResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifySignInWebauthnResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifySignUpWebauthnResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitRefreshProviderTokenResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitRefreshTokenResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignInIdTokenResponse( //nolint:revive
w http.ResponseWriter,
) error {
return response.visit(w)
}
func (response ErrorResponse) VisitLinkIdTokenResponse( //nolint:revive
w http.ResponseWriter,
) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifyTicketResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignInOTPEmailResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifySignInOTPEmailResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitElevateWebauthnResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifyElevateWebauthnResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitAddSecurityKeyResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifyAddSecurityKeyResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignInPasswordlessSmsResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitSignOutResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifyTokenResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitGetUserResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitGetProviderTokensResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorResponse) VisitVerifySignInPasswordlessSmsResponse(
w http.ResponseWriter,
) error {
return response.visit(w)
}
func isSensitive(err api.ErrorResponseError) bool {
switch err {
case
api.DisabledUser,
api.EmailAlreadyInUse,
api.EmailAlreadyVerified,
api.ForbiddenAnonymous,
api.InvalidEmailPassword,
api.InvalidPat,
api.RoleNotAllowed,
api.SignupDisabled,
api.UnverifiedUser,
api.InvalidRefreshToken,
api.InvalidTicket,
api.DisabledMfaTotp,
api.InvalidTotp,
api.InvalidOtp,
api.NoTotpSecret:
return true
case
api.DefaultRoleMustBeInAllowedRoles,
api.DisabledEndpoint,
api.InternalServerError,
api.InvalidRequest,
api.LocaleNotAllowed,
api.PasswordTooShort,
api.PasswordInHibpDatabase,
api.RedirectToNotAllowed,
api.UserNotAnonymous,
api.MfaTypeNotFound,
api.TotpAlreadyActive,
api.InvalidState,
api.OauthTokenEchangeFailed,
api.OauthProfileFetchFailed,
api.CannotSendSms,
api.OauthProviderError:
return false
}
return false
}
func (ctrl *Controller) getError(err *APIError) ErrorResponse { //nolint:gocyclo,cyclop,funlen
invalidRequest := ErrorResponse{
Status: http.StatusBadRequest,
Error: api.InvalidRequest,
Message: "The request payload is incorrect",
}
if ctrl.config.ConcealErrors && isSensitive(err.t) {
return invalidRequest
}
switch err.t {
case api.DefaultRoleMustBeInAllowedRoles:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Default role must be in allowed roles",
}
case api.DisabledUser:
return ErrorResponse{
Status: http.StatusUnauthorized,
Error: err.t,
Message: "User is disabled",
}
case api.DisabledEndpoint:
return ErrorResponse{
Status: http.StatusConflict,
Error: err.t,
Message: "This endpoint is disabled",
}
case api.EmailAlreadyInUse:
return ErrorResponse{
Status: http.StatusConflict,
Error: err.t,
Message: "Email already in use",
}
case api.EmailAlreadyVerified:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "User's email is already verified",
}
case api.ForbiddenAnonymous:
return ErrorResponse{
Status: http.StatusForbidden,
Error: err.t,
Message: "Forbidden, user is anonymous.",
}
case api.InternalServerError:
return ErrorResponse{
Status: http.StatusInternalServerError,
Error: err.t,
Message: "Internal server error",
}
case api.InvalidEmailPassword:
return ErrorResponse{
Status: http.StatusUnauthorized,
Error: err.t,
Message: "Incorrect email or password",
}
case api.InvalidPat:
return ErrorResponse{
Status: http.StatusUnauthorized,
Error: err.t,
Message: "Invalid or expired personal access token",
}
case api.InvalidRequest:
case api.LocaleNotAllowed:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Locale not allowed",
}
case api.PasswordInHibpDatabase:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Password is in HIBP database",
}
case api.PasswordTooShort:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Password is too short",
}
case api.RedirectToNotAllowed:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "The value of \"options.redirectTo\" is not allowed.",
}
case api.RoleNotAllowed:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Role not allowed",
}
case api.SignupDisabled:
return ErrorResponse{
Status: http.StatusForbidden,
Error: err.t,
Message: "Sign up is disabled.",
}
case api.UnverifiedUser:
return ErrorResponse{
Status: http.StatusUnauthorized,
Error: err.t,
Message: "User is not verified.",
}
case api.UserNotAnonymous:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Logged in user is not anonymous",
}
case api.InvalidRefreshToken:
return ErrorResponse{
Status: http.StatusUnauthorized,
Error: err.t,
Message: "Invalid or expired refresh token",
}
case api.InvalidTicket:
return ErrorResponse{
Status: http.StatusUnauthorized,
Error: err.t,
Message: "Invalid ticket",
}
case api.DisabledMfaTotp:
return ErrorResponse{
Status: http.StatusUnauthorized,
Error: err.t,
Message: "User does not have TOTP MFA enabled",
}
case api.NoTotpSecret:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "User does not have a TOTP secret",
}
case api.InvalidTotp:
return ErrorResponse{
Status: http.StatusUnauthorized,
Error: err.t,
Message: "Invalid TOTP code",
}
case api.MfaTypeNotFound:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "MFA type not found",
}
case api.TotpAlreadyActive:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "TOTP MFA is already active",
}
case api.InvalidState:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Invalid state",
}
case api.OauthTokenEchangeFailed:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Failed to exchange token",
}
case api.OauthProfileFetchFailed:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Failed to get user profile",
}
case api.OauthProviderError:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Provider returned an error",
}
case api.CannotSendSms:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Cannot send SMS, check your phone number is correct",
}
case api.InvalidOtp:
return ErrorResponse{
Status: http.StatusBadRequest,
Error: err.t,
Message: "Invalid or expired OTP",
}
}
return invalidRequest
}
func (ctrl *Controller) sendError(
err *APIError,
) ErrorResponse {
return ctrl.getError(err)
}
type ErrorRedirectResponse struct {
Headers struct {
Location string
}
}
func (response ErrorRedirectResponse) visit(w http.ResponseWriter) error {
w.Header().Set("Location", response.Headers.Location)
w.WriteHeader(http.StatusFound)
return nil
}
func (response ErrorRedirectResponse) VisitVerifyTicketResponse(w http.ResponseWriter) error {
return response.visit(w)
}
func (response ErrorRedirectResponse) VisitSignInProviderResponse(
w http.ResponseWriter,
) error {
return response.visit(w)
}
func (response ErrorRedirectResponse) VisitSignInProviderCallbackGetResponse(
w http.ResponseWriter,
) error {
return response.visit(w)
}
func (response ErrorRedirectResponse) VisitSignInProviderCallbackPostResponse(
w http.ResponseWriter,
) error {
return response.visit(w)
}
func (ctrl *Controller) sendRedirectError(
redirectURL *url.URL,
err *APIError,
) ErrorRedirectResponse {
errResponse := ctrl.getError(err)
redirectURL = appendURLValues(redirectURL, map[string]string{
"error": string(errResponse.Error),
"errorDescription": errResponse.Message,
})
return ErrorRedirectResponse{
Headers: struct {
Location string
}{
Location: redirectURL.String(),
},
}
}
func (ctrl *Controller) respondWithError(err *APIError) ErrorResponse {
return ctrl.sendError(err)
}
func sqlErrIsDuplicatedEmail(ctx context.Context, err error, logger *slog.Logger) *APIError {
if err == nil {
return nil
}
if strings.Contains(err.Error(), "SQLSTATE 23505") &&
strings.Contains(err.Error(), "\"users_email_key\"") {
logger.ErrorContext(ctx, "email already in use", logError(err))
return ErrEmailAlreadyInUse
}
logger.ErrorContext(ctx, "error inserting user", logError(err))
return &APIError{api.InternalServerError}
}
func sqlIsDuplcateError(err error, fkey string) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "SQLSTATE 23505") &&
strings.Contains(err.Error(), fkey)
}