package api

import (
	"bytes"
	"crypto/subtle"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/aaronarduino/goqrsvg"
	svg "github.com/ajstarks/svgo"
	"github.com/boombuler/barcode/qr"
	wbnprotocol "github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
	"github.com/gofrs/uuid"
	"github.com/pquerna/otp"
	"github.com/pquerna/otp/totp"
	"github.com/supabase/auth/internal/api/sms_provider"
	"github.com/supabase/auth/internal/conf"
	"github.com/supabase/auth/internal/crypto"
	"github.com/supabase/auth/internal/hooks"
	"github.com/supabase/auth/internal/metering"
	"github.com/supabase/auth/internal/models"
	"github.com/supabase/auth/internal/storage"
	"github.com/supabase/auth/internal/utilities"
)

const DefaultQRSize = 3

type EnrollFactorParams struct {
	FriendlyName string `json:"friendly_name"`
	FactorType   string `json:"factor_type"`
	Issuer       string `json:"issuer"`
	Phone        string `json:"phone"`
}

type TOTPObject struct {
	QRCode string `json:"qr_code,omitempty"`
	Secret string `json:"secret,omitempty"`
	URI    string `json:"uri,omitempty"`
}

type EnrollFactorResponse struct {
	ID           uuid.UUID   `json:"id"`
	Type         string      `json:"type"`
	FriendlyName string      `json:"friendly_name"`
	TOTP         *TOTPObject `json:"totp,omitempty"`
	Phone        string      `json:"phone,omitempty"`
}

type ChallengeFactorParams struct {
	Channel  string          `json:"channel"`
	WebAuthn *WebAuthnParams `json:"web_authn,omitempty"`
}

type VerifyFactorParams struct {
	ChallengeID uuid.UUID       `json:"challenge_id"`
	Code        string          `json:"code"`
	WebAuthn    *WebAuthnParams `json:"web_authn,omitempty"`
}

type ChallengeFactorResponse struct {
	ID                        uuid.UUID                        `json:"id"`
	Type                      string                           `json:"type"`
	ExpiresAt                 int64                            `json:"expires_at,omitempty"`
	CredentialRequestOptions  *wbnprotocol.CredentialAssertion `json:"credential_request_options,omitempty"`
	CredentialCreationOptions *wbnprotocol.CredentialCreation  `json:"credential_creation_options,omitempty"`
}

type UnenrollFactorResponse struct {
	ID uuid.UUID `json:"id"`
}

type WebAuthnParams struct {
	RPID string `json:"rp_id,omitempty"`
	// Can encode multiple origins as comma separated values like: "origin1,origin2"
	RPOrigins         string          `json:"rp_origins,omitempty"`
	AssertionResponse json.RawMessage `json:"assertion_response,omitempty"`
	CreationResponse  json.RawMessage `json:"creation_response,omitempty"`
}

func (w *WebAuthnParams) GetRPOrigins() []string {
	if w.RPOrigins == "" {
		return nil
	}
	return strings.Split(w.RPOrigins, ",")
}

func (w *WebAuthnParams) ToConfig() (*webauthn.WebAuthn, error) {
	if w.RPID == "" {
		return nil, fmt.Errorf("webAuthn RP ID cannot be empty")
	}

	origins := w.GetRPOrigins()
	if len(origins) == 0 {
		return nil, fmt.Errorf("webAuthn RP Origins cannot be empty")
	}

	var validOrigins []string
	var invalidOrigins []string

	for _, origin := range origins {
		parsedURL, err := url.Parse(origin)
		if err != nil || (parsedURL.Scheme != "https" && !(parsedURL.Scheme == "http" && parsedURL.Hostname() == "localhost")) || parsedURL.Host == "" {
			invalidOrigins = append(invalidOrigins, origin)
		} else {
			validOrigins = append(validOrigins, origin)
		}
	}

	if len(invalidOrigins) > 0 {
		return nil, fmt.Errorf("invalid RP origins: %s", strings.Join(invalidOrigins, ", "))
	}

	wconfig := &webauthn.Config{
		// DisplayName is optional in spec but required to be non-empty in libary, we use the RPID as a placeholder.
		RPDisplayName: w.RPID,
		RPID:          w.RPID,
		RPOrigins:     validOrigins,
	}

	return webauthn.New(wconfig)
}

const (
	QRCodeGenerationErrorMessage = "Error generating QR Code"
)

func validateFactors(db *storage.Connection, user *models.User, newFactorName string, config *conf.GlobalConfiguration, session *models.Session) error {
	if err := models.DeleteExpiredFactors(db, config.MFA.FactorExpiryDuration); err != nil {
		return err
	}
	if err := db.Load(user, "Factors"); err != nil {
		return err
	}
	factorCount := len(user.Factors)
	numVerifiedFactors := 0

	for _, factor := range user.Factors {
		if factor.FriendlyName == newFactorName {
			return unprocessableEntityError(
				ErrorCodeMFAFactorNameConflict,
				fmt.Sprintf("A factor with the friendly name %q for this user already exists", newFactorName),
			)
		}
		if factor.IsVerified() {
			numVerifiedFactors++
		}
	}

	if factorCount >= int(config.MFA.MaxEnrolledFactors) {
		return unprocessableEntityError(ErrorCodeTooManyEnrolledMFAFactors, "Maximum number of verified factors reached, unenroll to continue")
	}

	if numVerifiedFactors >= config.MFA.MaxVerifiedFactors {
		return unprocessableEntityError(ErrorCodeTooManyEnrolledMFAFactors, "Maximum number of verified factors reached, unenroll to continue")
	}

	if numVerifiedFactors > 0 && session != nil && !session.IsAAL2() {
		return forbiddenError(ErrorCodeInsufficientAAL, "AAL2 required to enroll a new factor")
	}

	return nil
}

func (a *API) enrollPhoneFactor(w http.ResponseWriter, r *http.Request, params *EnrollFactorParams) error {
	ctx := r.Context()
	user := getUser(ctx)
	session := getSession(ctx)
	db := a.db.WithContext(ctx)
	if params.Phone == "" {
		return badRequestError(ErrorCodeValidationFailed, "Phone number required to enroll Phone factor")
	}

	phone, err := validatePhone(params.Phone)
	if err != nil {
		return badRequestError(ErrorCodeValidationFailed, "Invalid phone number format (E.164 required)")
	}

	var factorsToDelete []models.Factor
	for _, factor := range user.Factors {
		if factor.IsPhoneFactor() && factor.Phone.String() == phone {
			if factor.IsVerified() {
				return unprocessableEntityError(
					ErrorCodeMFAVerifiedFactorExists,
					"A verified phone factor already exists, unenroll the existing factor to continue",
				)
			} else if factor.IsUnverified() {
				factorsToDelete = append(factorsToDelete, factor)
			}
		}
	}

	if err := db.Destroy(&factorsToDelete); err != nil {
		return internalServerError("Database error deleting unverified phone factors").WithInternalError(err)
	}

	if err := validateFactors(db, user, params.FriendlyName, a.config, session); err != nil {
		return err
	}

	factor := models.NewPhoneFactor(user, phone, params.FriendlyName)
	err = db.Transaction(func(tx *storage.Connection) error {
		if terr := tx.Create(factor); terr != nil {
			return terr
		}
		if terr := models.NewAuditLogEntry(r, tx, user, models.EnrollFactorAction, r.RemoteAddr, map[string]interface{}{
			"factor_id":   factor.ID,
			"factor_type": factor.FactorType,
		}); terr != nil {
			return terr
		}
		return nil
	})
	if err != nil {
		return err
	}
	return sendJSON(w, http.StatusOK, &EnrollFactorResponse{
		ID:           factor.ID,
		Type:         models.Phone,
		FriendlyName: factor.FriendlyName,
		Phone:        params.Phone,
	})
}

func (a *API) enrollWebAuthnFactor(w http.ResponseWriter, r *http.Request, params *EnrollFactorParams) error {
	ctx := r.Context()
	user := getUser(ctx)
	session := getSession(ctx)
	db := a.db.WithContext(ctx)

	if err := validateFactors(db, user, params.FriendlyName, a.config, session); err != nil {
		return err
	}

	factor := models.NewWebAuthnFactor(user, params.FriendlyName)
	err := db.Transaction(func(tx *storage.Connection) error {
		if terr := tx.Create(factor); terr != nil {
			return terr
		}
		if terr := models.NewAuditLogEntry(r, tx, user, models.EnrollFactorAction, r.RemoteAddr, map[string]interface{}{
			"factor_id":   factor.ID,
			"factor_type": factor.FactorType,
		}); terr != nil {
			return terr
		}
		return nil
	})
	if err != nil {
		return err
	}
	return sendJSON(w, http.StatusOK, &EnrollFactorResponse{
		ID:           factor.ID,
		Type:         models.WebAuthn,
		FriendlyName: factor.FriendlyName,
	})
}

func (a *API) enrollTOTPFactor(w http.ResponseWriter, r *http.Request, params *EnrollFactorParams) error {
	ctx := r.Context()
	user := getUser(ctx)
	db := a.db.WithContext(ctx)
	config := a.config
	session := getSession(ctx)
	issuer := ""
	if params.Issuer == "" {
		u, err := url.ParseRequestURI(config.SiteURL)
		if err != nil {
			return internalServerError("site url is improperly formatted")
		}
		issuer = u.Host
	} else {
		issuer = params.Issuer
	}

	if err := validateFactors(db, user, params.FriendlyName, config, session); err != nil {
		return err
	}

	var factor *models.Factor
	var buf bytes.Buffer
	var key *otp.Key
	key, err := totp.Generate(totp.GenerateOpts{
		Issuer:      issuer,
		AccountName: user.GetEmail(),
	})
	if err != nil {
		return internalServerError(QRCodeGenerationErrorMessage).WithInternalError(err)
	}

	svgData := svg.New(&buf)
	qrCode, _ := qr.Encode(key.String(), qr.H, qr.Auto)
	qs := goqrsvg.NewQrSVG(qrCode, DefaultQRSize)
	qs.StartQrSVG(svgData)
	if err = qs.WriteQrSVG(svgData); err != nil {
		return internalServerError(QRCodeGenerationErrorMessage).WithInternalError(err)
	}
	svgData.End()

	factor = models.NewTOTPFactor(user, params.FriendlyName)
	if err := factor.SetSecret(key.Secret(), config.Security.DBEncryption.Encrypt, config.Security.DBEncryption.EncryptionKeyID, config.Security.DBEncryption.EncryptionKey); err != nil {
		return err
	}

	err = db.Transaction(func(tx *storage.Connection) error {
		if terr := tx.Create(factor); terr != nil {
			return terr
		}

		if terr := models.NewAuditLogEntry(r, tx, user, models.EnrollFactorAction, r.RemoteAddr, map[string]interface{}{
			"factor_id": factor.ID,
		}); terr != nil {
			return terr
		}
		return nil
	})
	if err != nil {
		return err
	}
	return sendJSON(w, http.StatusOK, &EnrollFactorResponse{
		ID:           factor.ID,
		Type:         models.TOTP,
		FriendlyName: factor.FriendlyName,
		TOTP: &TOTPObject{
			// See: https://css-tricks.com/probably-dont-base64-svg/
			QRCode: buf.String(),
			Secret: key.Secret(),
			URI:    key.URL(),
		},
	})
}

func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error {
	ctx := r.Context()
	user := getUser(ctx)
	session := getSession(ctx)
	config := a.config

	if session == nil || user == nil {
		return internalServerError("A valid session and a registered user are required to enroll a factor")
	}
	params := &EnrollFactorParams{}
	if err := retrieveRequestParams(r, params); err != nil {
		return err
	}

	switch params.FactorType {
	case models.Phone:
		if !config.MFA.Phone.EnrollEnabled {
			return unprocessableEntityError(ErrorCodeMFAPhoneEnrollDisabled, "MFA enroll is disabled for Phone")
		}
		return a.enrollPhoneFactor(w, r, params)
	case models.TOTP:
		if !config.MFA.TOTP.EnrollEnabled {
			return unprocessableEntityError(ErrorCodeMFATOTPEnrollDisabled, "MFA enroll is disabled for TOTP")
		}
		return a.enrollTOTPFactor(w, r, params)
	case models.WebAuthn:
		if !config.MFA.WebAuthn.EnrollEnabled {
			return unprocessableEntityError(ErrorCodeMFAWebAuthnEnrollDisabled, "MFA enroll is disabled for WebAuthn")
		}
		return a.enrollWebAuthnFactor(w, r, params)
	default:
		return badRequestError(ErrorCodeValidationFailed, "factor_type needs to be totp, phone, or webauthn")
	}

}

