# How ipatool's Retry Logic Handles Authentication Errors: ErrPasswordTokenExpired, ErrAuthCodeRequired, and ErrLicenseRequired

> Discover how ipatool's retry logic resolves ErrPasswordTokenExpired, ErrAuthCodeRequired, and ErrLicenseRequired errors by prompting for updated credentials or codes.

- Repository: [Majd/ipatool](https://github.com/majd/ipatool)
- Tags: internals
- Published: 2026-09-06

---

**ipatool implements a centralized retry mechanism using `github.com/avast/retry-go` that specifically intercepts three authentication-related errors—`ErrPasswordTokenExpired`, `ErrAuthCodeRequired`, and `ErrLicenseRequired`—to prompt users for updated credentials, 2FA codes, or license purchases before automatically re-attempting the App Store operation.**

The `majd/ipatool` command-line utility interacts with Apple's App Store APIs, which frequently require re-authentication or license validation during download and purchase operations. Rather than failing immediately on authentication errors, the tool wraps critical App Store calls in a retry predicate that detects specific error constants defined in [`pkg/appstore/constants.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/constants.go) and triggers interactive recovery flows in the command layer.

## Centralized Retry Implementation in the cmd/ Package

The retry behavior is implemented at the command level within the `cmd/` package, using the third-party `retry` helper from `github.com/avast/retry-go`. Every operation that communicates with the App Store—such as `download`, `purchase`, `list-versions`, and `auth`—wraps its core call with `retry.Do()` and supplies a custom predicate via `retry.RetryIf()`.

The predicate closure inspects returned errors using `errors.Is()` to determine whether an operation warrants re-attempt:

```go
retry.RetryIf(func(err error) bool {
    // retry only for transient network or App Store errors
    return errors.Is(err, appstore.ErrPasswordTokenExpired) ||
           errors.Is(err, appstore.ErrAuthCodeRequired) ||
           errors.Is(err, appstore.ErrLicenseRequired)
})

```

By default, the retry mechanism attempts the operation up to three times. The predicate limits retries exclusively to the three authentication errors above, preventing infinite loops for unrelated failures while handling the transient authentication issues most users encounter.

### Practical Implementation in Command Handlers

In [`cmd/purchase.go`](https://github.com/majd/ipatool/blob/main/cmd/purchase.go), the purchase operation wraps the `appstore.Purchase()` call with the retry predicate to handle authentication interruptions:

```go
// Example from cmd/purchase.go – retrying a purchase while handling auth errors
if err := retry.Do(func() error {
    _, err = as.Purchase(purchaseInput)
    return err
}, retry.RetryIf(func(err error) bool {
    return errors.Is(err, appstore.ErrPasswordTokenExpired) ||
           errors.Is(err, appstore.ErrAuthCodeRequired) ||
           errors.Is(err, appstore.ErrLicenseRequired)
})); err != nil {
    return err
}

```

Similarly, [`cmd/download.go`](https://github.com/majd/ipatool/blob/main/cmd/download.go) implements identical retry logic for the `appstore.Download()` method, enabling automatic license acquisition flows when `ErrLicenseRequired` is detected.

## Error-Specific Recovery Flows

When the retry predicate returns `true`, the command layer intercepts the error and executes a targeted recovery workflow before the next attempt.

### Handling Expired Password Tokens (ErrPasswordTokenExpired)

**`ErrPasswordTokenExpired`** is returned by multiple App Store actions—including `appstore.Login`, `appstore.Download`, and `appstore.ListVersions`—when the stored password token becomes invalid. The error originates in [`pkg/appstore/appstore_login.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/appstore_login.go) when the response contains `FailureTypePasswordTokenExpired` or the message "Your password has changed".

When this error triggers the retry loop:
- The command code detects the specific error constant
- The user is prompted to enter a fresh password for the stored account
- The operation retries with the new credentials

### Managing Two-Factor Authentication (ErrAuthCodeRequired)

**`ErrAuthCodeRequired`** is emitted by `appstore.Login` in [`pkg/appstore/appstore_login.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/appstore_login.go) when the server responds with `FailureTypeAuthCodeRequired`. This occurs when two-factor authentication is enforced and the request lacks a valid auth code.

Upon retry detection:
- The command layer in [`cmd/auth.go`](https://github.com/majd/ipatool/blob/main/cmd/auth.go) and interactive paths in [`cmd/purchase.go`](https://github.com/majd/ipatool/blob/main/cmd/purchase.go) catch the error
- The user is prompted to provide the 2FA code (interactively or via command flags)
- The login retries with the supplied authentication code

### Automatic License Acquisition (ErrLicenseRequired)

**`ErrLicenseRequired`** is raised by actions requiring valid licenses—such as `appstore.Download` and `appstore.ListVersions`—when the App Store returns `FailureTypeLicenseNotFound` as defined in [`pkg/appstore/constants.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/constants.go).

The retry logic handles this error by:
- Detecting the error in the command wrapper (e.g., [`cmd/download.go`](https://github.com/majd/ipatool/blob/main/cmd/download.go))
- Checking if the `--purchase` flag is enabled
- Automatically invoking the purchase flow to obtain the license if the flag is set
- Retrying the original request after successful license acquisition
- Informing the user of the missing license if the flag is not set

## Key Source Files and Implementation Details

The retry mechanism spans several critical files in the repository:

- **[`pkg/appstore/constants.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/constants.go)** – Defines the underlying failure type constants (`FailureTypePasswordTokenExpired`, `FailureTypeAuthCodeRequired`, `FailureTypeLicenseNotFound`) that map to the exported error variables.

- **[`pkg/appstore/appstore_login.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/appstore_login.go)** – Returns `ErrPasswordTokenExpired` and `ErrAuthCodeRequired` based on App Store response codes, triggering the retry loops in command handlers.

- **[`pkg/appstore/appstore_download.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/appstore_download.go)** – Propagates `ErrPasswordTokenExpired` and `ErrLicenseRequired` from download operations back to the command layer.

- **[`cmd/auth.go`](https://github.com/majd/ipatool/blob/main/cmd/auth.go)** – Implements the interactive retry loop for handling `ErrAuthCodeRequired` and password re-entry flows.

- **[`cmd/purchase.go`](https://github.com/majd/ipatool/blob/main/cmd/purchase.go), [`cmd/download.go`](https://github.com/majd/ipatool/blob/main/cmd/download.go), [`cmd/list_versions.go`](https://github.com/majd/ipatool/blob/main/cmd/list_versions.go), [`cmd/get_version_metadata.go`](https://github.com/majd/ipatool/blob/main/cmd/get_version_metadata.go)** – Each wraps core App Store calls with `retry.RetryIf` predicates checking the three specific error constants.

## Summary

- **ipatool** centralizes retry logic in the `cmd/` package using `github.com/avast/retry-go` with a maximum of three attempts.
- The `retry.RetryIf` predicate specifically checks for `ErrPasswordTokenExpired`, `ErrAuthCodeRequired`, and `ErrLicenseRequired` using `errors.Is()`.
- Each error triggers a distinct recovery flow: password reprompting for expired tokens, 2FA code collection for authentication requirements, and conditional license purchase for missing entitlements.
- Error definitions reside in [`pkg/appstore/constants.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/constants.go) and [`pkg/appstore/appstore_login.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/appstore_login.go), while command implementations in `cmd/*.go` handle the interactive recovery logic.

## Frequently Asked Questions

### How does ipatool determine which errors are retryable?

ipatool uses a closure passed to `retry.RetryIf()` that applies `errors.Is()` checks against three specific constants: `appstore.ErrPasswordTokenExpired`, `appstore.ErrAuthCodeRequired`, and `appstore.ErrLicenseRequired`. This predicate returns `true` only for these authentication-related errors, allowing the `github.com/avast/retry-go` library to re-execute the wrapped function while preventing retries for unrelated failures.

### What happens if the user doesn't provide a new password or auth code during the retry loop?

If the user fails to provide valid credentials during the interactive prompt triggered by `ErrPasswordTokenExpired` or `ErrAuthCodeRequired`, the underlying App Store call will continue to return the same error. Since the retry predicate will still match this error, the loop continues until the maximum attempt count (default three) is reached, at which point the final error is propagated to the user and the command exits with a failure status.

### Can ipatool automatically purchase licenses without user interaction?

Yes, but only when the `--purchase` flag is explicitly provided. When `ErrLicenseRequired` triggers a retry and the flag is set, the command layer automatically invokes the purchase flow to obtain the required license before re-attempting the original download or metadata request. Without this flag, the tool informs the user that a license is missing and does not attempt automatic acquisition.

### Where are the error constants defined in the source code?

The error variables `ErrPasswordTokenExpired`, `ErrAuthCodeRequired`, and `ErrLicenseRequired` are defined and returned in [`pkg/appstore/appstore_login.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/appstore_login.go) and related files, while their underlying string identifiers and failure type constants (`FailureTypePasswordTokenExpired`, `FailureTypeAuthCodeRequired`, `FailureTypeLicenseNotFound`) are declared in [`pkg/appstore/constants.go`](https://github.com/majd/ipatool/blob/main/pkg/appstore/constants.go).