Skip to content

Commit

Permalink
fix: add verify
Browse files Browse the repository at this point in the history
  • Loading branch information
J0 committed Sep 13, 2024
1 parent c6809f1 commit 7446c0f
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 5 deletions.
131 changes: 126 additions & 5 deletions internal/api/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"bytes"
"crypto/subtle"
"encoding/json"
"fmt"
"net/http"
"net/url"
Expand Down Expand Up @@ -55,8 +56,9 @@ type ChallengeFactorParams struct {
}

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

type ChallengeFactorResponse struct {
Expand Down Expand Up @@ -862,6 +864,120 @@ func (a *API) verifyPhoneFactor(w http.ResponseWriter, r *http.Request, params *
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, "WebAuthn Assertion Response required to login")
case factor.IsUnverified() && params.WebAuthn.CreationResponse == nil:
return badRequestError(ErrorCodeValidationFailed, "WebAuthn Creation Response required to login")
default:
webAuthn, err = validateWebAuthnConfig(params.WebAuthn)
if err != nil {
return err
}
}
challenge, err := factor.FindChallengeByID(a.db, params.ChallengeID)
if err != nil {
return err
}
// TODO: change so we don't handle pointer
webAuthnSession := *challenge.WebAuthnSessionData.SessionData

if factor.IsUnverified() {
creationResponseJSON, err := json.Marshal(params.WebAuthn.CreationResponse)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Failed to marshal CreationResponse to JSON")
}
creationResponseReader := bytes.NewReader(creationResponseJSON)
parsedResponse, err := wbnprotocol.ParseCredentialCreationResponseBody(creationResponseReader)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Invalid credential creation response")
}

// Destroy right before use
// TODO: Consider wrapping into function
if err := db.Destroy(challenge); err != nil {
return internalServerError("Database error deleting challenge").WithInternalError(err)
}
credential, err = webAuthn.CreateCredential(user, webAuthnSession, parsedResponse)
if err != nil {
return err
}

} else if factor.IsVerified() {
assertionResponseJSON, err := json.Marshal(params.WebAuthn.AssertionResponse)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Failed to marshal AssertionResponse to JSON")
}
assertionResponseReader := bytes.NewReader(assertionResponseJSON)
parsedResponse, err := wbnprotocol.ParseCredentialRequestResponseBody(assertionResponseReader)
if err != nil {
return badRequestError(ErrorCodeValidationFailed, "Invalid credential request response")
}
if err := db.Destroy(challenge); err != nil {
return internalServerError("Database error deleting challenge").WithInternalError(err)
}
credential, err = webAuthn.ValidateLogin(user, webAuthnSession, parsedResponse)
if err != nil {
return internalServerError("error validating WebAuthn credentials")
}
}
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 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 sessions. %s", terr)
}
if terr = models.DeleteUnverifiedFactors(tx, user, models.WebAuthn); 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) VerifyFactor(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
factor := getFactor(ctx)
Expand All @@ -878,17 +994,22 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error {
switch factor.FactorType {
case models.Phone:
if !config.MFA.Phone.VerifyEnabled {
return unprocessableEntityError(ErrorCodeMFAPhoneEnrollDisabled, "MFA verification is disabled for Phone")
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(ErrorCodeMFATOTPEnrollDisabled, "MFA verification is disabled for TOTP")
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 or Phone")
return badRequestError(ErrorCodeValidationFailed, "factor_type needs to be totp, phone, or webauthn")
}

}
Expand Down
12 changes: 12 additions & 0 deletions internal/models/factor.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
OTP
TOTPSignIn
MFAPhone
MFAWebAuthn
SSOSAML
Recovery
Invite
Expand Down Expand Up @@ -83,6 +84,8 @@ func (authMethod AuthenticationMethod) String() string {
return "anonymous"
case MFAPhone:
return "mfa/phone"
case MFAWebAuthn:
return "mfa/webauthn"
}
return ""
}
Expand Down Expand Up @@ -116,6 +119,8 @@ func ParseAuthenticationMethod(authMethod string) (AuthenticationMethod, error)
return TokenRefresh, nil
case "mfa/sms":
return MFAPhone, nil
case "mfa/webauthn":
return MFAWebAuthn, nil
}
return 0, fmt.Errorf("unsupported authentication method %q", authMethod)
}
Expand Down Expand Up @@ -214,6 +219,13 @@ func (f *Factor) GetSecret(decryptionKeys map[string]string, encrypt bool, encry
return f.Secret, encrypt, nil
}

func (f *Factor) SaveWebAuthnCredential(tx *storage.Connection, credential *webauthn.Credential) error {
f.WebAuthnCredential = &WebAuthnCredential{
Credential: *credential,
}
return tx.UpdateOnly(f, "credential", "updated_at")
}

func FindFactorByFactorID(conn *storage.Connection, factorID uuid.UUID) (*Factor, error) {
var factor Factor
err := conn.Find(&factor, factorID)
Expand Down

0 comments on commit 7446c0f

Please sign in to comment.