func (a *API) challengePhoneFactor(w http.ResponseWriter, r *http.Request) error {
	ctx := r.Context()
	config := a.config
	db := a.db.WithContext(ctx)
	user := getUser(ctx)
	factor := getFactor(ctx)
	ipAddress := utilities.GetIPAddress(r)
	params := &ChallengeFactorParams{}
	if err := retrieveRequestParams(r, params); err != nil {
		return err
	}
	channel := params.Channel
	if channel == "" {
		channel = sms_provider.SMSProvider
	}
	if !sms_provider.IsValidMessageChannel(channel, config) {
		return badRequestError(ErrorCodeValidationFailed, InvalidChannelError)
	}

	if factor.IsPhoneFactor() && factor.LastChallengedAt != nil {
		if !factor.LastChallengedAt.Add(config.MFA.Phone.MaxFrequency).Before(time.Now()) {
			return tooManyRequestsError(ErrorCodeOverSMSSendRateLimit, generateFrequencyLimitErrorMessage(factor.LastChallengedAt, config.MFA.Phone.MaxFrequency))
		}
	}

	otp := crypto.GenerateOtp(config.MFA.Phone.OtpLength)

	challenge, err := factor.CreatePhoneChallenge(ipAddress, otp, config.Security.DBEncryption.Encrypt, config.Security.DBEncryption.EncryptionKeyID, config.Security.DBEncryption.EncryptionKey)
	if err != nil {
		return internalServerError("error creating SMS Challenge")
	}

	message, err := generateSMSFromTemplate(config.MFA.Phone.SMSTemplate, otp)
	if err != nil {
		return internalServerError("error generating sms template").WithInternalError(err)
	}

	if config.Hook.SendSMS.Enabled {
		input := hooks.SendSMSInput{
			User: user,
			SMS: hooks.SMS{
				OTP:     otp,
				SMSType: "mfa",
			},
		}
		output := hooks.SendSMSOutput{}
		err := a.invokeHook(a.db, r, &input, &output)
		if err != nil {
			return internalServerError("error invoking hook")
		}
	} else {
		smsProvider, err := sms_provider.GetSmsProvider(*config)
		if err != nil {
			return internalServerError("Failed to get SMS provider").WithInternalError(err)
		}
		// We omit messageID for now, can consider reinstating if there are requests.
		if _, err = smsProvider.SendMessage(factor.Phone.String(), message, channel, otp); err != nil {
			return internalServerError("error sending message").WithInternalError(err)
		}
	}
	if err := db.Transaction(func(tx *storage.Connection) error {
		if terr := factor.WriteChallengeToDatabase(tx, challenge); terr != nil {
			return terr
		}

		if terr := models.NewAuditLogEntry(r, tx, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{
			"factor_id":     factor.ID,
			"factor_status": factor.Status,
		}); terr != nil {
			return terr
		}
		return nil
	}); err != nil {
		return err
	}
	return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{
		ID:        challenge.ID,
		Type:      factor.FactorType,
		ExpiresAt: challenge.GetExpiryTime(config.MFA.ChallengeExpiryDuration).Unix(),
	})
}

func (a *API) challengeTOTPFactor(w http.ResponseWriter, r *http.Request) error {
	ctx := r.Context()
	config := a.config
	db := a.db.WithContext(ctx)

	user := getUser(ctx)
	factor := getFactor(ctx)
	ipAddress := utilities.GetIPAddress(r)

	challenge := factor.CreateChallenge(ipAddress)

	if err := db.Transaction(func(tx *storage.Connection) error {
		if terr := factor.WriteChallengeToDatabase(tx, challenge); terr != nil {
			return terr
		}
		if terr := models.NewAuditLogEntry(r, tx, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{
			"factor_id":     factor.ID,
			"factor_status": factor.Status,
		}); terr != nil {
			return terr
		}
		return nil
	}); err != nil {
		return err
	}

	return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{
		ID:        challenge.ID,
		Type:      factor.FactorType,
		ExpiresAt: challenge.GetExpiryTime(config.MFA.ChallengeExpiryDuration).Unix(),
	})
}

func (a *API) challengeWebAuthnFactor(w http.ResponseWriter, r *http.Request) error {
	ctx := r.Context()
	db := a.db.WithContext(ctx)
	config := a.config

	user := getUser(ctx)
	factor := getFactor(ctx)
	ipAddress := utilities.GetIPAddress(r)

	params := &ChallengeFactorParams{}
	if err := retrieveRequestParams(r, params); err != nil {
		return err
	}
	if params.WebAuthn == nil {
		return badRequestError(ErrorCodeValidationFailed, "web_authn config required")
	}
	webAuthn, err := params.WebAuthn.ToConfig()
	if err != nil {
		return err
	}
	var response *ChallengeFactorResponse
	var ws *models.WebAuthnSessionData
	var challenge *models.Challenge
	if factor.IsUnverified() {
		options, session, err := webAuthn.BeginRegistration(user)
		if err != nil {
			return internalServerError("Failed to generate WebAuthn registration data").WithInternalError(err)
		}
		ws = &models.WebAuthnSessionData{
			SessionData: session,
		}
		challenge = ws.ToChallenge(factor.ID, ipAddress)

		response = &ChallengeFactorResponse{
			CredentialCreationOptions: options,
			Type:                      factor.FactorType,
			ID:                        challenge.ID,
		}

	} else if factor.IsVerified() {
		options, session, err := webAuthn.BeginLogin(user)
		if err != nil {
			return err
		}
		ws = &models.WebAuthnSessionData{
			SessionData: session,
		}
		challenge = ws.ToChallenge(factor.ID, ipAddress)
		response = &ChallengeFactorResponse{
			CredentialRequestOptions: options,
			Type:                     factor.FactorType,
			ID:                       challenge.ID,
		}

	}

	if err := factor.WriteChallengeToDatabase(db, challenge); err != nil {
		return err
	}
	response.ExpiresAt = challenge.GetExpiryTime(config.MFA.ChallengeExpiryDuration).Unix()

	return sendJSON(w, http.StatusOK, response)

}

