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

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 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:

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, the purchase operation wraps the appstore.Purchase() call with the retry predicate to handle authentication interruptions:

// 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 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 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 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 and interactive paths in 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.

The retry logic handles this error by:

  • Detecting the error in the command wrapper (e.g., 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:

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 and 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 and related files, while their underlying string identifiers and failure type constants (FailureTypePasswordTokenExpired, FailureTypeAuthCodeRequired, FailureTypeLicenseNotFound) are declared in pkg/appstore/constants.go.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →