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/models" "github.com/authelia/authelia/v4/internal/regulation" ) // SecondFactorWebauthnAssertionGET handler starts the assertion ceremony. func SecondFactorWebauthnAssertionGET(ctx *middlewares.AutheliaCtx) { var ( w *webauthn.WebAuthn user *models.WebauthnUser err error ) userSession := ctx.GetSession() 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 := make(map[string]interface{}) if user.HasFIDOU2F() { extensions["appid"] = w.Config.RPOrigin } 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 } } // SecondFactorWebauthnAssertionPOST handler completes the assertion ceremony after verifying the challenge. func SecondFactorWebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) { var ( err error w *webauthn.WebAuthn requestBody signWebauthnRequestBody ) if err = ctx.ParseBody(&requestBody); err != nil { ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebauthn, err) respondUnauthorized(ctx, messageMFAValidationFailed) return } userSession := ctx.GetSession() 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 *models.WebauthnUser ) if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(ctx.PostBody())); 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.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx); 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.SetTwoFactor(ctx.Clock.Now()) userSession.Webauthn = nil 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 userSession.OIDCWorkflowSession != nil { handleOIDCWorkflowResponse(ctx) } else { Handle2FAResponse(ctx, requestBody.TargetURL) } }