func (a *API) validateChallenge(r *http.Request, db *storage.Connection, factor *models.Factor, challengeID uuid.UUID) (*models.Challenge, error) {
	config := a.config
	currentIP := utilities.GetIPAddress(r)

	challenge, err := factor.FindChallengeByID(db, challengeID)
	if err != nil {
		if models.IsNotFoundError(err) {
			return nil, unprocessableEntityError(ErrorCodeMFAFactorNotFound, "MFA factor with the provided challenge ID not found")
		}
		return nil, internalServerError("Database error finding Challenge").WithInternalError(err)
	}

	if challenge.VerifiedAt != nil || challenge.IPAddress != currentIP {
		return nil, unprocessableEntityError(ErrorCodeMFAIPAddressMismatch, "Challenge and verify IP addresses mismatch.")
	}

	if challenge.HasExpired(config.MFA.ChallengeExpiryDuration) {
		if err := db.Destroy(challenge); err != nil {
			return nil, internalServerError("Database error deleting challenge").WithInternalError(err)
		}
		return nil, unprocessableEntityError(ErrorCodeMFAChallengeExpired, "MFA challenge %v has expired, verify against another challenge or create a new challenge.", challenge.ID)
	}

	return challenge, nil
}

func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error {
	ctx := r.Context()
	config := a.config
	factor := getFactor(ctx)

	switch factor.FactorType {
	case models.Phone:
		if !config.MFA.Phone.VerifyEnabled {
			return unprocessableEntityError(ErrorCodeMFAPhoneVerifyDisabled, "MFA verification is disabled for Phone")
		}
		return a.challengePhoneFactor(w, r)

	case models.TOTP:
		if !config.MFA.TOTP.VerifyEnabled {
			return unprocessableEntityError(ErrorCodeMFATOTPVerifyDisabled, "MFA verification is disabled for TOTP")
		}
		return a.challengeTOTPFactor(w, r)
	case models.WebAuthn:
		if !config.MFA.WebAuthn.VerifyEnabled {
			return unprocessableEntityError(ErrorCodeMFAWebAuthnVerifyDisabled, "MFA verification is disabled for WebAuthn")
		}
		return a.challengeWebAuthnFactor(w, r)
	default:
		return badRequestError(ErrorCodeValidationFailed, "factor_type needs to be totp, phone, or webauthn")
	}

}

