{"record":{"id":"68a6aa7eb9335b32","repo":"gravitational/teleport","slug":"max-retry-attempts-reached-w","errorCode":null,"errorMessage":"max retry attempts reached: %w","messagePattern":"max retry attempts reached: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"lib/auth/webauthncli/fido2.go","lineNumber":1025,"sourceCode":"\n\t\t\t// See https://github.com/Yubico/libfido2/blob/main/src/fido/err.h#L32.\n\t\t\tswitch fidoErr.Code {\n\t\t\tcase 60: // FIDO_ERR_UV_BLOCKED, 0x3c\n\t\t\t\tconst msg = \"\" +\n\t\t\t\t\t\"The user verification function in your security key is blocked. \" +\n\t\t\t\t\t\"This is likely due to too many failed authentication attempts. \" +\n\t\t\t\t\t\"Consult your manufacturer documentation for how to unblock your security key. \" +\n\t\t\t\t\t\"Alternatively, you may unblock your device by using it in the Web UI.\"\n\t\t\t\treturn trace.Wrap(err, msg)\n\t\t\tcase 63: // FIDO_ERR_UV_INVALID, 0x3f\n\t\t\t\tfidoLog.DebugContext(context.Background(), \"Retrying libfido2 error 63\")\n\t\t\t\tcontinue\n\t\t\tdefault: // Unexpected code.\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn fmt.Errorf(\"max retry attempts reached: %w\", err)\n\t}\n}\n\nfunc withPINHandler(cb deviceCallbackFunc) pinAwareCallbackFunc {\n\treturn func(dev FIDODevice, info *deviceInfo, pin string) (requiresPIN bool, err error) {\n\t\t// Attempt to select a device by running \"deviceCallback\" on it.\n\t\t// For most scenarios this works, saving a touch.\n\t\terr = cb(dev, info, pin)\n\t\tswitch {\n\t\tcase errors.Is(err, libfido2.ErrPinRequired):\n\t\t\t// Continued below.\n\t\tcase errors.Is(err, libfido2.ErrUnsupportedOption) && pin == \"\" && !info.uv && info.clientPinSet:\n\t\t\t// The failing option is likely to be \"UV\", so we handle this the same as\n\t\t\t// ErrPinRequired: see if the user selects this device, ask for the PIN and\n\t\t\t// try again.\n\t\t\t// Continued below.\n\t\tdefault:\n\t\t\treturn","sourceCodeStart":1007,"sourceCodeEnd":1043,"githubUrl":"https://github.com/gravitational/teleport/blob/1283425b60ec5f60d509ba4c791183d452923ff7/lib/auth/webauthncli/fido2.go#L1007-L1043","documentation":"The FIDO2 device callback wrapper retries transient failures (e.g. touch required, wrong PIN attempts) up to a bounded number of attempts. When the retry budget is exhausted while the last attempt still returned an error, it returns \"max retry attempts reached\" wrapping the most recent error. This prevents infinite retry loops against misbehaving or repeatedly-failing devices.","triggerScenarios":"A fido2 device operation (login/register callback) fails repeatedly with a retryable status (e.g. FIDO_ERR_UV_BLOCKED, wrong PIN, device busy) for the maximum number of attempts, so the loop exits via the final return.","commonSituations":"User entering the wrong FIDO2 PIN repeatedly until UV is blocked; a security key left in a busy/unresponsive state; device drivers or libfido2 returning transient errors continuously; automated scripts hammering a device without human touch.","solutions":["Wait for the UV-blocked/cooldown period to expire and retry with the correct PIN.","Replace retry loops with user interaction: prompt the user to touch the key / re-enter PIN between attempts.","Unplug and reinsert the security key (or reset it if necessary) to clear a stuck state.","Increase the max retry count in the retry wrapper only if the operation is known to need more attempts, and add backoff between retries."],"exampleFix":"// before\nfor i := 0; i < maxRetries; i++ {\n    return deviceCallback(dev) // fails repeatedly, no backoff\n}\n// after\nfor i := 0; i < maxRetries; i++ {\n    err := deviceCallback(dev)\n    if err == nil { return nil }\n    time.Sleep(backoff(i)) // give the device/user time to recover\n}","handlingStrategy":"retry","validationCode":"info, err := dev.Info()\nif err != nil { return err }\nif info.AuthenticatorConfig != nil && uvBlocked(info) {\n    return errors.New(\"device UV is temporarily blocked; wait before retrying\")\n}","typeGuard":"func isRetryableFIDO2Error(err error) bool {\n    var le *libfido2.Error\n    if errors.As(err, &le) {\n        switch le.Code {\n        case libfido2.ErrTouchRequired, libfido2.ErrPinInvalid, libfido2.ErrPinAuthBlocked:\n            return le.Code != libfido2.ErrPinAuthBlocked // blocked needs cooldown, not blind retry\n        }\n    }\n    return false\n}","tryCatchPattern":"assertion, err := fido2Login(ctx, cfg, user, prompt)\nif err != nil {\n    if strings.Contains(err.Error(), \"max retry attempts reached\") {\n        return trace.Wrap(err, \"ask the user to touch the key or re-enter the correct PIN, then retry\")\n    }\n    return trace.Wrap(err)\n}","preventionTips":["Prompt for user touch/PIN between attempts instead of tight loops.","Respect UV-blocked cooldowns (err 0x37) — wait before retrying, don't hammer the device.","Apply backoff in any custom deviceCallback retry wrapper.","Keep the security key firmware/driver (libfido2) up to date to reduce transient failures."],"tags":["webauthn","fido2","retry-exhausted","hardware-keys"],"backgroundTag":"max-retries-exceeded","analyzedSha":"1283425b60ec5f60d509ba4c791183d452923ff7","analyzedAt":"2026-09-02T04:06:41.601Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}