package handlers import ( "bytes" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/session" ) // WebauthnAssertionGET handler starts the assertion ceremony. func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) { var ( w *webauthn.WebAuthn user *model.WebauthnUser userSession session.UserSession err error ) if userSession, err = ctx.GetSession(); err != nil { ctx.Logger.WithError(err).Error("Error occurred retrieving user session") respondUnauthorized(ctx, messageMFAValidationFailed) return } if w, err = newWebauthn(ctx); err != nil { ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } if user, err = getWebAuthnUser(ctx, userSession); err != nil { ctx.Logger.Errorf("Unable to create %s assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } var opts = []webauthn.LoginOption{ webauthn.WithAllowedCredentials(user.WebAuthnCredentialDescriptors()), } extensions := map[string]any{} if user.HasFIDOU2F() { extensions["appid"] = w.Config.RPOrigins[0] } if len(extensions) != 0 { opts = append(opts, webauthn.WithAssertionExtensions(extensions)) } var assertion *protocol.CredentialAssertion if assertion, userSession.Webauthn, err = w.BeginLogin(user, opts...); err != nil { ctx.Logger.Errorf("Unable to create %s assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } if err = ctx.SaveSession(userSession); err != nil { ctx.Logger.Errorf(logFmtErrSessionSave, "assertion challenge", regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } if err = ctx.SetJSONBody(assertion); err != nil { ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } } // WebauthnAssertionPOST handler completes the assertion ceremony after verifying the challenge. // //nolint:gocyclo func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) { var ( userSession session.UserSession err error w *webauthn.WebAuthn bodyJSON bodySignWebauthnRequest ) if err = ctx.ParseBody(&bodyJSON); err != nil { ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebauthn, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } if userSession, err = ctx.GetSession(); err != nil { ctx.Logger.WithError(err).Error("Error occurred retrieving user session") respondUnauthorized(ctx, messageMFAValidationFailed) return } if userSession.Webauthn == nil { ctx.Logger.Errorf("Webauthn session data is not present in order to handle assertion for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username) respondUnauthorized(ctx, messageMFAValidationFailed) return } if w, err = newWebauthn(ctx); err != nil { ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } var ( assertionResponse *protocol.ParsedCredentialAssertionData credential *webauthn.Credential user *model.WebauthnUser ) if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(bodyJSON.Response)); err != nil { ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } if user, err = getWebAuthnUser(ctx, userSession); err != nil { ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } if credential, err = w.ValidateLogin(user, *userSession.Webauthn, assertionResponse); err != nil { _ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeWebauthn, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } var found bool for _, device := range user.Devices { if bytes.Equal(device.KID.Bytes(), credential.ID) { device.UpdateSignInInfo(w.Config, ctx.Clock.Now(), credential.Authenticator.SignCount) found = true if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceSignIn(ctx, device.ID, device.RPID, device.LastUsedAt, device.SignCount, device.CloneWarning); err != nil { ctx.Logger.Errorf("Unable to save %s device signin count for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } break } } if !found { ctx.Logger.Errorf("Unable to save %s device signin count for assertion challenge for user '%s' device '%x' count '%d': unable to find device", regulation.AuthTypeWebauthn, userSession.Username, credential.ID, credential.Authenticator.SignCount) respondUnauthorized(ctx, messageMFAValidationFailed) return } if err = ctx.RegenerateSession(); err != nil { ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeWebauthn, nil); err != nil { respondUnauthorized(ctx, messageMFAValidationFailed) return } userSession.SetTwoFactorWebauthn(ctx.Clock.Now(), assertionResponse.Response.AuthenticatorData.Flags.UserPresent(), assertionResponse.Response.AuthenticatorData.Flags.UserVerified()) if err = ctx.SaveSession(userSession); err != nil { ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the assertion challenge and authentication time", regulation.AuthTypeWebauthn, userSession.Username, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } if bodyJSON.Workflow == workflowOpenIDConnect { handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL, bodyJSON.WorkflowID) } else { Handle2FAResponse(ctx, bodyJSON.TargetURL) } }