func (a *API) verifyTOTPFactor(w http.ResponseWriter, r *http.Request, params *VerifyFactorParams) error {
	var err error
	ctx := r.Context()
	user := getUser(ctx)
	factor := getFactor(ctx)
	config := a.config
	db := a.db.WithContext(ctx)

	challenge, err := a.validateChallenge(r, db, factor, params.ChallengeID)
	if err != nil {
		return err
	}

	secret, shouldReEncrypt, err := factor.GetSecret(config.Security.DBEncryption.DecryptionKeys, config.Security.DBEncryption.Encrypt, config.Security.DBEncryption.EncryptionKeyID)
	if err != nil {
		return internalServerError("Database error verifying MFA TOTP secret").WithInternalError(err)
	}

	valid, verr := totp.ValidateCustom(params.Code, secret, time.Now().UTC(), totp.ValidateOpts{
		Period:    30,
		Skew:      1,
		Digits:    otp.DigitsSix,
		Algorithm: otp.AlgorithmSHA1,
	})

	if config.Hook.MFAVerificationAttempt.Enabled {
		input := hooks.MFAVerificationAttemptInput{
			UserID:   user.ID,
			FactorID: factor.ID,
			Valid:    valid,
		}

		output := hooks.MFAVerificationAttemptOutput{}
		err := a.invokeHook(nil, r, &input, &output)
		if err != nil {
			return err
		}

		if output.Decision == hooks.HookRejection {
			if err := models.Logout(db, user.ID); err != nil {
				return err
			}

			if output.Message == "" {
				output.Message = hooks.DefaultMFAHookRejectionMessage
			}

			return forbiddenError(ErrorCodeMFAVerificationRejected, output.Message)
		}
	}
	if !valid {
		if shouldReEncrypt && config.Security.DBEncryption.Encrypt {
			if err := factor.SetSecret(secret, true, config.Security.DBEncryption.EncryptionKeyID, config.Security.DBEncryption.EncryptionKey); err != nil {
				return err
			}

			if err := db.UpdateOnly(factor, "secret"); err != nil {
				return err
			}
		}
		return unprocessableEntityError(ErrorCodeMFAVerificationFailed, "Invalid TOTP code entered").WithInternalError(verr)
	}

	var token *AccessTokenResponse

	err = db.Transaction(func(tx *storage.Connection) error {
		var terr error
		if terr = models.NewAuditLogEntry(r, tx, user, models.VerifyFactorAction, r.RemoteAddr, map[string]interface{}{
			"factor_id":    factor.ID,
			"challenge_id": challenge.ID,
			"factor_type":  factor.FactorType,
		}); terr != nil {
			return terr
		}
		if terr = challenge.Verify(tx); terr != nil {
			return terr
		}
		if !factor.IsVerified() {
			if terr = factor.UpdateStatus(tx, models.FactorStateVerified); terr != nil {
				return terr
			}
		}
		if shouldReEncrypt && config.Security.DBEncryption.Encrypt {
			es, terr := crypto.NewEncryptedString(factor.ID.String(), []byte(secret), config.Security.DBEncryption.EncryptionKeyID, config.Security.DBEncryption.EncryptionKey)
			if terr != nil {
				return terr
			}

			factor.Secret = es.String()
			if terr := tx.UpdateOnly(factor, "secret"); terr != nil {
				return terr
			}
		}
		user, terr = models.FindUserByID(tx, user.ID)
		if terr != nil {
			return terr
		}

		token, terr = a.updateMFASessionAndClaims(r, tx, user, models.TOTPSignIn, models.GrantParams{
			FactorID: &factor.ID,
		})
		if terr != nil {
			return terr
		}
		if terr = models.InvalidateSessionsWithAALLessThan(tx, user.ID, models.AAL2.String()); terr != nil {
			return internalServerError("Failed to update sessions. %s", terr)
		}
		if terr = models.DeleteUnverifiedFactors(tx, user, factor.FactorType); terr != nil {
			return internalServerError("Error removing unverified factors. %s", terr)
		}
		return nil
	})
	if err != nil {
		return err
	}
	metering.RecordLogin(string(models.MFACodeLoginAction), user.ID)

	return sendJSON(w, http.StatusOK, token)

}

