Coding

Auth0 Swift Major Migration

Try it

Use when upgrading an iOS or macOS app's Auth0.swift SDK from v2 to v3. Detects the current version, fetches the new SDK source to confirm API signatures, an...

What it does

Use when upgrading an iOS or macOS app's Auth0.swift SDK from v2 to v3. Detects the current version, fetches the new SDK source to confirm API signatures, and applies only the breaking changes that affect real call sites — use even if the user says "update my Auth0 Swift SDK" or "migrate to Auth0.swift v3".

The skill document

Auth0.swift v3 Migration

Migrates an existing Auth0.swift v2 integration to v3. Every code change is gated on a search that confirms the project actually calls the affected API — if the project never uses CredentialsManager, no CredentialsManager code is touched. Changes follow the project's existing architecture and Apple platform conventions.

When NOT to Use

  • New Auth0 integration (no existing Auth0.swift): Use auth0-swift
  • Minor/patch update (e.g., 2.17 → 2.18): Run pod update Auth0 or update SPM — no migration needed
  • Android apps: Use auth0-android
  • React Native / Expo: Use auth0-react-native or auth0-expo

Prerequisites

  • Existing Auth0.swift v2 integration
  • Xcode installed; project builds cleanly on the current version
  • Project under git version control with a clean working tree

Migration Workflow

Agent instruction: Execute every step in order. The goal is a green build with the smallest correct changeset. Each code-change step is gated by the Step 4 file-reading audit — if the API was not found in the project's source files, skip the entire step for that area. Never add code the project doesn't already call.


Step 1 — Pre-flight & Safety Backup

# 1a. Verify clean working tree — stop if there are uncommitted changes
git status --porcelain

If the output is non-empty, ask the user:

"You have uncommitted changes. Should I stash them before proceeding (git stash), or would you like to commit first?"

# 1b. Create a safety branch the user can reset to at any time
git checkout -b auth0-v3-migration-backup
git checkout -
# 1c. Pick an available simulator, then confirm the project builds before touching anything
SIM=$(xcrun simctl list devices available -j \
  | python3 -c "import sys,json; d=json.load(sys.stdin); \
    phones=[dev for devs in d['devices'].values() for dev in devs \
            if 'iPhone' in dev.get('name','') and dev.get('isAvailable')]; \
    print(phones[0]['name'] if phones else 'iPhone 16')")
xcodebuild build \
  -scheme  \
  -destination "platform=iOS Simulator,name=${SIM}" \
  2>&1 | tail -5

If the build fails, stop. Ask the user to fix the existing issues first.


Step 2 — Detect Current & Target Versions

Detect the current Auth0.swift version from the project's dependency files:

# Check Package.resolved first (most reliable)
find . -name "Package.resolved" | xargs grep -A3 '"auth0/Auth0.swift"\|Auth0.swift"' 2>/dev/null | grep '"version"'

# Fallback: Podfile.lock
grep "^  - Auth0 " Podfile.lock 2>/dev/null

# Fallback: Cartfile.resolved
grep "auth0/Auth0.swift" Cartfile.resolved 2>/dev/null

# Fallback: Package.swift
grep -A2 'auth0/Auth0.swift' Package.swift 2>/dev/null

Resolve the target version. There are two paths:

Path A — the user passed a target version argument ($ARGUMENTS):

Validate it against the published releases before using it. It must pass all three checks:

# List all published Auth0.swift v3 release tags
curl -s https://api.github.com/repos/auth0/Auth0.swift/releases | python3 -c "
import sys, json
releases = json.load(sys.stdin)
v3 = [r for r in releases if r['tag_name'].startswith('3') and not r['draft']]
for r in v3:
    print(r['tag_name'])
"
  1. Exists — the requested tag appears in the published release list above.
  2. Correct major — the tag is within the v3 major line (starts with 3). A 2.x or any other major is not valid; reject it.
  3. Not a downgrade — the tag is newer than the version detected in the project.

On any check failing, STOP and ask the user. Do not silently fall back. For example:

  • "3.9.9 isn't a published Auth0.swift release. Published v3 releases are: 3.0.0-beta.2, … . Please pass a valid v3 tag, or omit the argument to auto-resolve the latest v3 release."
  • "2.10.0 is a v2 release, not v3. This skill migrates to v3. Pass a v3 tag (e.g. 3.0.0-beta.2) or omit the argument."
  • "3.0.0-beta.1 is older than the 3.0.0-beta.2 already in your project — that's a downgrade. Pass a newer v3 tag or omit the argument."

Path B — no argument: auto-resolve the latest v3 release (including pre-releases):

