Files
hanko/backend/handler/webauthn.go
bjoern-m bc04b729dd feat: introduce mfa (#1645)
* feat: create otp_secrets table

* feat: create otp secret model

* feat: add mfa_only column to webauthn_credentials table

* feat: add mfa only field to webauthn credential model

* feat: add mfa config (#1607)

* feat: add otp secret persister (#1613)

* feat: MFA usage sub flow (#1614)

* feat: add mfa-usage sub-flow

---------

Co-authored-by: Lennart Fleischmann <67686424+lfleischmann@users.noreply.github.com>

* feat: include platform authenticator availybility in the preflight flow (#1615)

* feat: add mfa creation subflow

* feat: adjust registration flow

* feat: integrate mfa usage sub-flow

* feat: add pages for mfa (#1622)

* feat: profile flow adjustments for mfa support

* fix: suspension logic for mfa deletion actions

* feat: use dedicated action for security key creation options

* fix: mfa method stash entry can be stale on profile flow

The mfa_creation subflow sets an mfa_method stash value so that
when creating and persisting the credential the mfa_only flag can
be set correctly in the hook responsible for that. But the profile flow
never "ends" and and returns to the initial state so I can also
register a passkey afterwards. The mfa_method stash key remains on the
stash but is used in the hook nonetheless, so the passkey is incorrectly
recognized as a security key.

The mfa_method key is now deleted after successfully persisting the
credential/security_key. This should not have an effect on the login
flow because the mfa_creation subflow is the last subflow to be
executed. It also should not affect the registration flow, because the
hook is not applied in the registration flow (persistence of data is
all handled in the create_user hook).

* feat: add new icons and english translations (#1626)

* fix: credential id encoding corrected (#1628)

* feat: add audit logs for mfa creation

* feat: add a skip link to the mfa method chooser (#1630)

* feat: save the security key during login (#1629)

* feat: show security keys in profile

* feat: add authenticator app management to profile (#1633)

* feat: add authenticator app management to profile
* feat: passkey counts as second factor

* feat: prohibit security key first factor usage

* feat: add all WA creds to exclude list on registration

* refactor: mfa stash entries and webauthn credential persistence

Renames MFA stash entry for indicating usage (login) method to make its
meaning more explicit. Also removes code persisting a webauthn credential
from the attestation verification action in the onboarding flow because
this is already done by a shared hook.

* refactor: simplify WA creation call

Co-authored-by: bjoern-m <56024829+bjoern-m@users.noreply.github.com>

* chore: adjust mfa flow

* fix: mfa onboarding always shown during login

* fix: mfa onboarding not shown after password or email creation during login

* fix: mfa onboarding not shown without user detail onboarding

* fix: correct skip/back behaviour

* feat: reuse generated otp secret when the code is invalid

* chore: skip mfa prompt if the user only has a passkey

* chore: adjust login flow 

* chore: skip mfa prompt if the user only has a passkey

* chore: refactor and improve mfa onboarding

* fix: no mfa onboarding when passwords and passkeys are disabled

* fix: only show mfa onbooarding once

* feat: add a function to the flowpilot to check whether a state has been visited

* chore: adjust recovery flow (#1655)

* feat: disable password, passcode endpoints when mfa enabled

* Feat: remember last used login method (#1674)

* chore: remove omitempty from boolean (#1676)

* chore: improved error handling (#1679)

* chore: improved error handling

* feat: add missing translations (#1681)

* feat: update aaguid list (#1678)

* fix: do not suspend webauthn action for MFA (#1778)

Do not suspend the `webauthn_verify_attestation_response` action when passkeys are disabled, but security keys and MFA are enabled.

* fix: change texts (#1785)

Change texts regarding security creation to be more consistent across the flows and to be more precise.

* Fix: UI issues (#1846)

* fix: loading spinner alignment corrected

* fix: auth app deletion link is shown while deletion is not allowed

* Chore: remove test persister (#1876)

* chore: remove deprecated test persister

* chore: replace test persister calls

* chore: add saml state fixtures

* Update backend/flow_api/services/webauthn.go

Co-authored-by: Frederic Jahn <frederic.jahn@hanko.io>

* Update backend/dto/profile.go

Co-authored-by: Frederic Jahn <frederic.jahn@hanko.io>

* fix: otp validation uses the rate limiter key for passwords

* chore: add otp-limits to the default config

* chore: add explanation for 'UserVerification' setting on security keys

---------

Co-authored-by: Lennart Fleischmann <lennart.fleischmann@hanko.io>
Co-authored-by: Lennart Fleischmann <67686424+lfleischmann@users.noreply.github.com>
Co-authored-by: Frederic Jahn <frederic.jahn@hanko.io>
2024-11-01 19:38:30 +01:00

592 lines
22 KiB
Go

package handler
import (
"encoding/base64"
"errors"
"fmt"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/dto/intern"
"github.com/teamhanko/hanko/backend/mapper"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/session"
"net/http"
"strings"
"time"
)
type WebauthnHandler struct {
persister persistence.Persister
webauthn *webauthn.WebAuthn
sessionManager session.Manager
cfg *config.Config
auditLogger auditlog.Logger
authenticatorMetadata mapper.AuthenticatorMetadata
}
const (
GetUserFailureMessage = "failed to get user: %w"
CastSessionFailureMessage = "failed to cast session object"
CreateAuditLogFailureMessage = "failed to create audit log: %w"
UserNotFoundMessage = "user not found"
SubjectParseFailureMessage = "failed to parse subject as uuid: %w"
GetWebauthnCredentialFailureMessage = "failed to get webauthn credentials: %w"
StoredChallengeMismatchMessage = "Stored challenge and received challenge do not match"
UnknownUserMessage = "unknown user"
)
// NewWebauthnHandler creates a new handler which handles all webauthn related routes
func NewWebauthnHandler(cfg *config.Config, persister persistence.Persister, sessionManager session.Manager, auditLogger auditlog.Logger, authenticatorMetadata mapper.AuthenticatorMetadata) (*WebauthnHandler, error) {
f := false
wa, err := webauthn.New(&webauthn.Config{
RPDisplayName: cfg.Webauthn.RelyingParty.DisplayName,
RPID: cfg.Webauthn.RelyingParty.Id,
RPOrigins: cfg.Webauthn.RelyingParty.Origins,
AttestationPreference: protocol.PreferDirectAttestation,
AuthenticatorSelection: protocol.AuthenticatorSelection{
RequireResidentKey: &f,
ResidentKey: protocol.ResidentKeyRequirementDiscouraged,
UserVerification: protocol.VerificationRequired,
},
Debug: false,
Timeouts: webauthn.TimeoutsConfig{
Login: webauthn.TimeoutConfig{
Timeout: time.Duration(cfg.Webauthn.Timeouts.Login) * time.Millisecond,
Enforce: true,
},
Registration: webauthn.TimeoutConfig{
Timeout: time.Duration(cfg.Webauthn.Timeouts.Registration) * time.Millisecond,
Enforce: true,
},
},
})
if err != nil {
return nil, fmt.Errorf("failed to create webauthn instance: %w", err)
}
return &WebauthnHandler{
persister: persister,
webauthn: wa,
sessionManager: sessionManager,
cfg: cfg,
auditLogger: auditLogger,
authenticatorMetadata: authenticatorMetadata,
}, nil
}
// BeginRegistration returns credential creation options for the WebAuthnAPI. It expects a valid session JWT in the request.
func (h *WebauthnHandler) BeginRegistration(c echo.Context) error {
sessionToken, ok := c.Get("session").(jwt.Token)
if !ok {
return errors.New(CastSessionFailureMessage)
}
uId, err := uuid.FromString(sessionToken.Subject())
if err != nil {
return fmt.Errorf("failed to parse userId from JWT subject:%w", err)
}
webauthnUser, user, err := h.getWebauthnUser(h.persister.GetConnection(), uId)
if err != nil {
return fmt.Errorf(GetUserFailureMessage, err)
}
if webauthnUser == nil {
err = h.auditLogger.Create(c, models.AuditLogWebAuthnRegistrationInitFailed, nil, fmt.Errorf("unknown user"))
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusBadRequest, UserNotFoundMessage).SetInternal(errors.New(fmt.Sprintf("user %s not found ", uId)))
}
t := true
options, sessionData, err := h.webauthn.BeginRegistration(
webauthnUser,
webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{
RequireResidentKey: &t,
ResidentKey: protocol.ResidentKeyRequirementRequired,
UserVerification: protocol.UserVerificationRequirement(h.cfg.Passkey.UserVerification),
}),
webauthn.WithConveyancePreference(protocol.ConveyancePreference(h.cfg.Passkey.AttestationPreference)),
// don't set the excludeCredentials list, so an already registered device can be re-registered
)
if err != nil {
return fmt.Errorf("failed to create webauthn creation options: %w", err)
}
err = h.persister.GetWebauthnSessionDataPersister().Create(*intern.WebauthnSessionDataToModel(sessionData, models.WebauthnOperationRegistration))
if err != nil {
return fmt.Errorf("failed to store creation options session data: %w", err)
}
err = h.auditLogger.Create(c, models.AuditLogWebAuthnRegistrationInitSucceeded, user, nil)
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return c.JSON(http.StatusOK, options)
}
// FinishRegistration validates the WebAuthnAPI response and associates the credential with the user. It expects a valid session JWT in the request.
// The session JWT must be associated to the same user who requested the credential creation options.
func (h *WebauthnHandler) FinishRegistration(c echo.Context) error {
sessionToken, ok := c.Get("session").(jwt.Token)
if !ok {
return errors.New(CastSessionFailureMessage)
}
request, err := protocol.ParseCredentialCreationResponse(c.Request())
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return h.persister.Transaction(func(tx *pop.Connection) error {
sessionDataPersister := h.persister.GetWebauthnSessionDataPersisterWithConnection(tx)
sessionData, err := sessionDataPersister.GetByChallenge(request.Response.CollectedClientData.Challenge)
if err != nil {
return fmt.Errorf("failed to get webauthn registration session data: %w", err)
}
if sessionData != nil && sessionData.Operation != models.WebauthnOperationRegistration {
sessionData = nil
}
if sessionData == nil {
err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnRegistrationFinalFailed, nil, fmt.Errorf("received unkown challenge"))
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusBadRequest, StoredChallengeMismatchMessage).SetInternal(errors.New("sessionData not found"))
}
if sessionToken.Subject() != sessionData.UserId.String() {
err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnRegistrationFinalFailed, nil, fmt.Errorf("user session does not match sessionData subject"))
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusBadRequest, StoredChallengeMismatchMessage).SetInternal(errors.New("userId in webauthn.sessionData does not match user session"))
}
webauthnUser, user, err := h.getWebauthnUser(tx, sessionData.UserId)
if err != nil {
return fmt.Errorf(GetUserFailureMessage, err)
}
if webauthnUser == nil {
err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnRegistrationFinalFailed, nil, fmt.Errorf(UnknownUserMessage))
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusBadRequest).SetInternal(errors.New(UserNotFoundMessage))
}
credential, err := h.webauthn.CreateCredential(webauthnUser, *intern.WebauthnSessionDataFromModel(sessionData), request)
if err != nil {
errorMessage := "failed to validate attestation"
errorStatus := http.StatusBadRequest
// Safari currently (v. 16.2) does not provide a UI in case of a (registration) ceremony
// being performed with an authenticator NOT protected by e.g. a PIN. While Chromium based browsers do offer
// a UI guiding through the setup of a PIN, Safari simply performs the ceremony without then setting the UV
// flag even if it is required. In order to provide an appropriate error message to the frontend/user, we
// need to return an error response distinguishable from other error cases. We use a dedicated/separate HTTP
// status code because it seemed a bit more robust than forcing the frontend to check on a matching
// (sub-)string in the error message in order to properly display the error.
if err, ok := err.(*protocol.Error); ok && err.Type == protocol.ErrVerification.Type && strings.Contains(err.DevInfo, "User verification") {
errorMessage = fmt.Sprintf("%s: %s: %s", errorMessage, err.Details, err.DevInfo)
errorStatus = http.StatusUnprocessableEntity
}
aErr := h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnRegistrationFinalFailed, user, errors.New(errorMessage))
if aErr != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, aErr)
}
return echo.NewHTTPError(errorStatus, errorMessage).SetInternal(err)
}
backupEligible := request.Response.AttestationObject.AuthData.Flags.HasBackupEligible()
backupState := request.Response.AttestationObject.AuthData.Flags.HasBackupState()
model := intern.WebauthnCredentialToModel(credential, sessionData.UserId, backupEligible, backupState, false, h.authenticatorMetadata)
err = h.persister.GetWebauthnCredentialPersisterWithConnection(tx).Create(*model)
if err != nil {
return fmt.Errorf("failed to store webauthn credential: %w", err)
}
err = sessionDataPersister.Delete(*sessionData)
if err != nil {
c.Logger().Errorf("failed to delete attestation session data: %w", err)
}
err = h.auditLogger.Create(c, models.AuditLogWebAuthnRegistrationFinalSucceeded, user, nil)
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return c.JSON(http.StatusOK, map[string]string{"credential_id": model.ID, "user_id": webauthnUser.UserId.String()})
})
}
type BeginAuthenticationBody struct {
UserID *string `json:"user_id" validate:"uuid4"`
}
// BeginAuthentication returns credential assertion options for the WebAuthnAPI.
func (h *WebauthnHandler) BeginAuthentication(c echo.Context) error {
var request BeginAuthenticationBody
if err := (&echo.DefaultBinder{}).BindBody(c, &request); err != nil {
return dto.ToHttpError(err)
}
var options *protocol.CredentialAssertion
var sessionData *webauthn.SessionData
var user *models.User
if request.UserID != nil {
// non discoverable login initialization
userId, err := uuid.FromString(*request.UserID)
if err != nil {
err = h.auditLogger.Create(c, models.AuditLogWebAuthnAuthenticationInitFailed, nil, fmt.Errorf("user_id is not a uuid"))
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusBadRequest, "failed to parse UserID as uuid").SetInternal(err)
}
var webauthnUser *intern.WebauthnUser
webauthnUser, user, err = h.getWebauthnUser(h.persister.GetConnection(), userId)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError).SetInternal(fmt.Errorf(GetUserFailureMessage, err))
}
if webauthnUser == nil {
err = h.auditLogger.Create(c, models.AuditLogWebAuthnAuthenticationInitFailed, nil, fmt.Errorf(UnknownUserMessage))
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusBadRequest, UserNotFoundMessage)
}
if len(webauthnUser.WebAuthnCredentials()) > 0 {
options, sessionData, err = h.webauthn.BeginLogin(
webauthnUser,
webauthn.WithUserVerification(protocol.UserVerificationRequirement(h.cfg.Passkey.UserVerification)),
)
if err != nil {
return fmt.Errorf("failed to create webauthn assertion options: %w", err)
}
}
}
if options == nil && sessionData == nil {
var err error
options, sessionData, err = h.webauthn.BeginDiscoverableLogin(
webauthn.WithUserVerification(protocol.UserVerificationRequirement(h.cfg.Passkey.UserVerification)),
)
if err != nil {
return fmt.Errorf("failed to create webauthn assertion options for discoverable login: %w", err)
}
}
err := h.persister.GetWebauthnSessionDataPersister().Create(*intern.WebauthnSessionDataToModel(sessionData, models.WebauthnOperationAuthentication))
if err != nil {
return fmt.Errorf("failed to store webauthn assertion session data: %w", err)
}
// Remove all transports, because of a bug in android and windows where the internal authenticator gets triggered,
// when the transports array contains the type 'internal' although the credential is not available on the device.
for i := range options.Response.AllowedCredentials {
options.Response.AllowedCredentials[i].Transport = nil
}
err = h.auditLogger.Create(c, models.AuditLogWebAuthnAuthenticationInitSucceeded, user, nil)
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return c.JSON(http.StatusOK, options)
}
// FinishAuthentication validates the WebAuthnAPI response and on success it returns a new session JWT.
func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error {
request, err := protocol.ParseCredentialRequestResponse(c.Request())
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return h.persister.Transaction(func(tx *pop.Connection) error {
sessionDataPersister := h.persister.GetWebauthnSessionDataPersisterWithConnection(tx)
sessionData, err := sessionDataPersister.GetByChallenge(request.Response.CollectedClientData.Challenge)
if err != nil {
return fmt.Errorf("failed to get webauthn assertion session data: %w", err)
}
if sessionData != nil && sessionData.Operation != models.WebauthnOperationAuthentication {
sessionData = nil
}
if sessionData == nil {
err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, nil, fmt.Errorf("received unkown challenge"))
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusUnauthorized, StoredChallengeMismatchMessage).SetInternal(errors.New("sessionData not found"))
}
model := intern.WebauthnSessionDataFromModel(sessionData)
var credential *webauthn.Credential
var webauthnUser *intern.WebauthnUser
var user *models.User
if sessionData.UserId.IsNil() {
// Discoverable Login
userId, err := uuid.FromBytes(request.Response.UserHandle)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "failed to parse userHandle as uuid").SetInternal(err)
}
webauthnUser, user, err = h.getWebauthnUser(tx, userId)
if err != nil {
return fmt.Errorf(GetUserFailureMessage, err)
}
if webauthnUser == nil {
err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, nil, fmt.Errorf(UnknownUserMessage))
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New(UserNotFoundMessage))
}
credential, err = h.webauthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (user webauthn.User, err error) {
return webauthnUser, nil
}, *model, request)
if err != nil {
logErr := h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, user, fmt.Errorf("assertion validation failed"))
if logErr != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusUnauthorized, "failed to validate assertion").SetInternal(err)
}
} else {
// non discoverable Login
webauthnUser, user, err = h.getWebauthnUser(tx, sessionData.UserId)
if err != nil {
return fmt.Errorf(GetUserFailureMessage, err)
}
if webauthnUser == nil {
err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, nil, fmt.Errorf(UnknownUserMessage))
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New(UserNotFoundMessage))
}
credential, err = h.webauthn.ValidateLogin(webauthnUser, *model, request)
if err != nil {
logErr := h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnAuthenticationFinalFailed, user, fmt.Errorf("assertion validation failed"))
if logErr != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return echo.NewHTTPError(http.StatusUnauthorized, "failed to validate assertion").SetInternal(err)
}
}
var dbCred *models.WebauthnCredential
for i := range webauthnUser.WebauthnCredentials {
if webauthnUser.WebauthnCredentials[i].ID == base64.RawURLEncoding.EncodeToString(credential.ID) {
dbCred = &webauthnUser.WebauthnCredentials[i]
break
}
}
if dbCred != nil {
if dbCred.BackupEligible != request.Response.AuthenticatorData.Flags.HasBackupEligible() || dbCred.BackupState != request.Response.AuthenticatorData.Flags.HasBackupState() {
dbCred.BackupState = request.Response.AuthenticatorData.Flags.HasBackupState()
dbCred.BackupEligible = request.Response.AuthenticatorData.Flags.HasBackupEligible()
}
now := time.Now().UTC()
dbCred.LastUsedAt = &now
err = h.persister.GetWebauthnCredentialPersisterWithConnection(tx).Update(*dbCred)
if err != nil {
return fmt.Errorf("failed to update webauthn credential: %w", err)
}
}
err = sessionDataPersister.Delete(*sessionData)
if err != nil {
return fmt.Errorf("failed to delete assertion session data: %w", err)
}
var emailJwt *dto.EmailJwt
if e := user.Emails.GetPrimary(); e != nil {
emailJwt = dto.JwtFromEmailModel(e)
}
token, _, err := h.sessionManager.GenerateJWT(webauthnUser.UserId, emailJwt)
if err != nil {
return fmt.Errorf("failed to generate jwt: %w", err)
}
cookie, err := h.sessionManager.GenerateCookie(token)
if err != nil {
return fmt.Errorf("failed to create session cookie: %w", err)
}
c.Response().Header().Set("X-Session-Lifetime", fmt.Sprintf("%d", cookie.MaxAge))
if h.cfg.Session.EnableAuthTokenHeader {
c.Response().Header().Set("X-Auth-Token", token)
} else {
c.SetCookie(cookie)
}
err = h.auditLogger.Create(c, models.AuditLogWebAuthnAuthenticationFinalSucceeded, user, nil)
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return c.JSON(http.StatusOK, map[string]string{"credential_id": base64.RawURLEncoding.EncodeToString(credential.ID), "user_id": webauthnUser.UserId.String()})
})
}
func (h *WebauthnHandler) ListCredentials(c echo.Context) error {
sessionToken, ok := c.Get("session").(jwt.Token)
if !ok {
return errors.New(CastSessionFailureMessage)
}
userId, err := uuid.FromString(sessionToken.Subject())
if err != nil {
return fmt.Errorf(SubjectParseFailureMessage, err)
}
credentials, err := h.persister.GetWebauthnCredentialPersister().GetFromUser(userId)
if err != nil {
return fmt.Errorf(GetWebauthnCredentialFailureMessage, err)
}
response := make([]*dto.WebauthnCredentialResponse, len(credentials))
for i := range credentials {
response[i] = dto.FromWebauthnCredentialModel(&credentials[i])
}
return c.JSON(http.StatusOK, response)
}
func (h *WebauthnHandler) UpdateCredential(c echo.Context) error {
sessionToken, ok := c.Get("session").(jwt.Token)
if !ok {
return errors.New(CastSessionFailureMessage)
}
userId, err := uuid.FromString(sessionToken.Subject())
if err != nil {
return fmt.Errorf(SubjectParseFailureMessage, err)
}
credentialID := c.Param("id")
var body dto.WebauthnCredentialUpdateRequest
err = (&echo.DefaultBinder{}).BindBody(c, &body)
if err != nil {
return dto.ToHttpError(err)
}
user, err := h.persister.GetUserPersister().Get(userId)
if err != nil {
return fmt.Errorf(GetUserFailureMessage, err)
}
credential, err := h.persister.GetWebauthnCredentialPersister().Get(credentialID)
if err != nil {
return fmt.Errorf(GetWebauthnCredentialFailureMessage, err)
}
if credential == nil || credential.UserId.String() != user.ID.String() {
return echo.NewHTTPError(http.StatusNotFound).SetInternal(errors.New("the user does not have a webauthn credential with the specified credentialId"))
}
if body.Name != nil {
credential.Name = body.Name
}
return h.persister.Transaction(func(tx *pop.Connection) error {
err = h.persister.GetWebauthnCredentialPersisterWithConnection(tx).Update(*credential)
if err != nil {
return fmt.Errorf("failed to update webauthn credential: %w", err)
}
err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnCredentialUpdated, user, nil)
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return nil
})
}
func (h *WebauthnHandler) DeleteCredential(c echo.Context) error {
sessionToken, ok := c.Get("session").(jwt.Token)
if !ok {
return errors.New(CastSessionFailureMessage)
}
userId, err := uuid.FromString(sessionToken.Subject())
if err != nil {
return fmt.Errorf(SubjectParseFailureMessage, err)
}
user, err := h.persister.GetUserPersister().Get(userId)
if err != nil {
return fmt.Errorf("failed to fetch user from db: %w", err)
}
credentialId := c.Param("id")
credential, err := h.persister.GetWebauthnCredentialPersister().Get(credentialId)
if err != nil {
return fmt.Errorf("failed to get webauthn credential: %w", err)
}
if credential == nil || credential.UserId.String() != user.ID.String() {
return echo.NewHTTPError(http.StatusNotFound).SetInternal(errors.New("the user does not have a webauthn credential with the specified credentialId"))
}
return h.persister.Transaction(func(tx *pop.Connection) error {
err = h.persister.GetWebauthnCredentialPersisterWithConnection(tx).Delete(*credential)
if err != nil {
return fmt.Errorf("failed to delete credential from db: %w", err)
}
err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogWebAuthnCredentialDeleted, user, nil)
if err != nil {
return fmt.Errorf(CreateAuditLogFailureMessage, err)
}
return c.NoContent(http.StatusNoContent)
})
}
func (h WebauthnHandler) getWebauthnUser(connection *pop.Connection, userId uuid.UUID) (*intern.WebauthnUser, *models.User, error) {
user, err := h.persister.GetUserPersisterWithConnection(connection).Get(userId)
if err != nil {
return nil, nil, fmt.Errorf(GetUserFailureMessage, err)
}
if user == nil {
return nil, nil, nil
}
credentials, err := h.persister.GetWebauthnCredentialPersisterWithConnection(connection).GetFromUser(user.ID)
if err != nil {
return nil, nil, fmt.Errorf(GetWebauthnCredentialFailureMessage, err)
}
webauthnUser, err := intern.NewWebauthnUser(*user, credentials)
if err != nil {
return nil, nil, err
}
return webauthnUser, user, nil
}