func (a *API) verifyPhoneFactor(w http.ResponseWriter, r *http.Request, params *VerifyFactorParams) error {
	ctx := r.Context()
	config := a.config
	user := getUser(ctx)
	factor := getFactor(ctx)
	db := a.db.WithContext(ctx)
	currentIP := utilities.GetIPAddress(r)

	challenge, err := a.validateChallenge(r, db, factor, params.ChallengeID)
	if err != nil {
		return err
	}

	if challenge.VerifiedAt != nil || challenge.IPAddress != currentIP {
		return unprocessableEntityError(ErrorCodeMFAIPAddressMismatch, "Challenge and verify IP addresses mismatch")
	}

	if challenge.HasExpired(config.MFA.ChallengeExpiryDuration) {
		if err := db.Destroy(challenge); err != nil {
			return internalServerError("Database error deleting challenge").WithInternalError(err)
		}
		return unprocessableEntityError(ErrorCodeMFAChallengeExpired, "MFA challenge %v has expired, verify against another challenge or create a new challenge.", challenge.ID)
	}
	var valid bool
	var otpCode string
	var shouldReEncrypt bool
	if config.Sms.IsTwilioVerifyProvider() {
		smsProvider, err := sms_provider.GetSmsProvider(*config)
		if err != nil {
			return internalServerError("Failed to get SMS provider").WithInternalError(err)
		}
		if err := smsProvider.VerifyOTP(factor.Phone.String(), params.Code); err != nil {
			return forbiddenError(ErrorCodeOTPExpired, "Token has expired or is invalid").WithInternalError(err)
		}
		valid = true
	} else {
		otpCode, shouldReEncrypt, err = challenge.GetOtpCode(config.Security.DBEncryption.DecryptionKeys, config.Security.DBEncryption.Encrypt, config.Security.DBEncryption.EncryptionKeyID)
		if err != nil {
			return internalServerError("Database error verifying MFA TOTP secret").WithInternalError(err)
		}
		valid = subtle.ConstantTimeCompare([]byte(otpCode), []byte(params.Code)) == 1
	}
	if config.Hook.MFAVerificationAttempt.Enabled {
		input := hooks.MFAVerificationAttemptInput{
			UserID:     user.ID,
			FactorID:   factor.ID,
			FactorType: factor.FactorType,
			Valid:      valid,
		}

		output := hooks.MFAVerificationAttemptOutput{}
		err := a.invokeHook(nil, r, &input, &output)
		if err != nil {
			return err
		}

		if output.Decision == hooks.HookRejection {
			if err := models.Logout(db, user.ID); err != nil {
				return err
			}

			if output.Message == "" {
				output.Message = hooks.DefaultMFAHookRejectionMessage
			}

			return forbiddenError(ErrorCodeMFAVerificationRejected, output.Message)
		}
	}
	if !valid {
		if shouldReEncrypt && config.Security.DBEncryption.Encrypt {
			if err := challenge.SetOtpCode(otpCode, true, config.Security.DBEncryption.EncryptionKeyID, config.Security.DBEncryption.EncryptionKey); err != nil {
				return err
			}

			if err := db.UpdateOnly(challenge, "otp_code"); err != nil {
				return err
			}
		}
		return unprocessableEntityError(ErrorCodeMFAVerificationFailed, "Invalid MFA Phone code entered")
	}

	var token *AccessTokenResponse

	err = db.Transaction(func(tx *storage.Connection) error {
		var terr error
		if terr = models.NewAuditLogEntry(r, tx, user, models.VerifyFactorAction, r.RemoteAddr, map[string]interface{}{
			"factor_id":    factor.ID,
			"challenge_id": challenge.ID,
			"factor_type":  factor.FactorType,
		}); terr != nil {
			return terr
		}
		if terr = challenge.Verify(tx); terr != nil {
			return terr
		}
		if !factor.IsVerified() {
			if terr = factor.UpdateStatus(tx, models.FactorStateVerified); terr != nil {
				return terr
			}
		}
		user, terr = models.FindUserByID(tx, user.ID)
		if terr != nil {
			return terr
		}

		token, terr = a.updateMFASessionAndClaims(r, tx, user, models.MFAPhone, models.GrantParams{
			FactorID: &factor.ID,
		})
		if terr != nil {
			return terr
		}
		if terr = models.InvalidateSessionsWithAALLessThan(tx, user.ID, models.AAL2.String()); terr != nil {
			return internalServerError("Failed to update sessions. %s", terr)
		}
		if terr = models.DeleteUnverifiedFactors(tx, user, factor.FactorType); terr != nil {
			return internalServerError("Error removing unverified factors. %s", terr)
		}
		return nil
	})
	if err != nil {
		return err
	}
	metering.RecordLogin(string(models.MFACodeLoginAction), user.ID)

	return sendJSON(w, http.StatusOK, token)
}