# Newest v3.x release tag (stable or pre-release), most recent first
curl -s https://api.github.com/repos/auth0/Auth0.swift/releases | python3 -c "
import sys, json
releases = json.load(sys.stdin)
v3 = [r for r in releases if r['tag_name'].startswith('3') and not r['draft']]
if v3:
    print(v3[0]['tag_name'])
else:
    print('')
"

Record the result as `` and use it in every subsequent step.

*If is a pre-release** (contains `-beta`, `-rc`, etc.), inform the user before continuing: *"The latest v3 release is (a pre-release). I'll migrate to that. You can pin a different tag by passing it as an argument: auth0-swift-major-migration ."

If no v3 release exists (the resolver returns empty), stop and tell the user there is no published v3 release to migrate to.


Step 3 — Fetch & Read the v3 SDK Source

Fetch the actual Swift source for the target tag. The signatures here are the authoritative reference for every change made in Step 6.

TAG=   # the version the developer chose in Step 2, e.g. 3.0.0-beta.2

# List all public Swift files in the SDK
curl -s "https://api.github.com/repos/auth0/Auth0.swift/git/trees/${TAG}?recursive=1" \
  | python3 -c "
import sys, json
for item in json.load(sys.stdin).get('tree', []):
    if item['path'].startswith('Auth0/') and item['path'].endswith('.swift'):
        print(item['path'])
"

# Fetch core public API files
for FILE in WebAuth.swift CredentialsManager.swift Authentication.swift \
            Credentials.swift UserProfile.swift Requestable.swift \
            CredentialsStorage.swift CredentialsManagerError.swift WebAuthError.swift; do
    URL="https://raw.githubusercontent.com/auth0/Auth0.swift/${TAG}/Auth0/${FILE}"
    CONTENT=$(curl -sf "$URL")
    [ -n "$CONTENT" ] && echo "=== $FILE ===" && echo "$CONTENT"
done

# MFA files live in a subdirectory
for FILE in MFA/MFAClient.swift MFA/MFAErrors.swift; do
    URL="https://raw.githubusercontent.com/auth0/Auth0.swift/${TAG}/Auth0/${FILE}"
    CONTENT=$(curl -sf "$URL")
    [ -n "$CONTENT" ] && echo "=== $FILE ===" && echo "$CONTENT"
done

Read the fetched source and note:

  • Every public method signature that changed (return type, parameters, throws added)
  • Types that were renamed or removed
  • Protocol requirements that changed
  • Default parameter values that changed

This is the ground truth. Every change in Step 6 must match a real signature in these files.


Step 4 — Audit Which Auth0 APIs the Project Uses

Find all Swift files that import Auth0 — these are the scope of the migration:

grep -rl "import Auth0" --include="*.swift" .

Read every file from that list. Do not grep for specific API patterns — read the full source so you can see exactly how Auth0, webAuth, authentication, credentialsManager, and any Auth0 types are used, including calls with domain/clientId parameters, chained builder calls, and any custom conformances.

For each file, identify:

What to look forWhy it matters
Any call to webAuth(), webAuth(domain:), webAuth(domain:clientId:)§6.1 – clearSession rename; §6.14 – default scope
Any call to .clearSession(§6.1 — rename to logout
Switch/catch on WebAuthError with explicit case names§6.2 — removed and new cases
DispatchQueue.main.async or MainActor.run wrapping an Auth0 callback§6.3 — removable in v3
Any stored Request<…> type annotation (not just chained .start(…))§6.4 — type changed to Requestable
Test mocks conforming to Authentication, MFAClient, or Requestable§6.4 — return type + @MainActor update
Any call to credentialsManager.store(§6.5 — Bool → throws
Any call to credentialsManager.clear() or credentialsManager.clear(forAudience:§6.6 — Bool → throws (both overloads)
Any access to credentialsManager.user (property, not method)§6.7 — replaced by userProfile() method
Any call to credentialsManager.revoke(§6.8 — new error paths
Any type annotation or declaration using UserInfo§6.9 — renamed to UserProfile
Any access to .expiresIn on a Credentials-like object§6.10 — renamed to expiresAt
Any type conforming to CredentialsStorage§6.11 — method signatures changed
Any call to Auth0.users( or Auth0.users(token:§6.12 — Management client removed
login(withOTP:, login(withOOBCode:, login(withRecoveryCode:, multifactorChallenge(§6.13 — MFA methods removed
Any call to webAuth() that does not chain .scope(§6.14 — default scope changed
Any call to credentialsManager.credentials( without explicit minTTL: parameter§6.15 — default minTTL changed from 0 to 60 seconds

Build a checklist: "This project uses: [list]" and "This project does NOT use: [list]". Only work through the §6.x sections that appear in the "uses" list. Skip the rest entirely.


Step 5 — Update the SDK Dependency

Apply only the matching package manager.

Use the `` chosen in Step 2. For stable releases (3.x.y with no suffix), use a range specifier. For pre-releases (3.x.y-beta.z), pin the exact tag — package managers treat pre-release versions as out-of-range for ~> / from: rules.

Swift Package Manager (Package.swift):

// Stable v3 — range specifier picks up all 3.x.y patches
.package(url: "https://github.com/auth0/Auth0.swift", from: "3.0.0")

// Pre-release / specific beta — exact tag required
.package(url: "https://github.com/auth0/Auth0.swift", exact: "3.0.0-beta.2")

Then resolve:

swift package resolve

CocoaPods (Podfile):

# Stable v3
pod 'Auth0', '~> 3.0'

# Pre-release / specific beta — pin the exact version
pod 'Auth0', '3.0.0-beta.2'

Then:

pod update Auth0

Carthage (Cartfile):

# Stable v3
github "auth0/Auth0.swift" ~> 3.0

# Pre-release / specific beta — pin the exact tag
github "auth0/Auth0.swift" "3.0.0-beta.2"

Then:

carthage update Auth0.swift --use-xcframeworks

Xcode-managed SPM (no Package.swift at root):

  • Stable: File → Packages → Update to Latest Package Versions, then verify the version rule is Up to Next Major from 3.0.0.
  • Pre-release / specific beta: File → Packages → Update to Latest Package Versions won't resolve a beta unless the dependency already pins an exact version. Tell the user to change the version rule to Exact Version and enter 3.0.0-beta.2 (or the chosen tag).

Do not build yet — apply all known code changes first.


Step 6 — Apply Breaking Changes

Agent instruction: Work through only the §6.x sections that matched during the Step 4 file-reading audit. Skip every section whose API the project does not use — do not touch those files.

Apply each change exactly as shown. Do not alter surrounding code, rename variables, reformat, or modernise code that isn't being migrated. Match the project's existing style: completion handler → completion handler, async/await → async/await, Combine → Combine.


6.1 — WebAuth.clearSession()WebAuth.logout()

Applies if: Step 4 found any call to .clearSession( in the project's source files.

The clearSession(federated:) method was renamed to logout(federated:). The parameter and its default value are unchanged.

Completion handler:

// v2
Auth0.webAuth().clearSession { result in
    switch result {
    case .success: handleLogoutSuccess()
    case .failure(let error): handleError(error)
    }
}

// v3
Auth0.webAuth().logout { result in
    switch result {
    case .success: handleLogoutSuccess()
    case .failure(let error): handleError(error)
    }
}

async/await:

// v2
try await Auth0.webAuth().clearSession()

// v3
try await Auth0.webAuth().logout()

Combine:

// v2
Auth0.webAuth().clearSession()
    .sink(receiveCompletion: { ... }, receiveValue: { ... })
    .store(in: &cancellables)

// v3
Auth0.webAuth().logout()
    .sink(receiveCompletion: { ... }, receiveValue: { ... })
    .store(in: &cancellables)

With federated: true: The parameter name is the same — just rename the method:

// v2
try await Auth0.webAuth().clearSession(federated: true)

// v3
try await Auth0.webAuth().logout(federated: true)

6.2 — WebAuthError — removed and new cases in exhaustive switch statements

Applies if: Step 4 found any switch or catch on WebAuthError with explicit case names in the project's source files.

Two WebAuthError cases were removed in v3. If the project has an exhaustive switch over WebAuthError (or explicitly matches these cases), the build will fail.

Three new cases were added to surface previously hidden failures.

Removed cases (will no longer compile if matched):

v2 casev3 behaviour
.invalidInvitationURLRemoved — now surfaces as .unknown
.pkceNotAllowedRemoved — now surfaces as .unknown

New cases (can now appear in catch/switch blocks):

v3 caseWhen it fires
.authenticationFailedServer-side failure: wrong password, MFA required, account locked, etc.
.codeExchangeFailedToken exchange failed: network issue, invalid grant, backend error
.credentialsManagerErrorCredentials manager failed to store or clear credentials after login/logout; access the underlying error via .cause

Migration — remove the deleted cases from switch statements:

// v2 — exhaustive switch including cases that no longer exist
Auth0.webAuth().start { result in
    switch result {
    case .success(let credentials):
        handle(credentials)
    case .failure(let error):
        switch error {
        case .userCancelled:
            break  // user dismissed — no action needed
        case .pkceNotAllowed:
            // ❌ compile error in v3 — remove this case
            showConfigError("PKCE not allowed")
        default:
            showError(error)
        }
    }
}

// v3 — remove the deleted cases; handle the new ones where appropriate
Auth0.webAuth().start { result in
    switch result {
    case .success(let credentials):
        handle(credentials)
    case .failure(let error):
        switch error {
        case .userCancelled:
            break  // user dismissed — no action needed
        case .authenticationFailed:
            // server rejected the login — show an appropriate message
            showError("Login failed. Please check your credentials.")
        case .codeExchangeFailed:
            // token exchange failed — network or server issue
            showError("Something went wrong. Please try again.")
        case .credentialsManagerError:
            // login succeeded but credentials could not be stored
            // the user is authenticated in memory but will need to log in again next launch
            // access the underlying error via error.cause (WebAuthError.cause: Error?)
            reportToMonitoring(error.cause)
            showError("Could not save your session.")
        default:
            showError(error)
        }
    }
}

If the project uses async/await and catches specific cases:

// v2
do {
    let credentials = try await Auth0.webAuth().start()
    handle(credentials)
} catch WebAuthError.userCancelled {
    break
} catch WebAuthError.pkceNotAllowed {
    // ❌ compile error in v3 — remove this catch
    showConfigError()
} catch {
    showError(error)
}

// v3 — remove deleted cases; add new ones if the project should handle them
do {
    let credentials = try await Auth0.webAuth().start()
    handle(credentials)
} catch WebAuthError.userCancelled {
    break
} catch WebAuthError.authenticationFailed {
    showError("Login failed. Please check your credentials.")
} catch WebAuthError.codeExchangeFailed {
    showError("Something went wrong. Please try again.")
} catch {
    showError(error)
}

The new cases .authenticationFailed and .codeExchangeFailed are not required to be handled explicitly — a default: branch already catches them. Only add explicit cases if the project wants to show different UI or telemetry for those failures.


6.3 — Remove redundant main-thread dispatch around WebAuth and CredentialsManager callbacks

Applies if: Step 4 found DispatchQueue.main.async or MainActor.run wrapping an Auth0 callback body.

In v3, all completion-handler callbacks, Combine publishers, and async/await methods deliver results on the main thread (they are @MainActor). Wrapping callback bodies in DispatchQueue.main.async { } or await MainActor.run { } is no longer necessary and can be removed.

Completion handler callback — remove the dispatch wrapper:

// v2 — dispatch to main manually
credentialsManager.credentials { result in
    DispatchQueue.main.async {
        switch result {
        case .success(let credentials):
            self.accessToken = credentials.accessToken
            self.isAuthenticated = true
        case .failure(let error):
            self.authError = error
        }
    }
}

// v3 — callback already arrives on main thread
credentialsManager.credentials { result in
    switch result {
    case .success(let credentials):
        self.accessToken = credentials.accessToken
        self.isAuthenticated = true
    case .failure(let error):
        self.authError = error
    }
}

async/await — remove the MainActor.run wrapper:

// v2
let credentials = try await Auth0.webAuth().start()
await MainActor.run {
    self.isAuthenticated = true
}

// v3 — start() is @MainActor; already on main thread after the await
let credentials = try await Auth0.webAuth().start()
self.isAuthenticated = true

Only remove dispatch wrappers that are solely protecting Auth0 callback bodies. If a DispatchQueue.main.async block also dispatches unrelated UI work, remove only what's attributable to the Auth0 callback.


6.4 — Authentication / MFAClient methods return Requestable instead of Request — app code and test mocks

Applies if: Step 4 found either (a) a stored Request<…> type annotation in app code, or (b) test/mock files with types conforming to Authentication, MFAClient, or Requestable.

In v3, all Authentication and MFAClient methods return protocol types rather than the concrete Request struct:

  • Credential-returning methods (login, codeExchange, renew, ssoExchange, etc.) now return any TokenRequestable
  • All other methods (signup, resetPassword, userInfo, jwks, etc.) now return any Requestable

Impact on app code: Call sites that chain directly to .start(_:) — the overwhelming majority — compile without any change. The only app code that breaks is a stored Request<> type annotation:

// v2 — storing the request in a typed variable
let request: Request = Auth0
    .authentication()
    .login(usernameOrEmail: email, password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience, scope: scope)
request.start { result in ... }

// v3 — update the type annotation to the protocol type
// For credential-returning methods:
let request: any TokenRequestable = Auth0
    .authentication()
    .login(usernameOrEmail: email, password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience, scope: scope)
request.start { result in ... }

// For non-credential methods (signup, resetPassword, userInfo, jwks):
let request: any Requestable = Auth0
    .authentication()
    .signup(email: email, password: password, connection: connection)
request.start { result in ... }

// Most common pattern — chaining directly, no annotation needed, no change required:
Auth0.authentication()
    .login(usernameOrEmail: email, password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience, scope: scope)
    .start { result in ... }  // ✅ unchanged

Credential-returning methods that now return any TokenRequestable (full list):

  • login(email:code:audience:scope:)
  • login(phoneNumber:code:audience:scope:)
  • login(usernameOrEmail:password:realmOrConnection:audience:scope:)
  • loginDefaultDirectory(withUsername:password:audience:scope:)
  • login(appleAuthorizationCode:fullName:profile:audience:scope:)
  • login(facebookSessionAccessToken:profile:audience:scope:)
  • login(passkey:challenge:connection:audience:scope:organization:) — two overloads (sign in + sign up with passkey)
  • codeExchange(withCode:codeVerifier:redirectURI:)
  • renew(withRefreshToken:audience:scope:)
  • ssoExchange(withRefreshToken:)
  • customTokenExchange(subjectToken:subjectTokenType:audience:scope:organization:parameters:)
  • MFAClient.verify(otp:mfaToken:), verify(oobCode:bindingCode:mfaToken:), verify(recoveryCode:mfaToken:)

Impact on test targets — custom Authentication mocks:

If the project's test target has a mock or stub conforming to the Authentication or MFAClient protocol, two changes are required:

  1. Return type: Change Request to any TokenRequestable (credential methods) or any Requestable (other methods)
  2. start(_:) callback: Add @MainActor to match the updated Requestable protocol requirement
// v2 — mock Authentication conformance in tests
class MockAuthentication: Authentication {
    var credentialsResult: Result = .failure(.init(info: [:], statusCode: 0))

    func login(usernameOrEmail username: String,
               password: String,
               realmOrConnection realm: String,
               audience: String?,
               scope: String) -> Request {
        // ❌ compile error in v3 — Request is no longer the return type
        return Request(session: URLSession.shared, ...) // v2 internal — no longer works
    }
}

// v2 — mock Requestable used as stub
struct MockRequest: Requestable {
    let result: Result
    func start(_ callback: @escaping (Result) -> Void) {
        // ❌ @MainActor missing — does not conform to v3 Requestable
        callback(result)
    }
}

// v3 — updated mock
struct MockRequest: Requestable {
    let result: Result
    // ✅ Add @MainActor to match the protocol; dispatch via Task to satisfy @MainActor isolation
    func start(_ callback: @escaping @MainActor (Result) -> Void) {
        Task { @MainActor in callback(result) }
    }
}

// v3 — updated Authentication mock returning the correct protocol type
class MockAuthentication: Authentication {
    var credentialsResult: Result = .failure(.init(info: [:], statusCode: 0))

    func login(usernameOrEmail username: String,
               password: String,
               realmOrConnection realm: String,
               audience: String?,
               scope: String) -> any TokenRequestable {
        // ✅ Return MockTokenRequest, not Request
        return MockTokenRequest(result: credentialsResult)
    }
}

// v3 — TokenRequestable mock (for credential-returning methods)
struct MockTokenRequest: TokenRequestable {
    typealias ResultType = T
    typealias ErrorType = E

    let result: Result

    func start(_ callback: @escaping @MainActor (Result) -> Void) {
        Task { @MainActor in callback(result) }
    }

    // TokenRequestable adds these claim-validation builder methods — return self
    func validateClaims() -> any TokenRequestable { self }
    func withLeeway(_ leeway: Int) -> any TokenRequestable { self }
    func withIssuer(_ issuer: String) -> any TokenRequestable { self }
    func withNonce(_ nonce: String?) -> any TokenRequestable { self }
    func withMaxAge(_ maxAge: Int?) -> any TokenRequestable { self }
    func withOrganization(_ organization: String?) -> any TokenRequestable { self }
}

The MockTokenRequest stub above stubs out all TokenRequestable builder methods by returning self. In most tests, validateClaims() and the with* modifiers are never called, so returning self is correct. If a specific test verifies claim validation behaviour, implement those methods properly.


6.5 — CredentialsManager.store(credentials:) — Bool return → throws

Applies if: Step 4 found any call to credentialsManager.store(credentials: in the project's source files.

store(credentials:) previously returned Bool. In v3 it throws on failure and returns Void on success.

If the project checked the return value:

// v2
if credentialsManager.store(credentials: credentials) {
    print("Stored successfully")
} else {
    print("Store failed")
}

// v3 — use do-catch; map the error into the project's existing error handler
do {
    try credentialsManager.store(credentials: credentials)
} catch {
    // replace with whatever logging/error handling the project already uses
    handleError(error)
}

If the project discarded the return value:

// v2 — silently discarded
_ = credentialsManager.store(credentials: credentials)

// v3 — try? discards the error the same way; use if the project didn't handle failures before
try? credentialsManager.store(credentials: credentials)

Prefer do-catch over try? when the project has an error-handling pattern to route into. Use try? only to preserve intentional silent-discard behaviour.


6.6 — CredentialsManager.clear() and clear(forAudience:scope:) — Bool return → throws

Applies if: Step 4 found any call to credentialsManager.clear() or credentialsManager.clear(forAudience: in the project's source files.

Both overloads previously returned Bool. In v3 both throw:

  • clear() throws — clears the main stored credentials
  • clear(forAudience:scope:) throws — clears API credentials for a specific audience
// v2
_ = credentialsManager.clear()
_ = credentialsManager.clear(forAudience: "https://api.example.com")

// v3
try? credentialsManager.clear()
try? credentialsManager.clear(forAudience: "https://api.example.com")
// or, if the project handles errors:
do {
    try credentialsManager.clear()
} catch {
    handleError(error)
}

6.7 — CredentialsManager.user property → userProfile() throwing method

Applies if: Step 4 found any access to credentialsManager.user as a property (not a method call) in the project's source files.

The user: UserInfo? computed property was replaced by userProfile() throws -> UserProfile? (see also §6.9 for the type rename).

// v2 — property access, returns UserInfo?
func currentUser() -> UserInfo? {
    return credentialsManager.user
}

// v3 — method call that throws, returns UserProfile?
func currentUser() -> UserProfile? {
    return try? credentialsManager.userProfile()
}

// v3 — if the project needs to surface errors:
func loadUser() throws {
    let profile = try credentialsManager.userProfile()
    self.userProfile = profile
}

6.8 — CredentialsManager async methods — new error paths from throwing storage

Applies if: Step 4 found any call to credentialsManager.revoke( in the project's source files.

Because CredentialsManager storage methods now throw, several async methods gain new failure paths that were previously silently swallowed. The most significant is revoke(). Only update error-handling code that the project actually writes — call sites that already use a default: branch need no change.

New errors that can now surface from revoke():

New errorWhen it firesWhat to do
.noCredentialsgetEntry threw — no credentials in storage, nothing to revokeTreat as already logged out; navigate to login
.revokeFailedNetwork call to revoke the refresh token failedThe token may still be active on the server; show an error
.clearFailedRevocation succeeded but Keychain delete failedTreat as logged out — the token is no longer valid server-side
// v2 — only .revokeFailed was possible; missing credentials returned .success silently
credentialsManager.revoke { result in
    switch result {
    case .success:
        navigateToLogin()
    case .failure(let error):
        showError(error)  // only .revokeFailed reached here
    }
}

// v3 — new cases surface; update the switch if the project checks specific cases
credentialsManager.revoke { result in
    switch result {
    case .success:
        navigateToLogin()
    case .failure(let error):
        switch error {
        case .noCredentials:
            // nothing was stored — already effectively logged out
            navigateToLogin()
        case .revokeFailed:
            // server revocation failed — refresh token may still be active
            showError("Could not revoke your session. Please try again.")
        case .clearFailed:
            // token revoked server-side but Keychain delete failed
            // treat as logged out — token is no longer valid
            navigateToLogin()
        default:
            showError(error)
        }
    }
}

New errors that can now surface from credentials(), renew(), apiCredentials(), ssoCredentials():

New errorWhen it fires
.noCredentialsgetEntry throws (e.g., Keychain item not found) — previously swallowed by try?
.renewFailedRefresh token renewal request failed — network error, invalid/expired refresh token
.storeFailedKeychain write fails when saving renewed credentials

These only matter if the project's existing catch/failure handler needs to distinguish these cases. If it uses a generic fallback, no change is needed.

// v3 — if the project wants to distinguish storage failures from network failures:
credentialsManager.credentials { result in
    switch result {
    case .success(let credentials):
        use(credentials)
    case .failure(let error):
        switch error {
        case .noCredentials, .renewFailed:
            // credentials missing or refresh failed — force re-login
            navigateToLogin()
        case .storeFailed:
            // renewed successfully but couldn't save — credentials valid in memory this session
            // user will be asked to log in again on next launch
            reportToMonitoring(error)
            use(/* last known credentials if available */)
        default:
            showError(error)
        }
    }
}

Only add these new case branches if the project currently has a switch on CredentialsManagerError that would benefit from handling them differently. A default: branch already handles them correctly without any change.


6.9 — UserInfoUserProfile type rename

Applies if: Step 4 found any type annotation, function signature, or variable declaration referencing UserInfo in the project's source files.

The UserInfo type was renamed to UserProfile. Update every type annotation, function signature, and variable declaration that references UserInfo.

// v2
var currentUser: UserInfo?
func showProfile(_ profile: UserInfo) { ... }
func fetchUser() -> UserInfo? { ... }

// v3
var currentUser: UserProfile?
func showProfile(_ profile: UserProfile) { ... }
func fetchUser() -> UserProfile? { ... }

If the project calls Auth0.authentication().userInfo(withAccessToken:), the method name is unchanged but the return type changed:

// v2 — returns Request
Auth0.authentication()
    .userInfo(withAccessToken: accessToken)
    .start { (result: Result) in ... }

// v3 — returns Request
Auth0.authentication()
    .userInfo(withAccessToken: accessToken)
    .start { (result: Result) in ... }

6.10 — Credentials.expiresInCredentials.expiresAt

Applies if: Step 4 found any access to .expiresIn on a Credentials, APICredentials, or SSOCredentials object.

The expiresIn: Date property on Credentials, APICredentials, and SSOCredentials was renamed to expiresAt: Date. The underlying JSON key is unchanged; only the Swift property name changed.

// v2
let expiry: Date = credentials.expiresIn

// v3
let expiry: Date = credentials.expiresAt

6.11 — CredentialsStorage custom implementation — methods now throw

Applies if: Step 4 found a type conforming to CredentialsStorage in the project's source files. Skip if the project only passes a SimpleKeychain instance — the default storage needs no change.

Only applies if the project provides a custom CredentialsStorage implementation (i.e., a type conforming to the protocol — not just using the default SimpleKeychain). Skip if the project only passes a SimpleKeychain instance.

The protocol changed from Bool/Data? returns to throwing methods, and added a new required deleteAllEntries().

// v2 — protocol conformance
final class AppKeychain: CredentialsStorage {
    func getEntry(forKey key: String) -> Data? {
        return Keychain.shared.read(key: key)
    }

    func setEntry(_ data: Data, forKey key: String) -> Bool {
        return Keychain.shared.write(data, forKey: key)
    }

    func deleteEntry(forKey key: String) -> Bool {
        return Keychain.shared.delete(key: key)
    }
}

// v3 — methods throw; deleteAllEntries() required
final class AppKeychain: CredentialsStorage {
    func getEntry(forKey key: String) throws -> Data {
        guard let data = Keychain.shared.read(key: key) else {
            throw CredentialsManagerError.noCredentials
        }
        return data
    }

    func setEntry(_ data: Data, forKey key: String) throws {
        guard Keychain.shared.write(data, forKey: key) else {
            throw CredentialsManagerError.storeFailed
        }
    }

    func deleteEntry(forKey key: String) throws {
        guard Keychain.shared.delete(key: key) else {
            throw CredentialsManagerError.revokeFailed
        }
    }

    func deleteAllEntries() throws {
        Keychain.shared.deleteAll()
    }
}

The CredentialsStorage protocol declares its methods as throws with no specific error type — you can throw any Error. The example above uses CredentialsManagerError cases for illustration only; your implementation should throw an error type that makes sense for your storage backend. Verify the CredentialsManagerError case names in the SDK source fetched in Step 3 if you choose to reuse them.


6.12 — Management client removed

Applies if: Step 4 found any call to Auth0.users( or Auth0.users(token: in the project's source files.

Auth0.users(token:) and the entire Users management client were removed from the SDK in v3. Do not silently delete any call sites — add a TODO comment and surface this in the migration summary.

// v2 — direct Management API call in the app
Auth0
    .users(token: managementToken)
    .patch(userId, userPatch: UserPatchAttributes(name: newName))
    .start { result in
        switch result {
        case .success: print("Updated")
        case .failure(let error): print(error)
        }
    }

// v3 — Management client removed; add TODO and preserve intent
// TODO: Auth0.swift v3 removed the Management client.
// Replace this with a call to your own backend endpoint, which
// calls the Auth0 Management API using a machine-to-machine token.
// NEVER embed a Management API token in the client app.
// See: https://auth0.com/docs/secure/tokens/access-tokens/management-api-access-tokens

This requires backend work — record it in the Step 9 summary.


6.13 — MFA methods removed from Authentication → migrate to MFAClient

Applies if: Step 4 found any call to login(withOTP:, login(withOOBCode:, login(withRecoveryCode:, or multifactorChallenge( — or test mocks conforming to MFAClient — in the project's source files.

The four MFA methods on the Authentication protocol were removed in v3. They are replaced by the dedicated MFAClient protocol, accessible via Auth0.mfa():

v2 (Authentication)v3 (MFAClient)
authentication().login(withOTP: otp, mfaToken: token)mfa().verify(otp: otp, mfaToken: token)
authentication().login(withOOBCode: code, mfaToken: token, bindingCode: binding)mfa().verify(oobCode: code, bindingCode: binding, mfaToken: token)
authentication().login(withRecoveryCode: code, mfaToken: token)mfa().verify(recoveryCode: code, mfaToken: token)
authentication().multifactorChallenge(mfaToken: token, types: types, authenticatorId: id)mfa().challenge(with: id, mfaToken: token)

The mfaToken itself still comes from the same place — an AuthenticationError where error.isMultifactorRequired == true returns the token via error.mfaRequiredErrorPayload?.mfaToken.


OTP (TOTP authenticator app):

// v2
Auth0.authentication()
    .login(withOTP: otpCode, mfaToken: mfaToken)
    .start { result in
        switch result {
        case .success(let credentials): storeCredentials(credentials)
        case .failure(let error): showError(error)
        }
    }

// v3 — verify returns any TokenRequestable
Auth0.mfa()
    .verify(otp: otpCode, mfaToken: mfaToken)
    .start { result in
        switch result {
        case .success(let credentials): storeCredentials(credentials)
        case .failure(let error): showError(error)
        }
    }

// async/await
let credentials = try await Auth0.mfa().verify(otp: otpCode, mfaToken: mfaToken).start()

OOB (SMS / email code):

// v2
Auth0.authentication()
    .login(withOOBCode: oobCode, mfaToken: mfaToken, bindingCode: bindingCode)
    .start { result in ... }

// v3 — parameter order changed: oobCode first, bindingCode second
Auth0.mfa()
    .verify(oobCode: oobCode, bindingCode: bindingCode, mfaToken: mfaToken)
    .start { result in ... }

Recovery code:

// v2
Auth0.authentication()
    .login(withRecoveryCode: recoveryCode, mfaToken: mfaToken)
    .start { result in ... }

// v3
Auth0.mfa()
    .verify(recoveryCode: recoveryCode, mfaToken: mfaToken)
    .start { result in ... }

MFA challenge (request an OOB code to be sent):

// v2
Auth0.authentication()
    .multifactorChallenge(mfaToken: mfaToken,
                          types: ["oob"],
                          authenticatorId: authenticatorId)
    .start { result in ... }

// v3 — types parameter removed; pass authenticatorId directly
Auth0.mfa()
    .challenge(with: authenticatorId, mfaToken: mfaToken)
    .start { result in ... }

Handling the MFA required error to obtain the mfaToken (unchanged between v2 and v3):

Auth0.authentication()
    .login(usernameOrEmail: email,
           password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience,
           scope: scope)
    .start { result in
        switch result {
        case .success(let credentials):
            storeCredentials(credentials)
        case .failure(let error) where error.isMultifactorRequired:
            // mfaToken extracted the same way in both v2 and v3
            if let mfaToken = error.mfaRequiredErrorPayload?.mfaToken {
                presentMFAChallenge(mfaToken: mfaToken)
            }
        case .failure(let error):
            showError(error)
        }
    }

Error type changed: AuthenticationErrorMFAVerifyError

The verify methods on MFAClient return any TokenRequestable. If the project previously matched specific AuthenticationError cases in MFA failure handlers, map them onto MFAVerifyError:

// v2 — MFA failures came as Authentic

Related skills

Use when upgrading an Android app's Auth0 SDK (com.auth0.android:auth0) to the next major version. Detects the current version, checks prerequisites, and app...

1 installs

Use when adding, fixing, or improving authentication in any app — login, logout, signup, route protection, JWT/access token validation, refresh token rotatio...

1 installs1 stars

Build native macOS apps with Swift 6.3, SwiftUI, SwiftData, concurrency, and on-device AI.

19 installs

Auth0 Management (auth0.com). Use this skill for ANY Auth0 Management request — reading, creating, updating, and deleting data. Whenever a task involves Auth...

Deprecated — use the `auth0` skill instead (run `npx clawhub install auth0`). Use when adding Auth0 login, logout, protected routes, or user sessions to a React SPA. Integrates @auth0/auth0-react — use even if the user says "add login to my React app" or "protect my React routes" without naming the

19 installs

Use when migrating iOS projects from CocoaPods to Swift Package Manager, or when planning a migration strategy for enterprise iOS dependencies. Covers depend...