func (a *API) verifyWebAuthnFactor(w http.ResponseWriter, r *http.Request, params *VerifyFactorParams) error {
	ctx := r.Context()
	user := getUser(ctx)
	factor := getFactor(ctx)
	db := a.db.WithContext(ctx)

	var webAuthn *webauthn.WebAuthn
	var credential *webauthn.Credential
	var err error

	switch {
	case params.WebAuthn == nil:
		return badRequestError(ErrorCodeValidationFailed, "WebAuthn config required")
	case factor.IsVerified() && params.WebAuthn.AssertionResponse == nil:
		return badRequestError(ErrorCodeValidationFailed, "creation_response required to login")
	case factor.IsUnverified() && params.WebAuthn.CreationResponse == nil:
		return badRequestError(ErrorCodeValidationFailed, "assertion_response required to login")
	default:
		webAuthn, err = params.WebAuthn.ToConfig()
		if err != nil {
			return err
		}
	}

	challenge, err := a.validateChallenge(r, db, factor, params.ChallengeID)
	if err != nil {
		return err
	}
	webAuthnSession := *challenge.WebAuthnSessionData.SessionData
	// Once the challenge is validated, we consume the challenge
	if err := db.Destroy(challenge); err != nil {
		return internalServerError("Database error deleting challenge").WithInternalError(err)
	}

	if factor.IsUnverified() {
		parsedResponse, err := wbnprotocol.ParseCredentialCreationResponseBody(bytes.NewReader(params.WebAuthn.CreationResponse))
		if err != nil {
			return badRequestError(ErrorCodeValidationFailed, "Invalid credential_creation_response")
		}
		credential, err = webAuthn.CreateCredential(user, webAuthnSession, parsedResponse)
		if err != nil {
			return err
		}

	} else if factor.IsVerified() {
		parsedResponse, err := wbnprotocol.ParseCredentialRequestResponseBody(bytes.NewReader(params.WebAuthn.AssertionResponse))
		if err != nil {
			return badRequestError(ErrorCodeValidationFailed, "Invalid credential_request_response")
		}
		credential, err = webAuthn.ValidateLogin(user, webAuthnSession, parsedResponse)
		if err != nil {
			return internalServerError("Failed to validate WebAuthn MFA response").WithInternalError(err)
		}
	}
	var token *AccessTokenResponse
	err = db.Transaction(func(tx *storage.Connection) error {
		var terr error
		if terr = models.NewAuditLogEntry(r, tx, user, models.VerifyFactorAction, r.RemoteAddr, map[string]interface{}{
			"factor_id":    factor.ID,
			"challenge_id": challenge.ID,
			"factor_type":  factor.FactorType,
		}); terr != nil {
			return terr
		}
		// Challenge verification not needed as the challenge is destroyed on use
		if !factor.IsVerified() {
			if terr = factor.UpdateStatus(tx, models.FactorStateVerified); terr != nil {
				return terr
			}
			if terr = factor.SaveWebAuthnCredential(tx, credential); terr != nil {
				return terr
			}
		}
		user, terr = models.FindUserByID(tx, user.ID)
		if terr != nil {
			return terr
		}
		token, terr = a.updateMFASessionAndClaims(r, tx, user, models.MFAWebAuthn, models.GrantParams{
			FactorID: &factor.ID,
		})
		if terr != nil {
			return terr
		}
		if terr = models.InvalidateSessionsWithAALLessThan(tx, user.ID, models.AAL2.String()); terr != nil {
			return internalServerError("Failed to update session").WithInternalError(terr)
		}
		if terr = models.DeleteUnverifiedFactors(tx, user, models.WebAuthn); terr != nil {
			return internalServerError("Failed to remove unverified MFA WebAuthn factors").WithInternalError(terr)
		}
		return nil
	})
	if err != nil {
		return err
	}
	metering.RecordLogin(string(models.MFACodeLoginAction), user.ID)

	return sendJSON(w, http.StatusOK, token)
}

func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error {
	ctx := r.Context()
	factor := getFactor(ctx)
	config := a.config

	params := &VerifyFactorParams{}
	if err := retrieveRequestParams(r, params); err != nil {
		return err
	}
	if params.Code == "" && factor.FactorType != models.WebAuthn {
		return badRequestError(ErrorCodeValidationFailed, "Code needs to be non-empty")
	}

	switch factor.FactorType {
	case models.Phone:
		if !config.MFA.Phone.VerifyEnabled {
			return unprocessableEntityError(ErrorCodeMFAPhoneVerifyDisabled, "MFA verification is disabled for Phone")
		}

		return a.verifyPhoneFactor(w, r, params)
	case models.TOTP:
		if !config.MFA.TOTP.VerifyEnabled {
			return unprocessableEntityError(ErrorCodeMFATOTPVerifyDisabled, "MFA verification is disabled for TOTP")
		}
		return a.verifyTOTPFactor(w, r, params)
	case models.WebAuthn:
		if !config.MFA.WebAuthn.VerifyEnabled {
			return unprocessableEntityError(ErrorCodeMFAWebAuthnEnrollDisabled, "MFA verification is disabled for WebAuthn")
		}
		return a.verifyWebAuthnFactor(w, r, params)
	default:
		return badRequestError(ErrorCodeValidationFailed, "factor_type needs to be totp, phone, or webauthn")
	}

}

func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error {
	var err error
	ctx := r.Context()
	user := getUser(ctx)
	factor := getFactor(ctx)
	session := getSession(ctx)
	db := a.db.WithContext(ctx)

	if factor == nil || session == nil || user == nil {
		return internalServerError("A valid session and factor are required to unenroll a factor")
	}

	if factor.IsVerified() && !session.IsAAL2() {
		return unprocessableEntityError(ErrorCodeInsufficientAAL, "AAL2 required to unenroll verified factor")
	}

	err = db.Transaction(func(tx *storage.Connection) error {
		var terr error
		if terr := tx.Destroy(factor); terr != nil {
			return terr
		}
		if terr = models.NewAuditLogEntry(r, tx, user, models.UnenrollFactorAction, r.RemoteAddr, map[string]interface{}{
			"factor_id":     factor.ID,
			"factor_status": factor.Status,
			"session_id":    session.ID,
		}); terr != nil {
			return terr
		}
		if terr = factor.DowngradeSessionsToAAL1(tx); terr != nil {
			return terr
		}
		return nil
	})
	if err != nil {
		return err
	}

	return sendJSON(w, http.StatusOK, &UnenrollFactorResponse{
		ID: factor.ID,
	})
}
