> ## Documentation Index
> Fetch the complete documentation index at: https://docs.portalhq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# PortalAuth

> Client for Portal-managed end user authentication: magic links, Google and Apple sign-in, two-factor codes and session persistence.

**Class Definition**

```swift theme={null}
public final class PortalAuth
```

`PortalAuth` drives Portal's Client Auth APIs and resolves a `PortalSession` that you pass to `Portal(credentials:)`. It shares no state with `Portal`: it never touches wallets, MPC or signing, and your app owns all UI, redirect forwarding and wallet creation.

**Initializer**

```swift theme={null}
public convenience init(
    authEnvironmentId: String,
    redirectUrl: String,
    apiHost: String = "api.portalhq.io",
    magicLink: MagicLinkConfig? = nil,
    isAccountAbstracted: Bool? = nil
) throws
```

**Parameters**

* `authEnvironmentId`: The Auth Environment ID from **Authentication > Configure** in the Portal dashboard. Sent as the `x-portal-auth-environment-id` header on every request, and used as the Keychain key for the persisted session, so two instances built with the same ID share one session.
* `redirectUrl`: Where Portal sends the user once a magic link or provider sign-in completes. Must appear byte-for-byte on the environment's Redirect URLs allow list and must match the URL scheme (or Universal Link) your app registers. `signInWithGoogle` / `signInWithApple` additionally require a custom scheme such as `myapp://auth/callback`.
* `apiHost`: Optional Portal API host override. `localhost` and `127.0.0.1` use `http://`.
* `magicLink`: The `fromEmail` and `templateId` that `sendMagicLink` requires. Omit it in an OAuth-only app.
* `isAccountAbstracted`: Whether the client Portal creates for a first-time user uses [gas sponsorship](../../../resources/authentication/overview#gas-sponsorship). Omitted from every request when `nil`. Only takes effect on a user's first sign-in.

**Throws**

* `PortalAuthError.invalidArgument(name:)`: `authEnvironmentId` or `redirectUrl` is empty or blank. The initializer performs no network or Keychain I/O.

**Properties and configuration**

* `prefersEphemeralWebBrowserSession: Bool`: When `true`, `signInWith*` opens a browser sheet that does not share Safari's cookies, so an existing Google or Apple sign-in is not reused. Defaults to `false`. Set it to `true` to let a user who signed out pick a different account.
* `setAuthPresentationAnchor(_ anchor: ASPresentationAnchor)`: The window `signInWith*` presents the browser sheet from. Held weakly. Required before calling `signInWithGoogle` or `signInWithApple`.

**Methods**

| Method | Purpose |
| - | - |
| [`getMethods()`](./portalauthgetmethods) | Which sign-in methods the environment has enabled, plus the `autoCreateWallet` hint. |
| [`sendMagicLink(_:)`](./portalauthsendmagiclink) | Email the user a sign-in link. |
| [`loginWithGoogle()`](./portalauthloginwithgoogle) / [`loginWithApple()`](./portalauthloginwithapple) | Fetch a provider authorize URL for you to open. |
| [`signInWithGoogle()`](./portalauthsigninwithgoogle) / [`signInWithApple()`](./portalauthsigninwithapple) | Complete a provider sign-in in an `ASWebAuthenticationSession` sheet. |
| [`handleRedirect(_:)`](./portalauthhandleredirect) | Exchange the grant on an inbound redirect URL for an `AuthResult`. |
| [`verifyTotp(_:userJwt:)`](./portalauthverifytotp) | Submit a two-factor code. |
| [`restoreSession()`](./portalauthrestoresession) | Rebuild the persisted session on launch. |
| [`clearPersistedSession()`](./portalauthclearpersistedsession) | Delete the persisted session. |

**Types**

```swift theme={null}
public struct MagicLinkConfig: Equatable {
    public let fromEmail: String
    public let templateId: String
}

public enum AuthMethod: String, Codable, CaseIterable {
    case emailMagicLink = "EMAIL_MAGIC_LINK"
    case google = "GOOGLE"
    case apple = "APPLE"
}

public struct AuthMethodsResult: Equatable {
    public let allowedAuthMethods: [AuthMethod]
    public let autoCreateWallet: Bool
}

public struct AuthorizeUrlResult: Equatable {
    public let authorizeUrl: String
}

public enum AuthResult: Sendable {
    case authenticated(AuthenticatedResult)
    case totpRequired(TotpRequiredResult)
}

public struct AuthenticatedResult: Sendable {
    public let session: PortalSession
    public let clientId: String?
    public let isAccountAbstracted: Bool?
}

public struct TotpRequiredResult: Equatable, Sendable {
    public let userJwt: String
    public let totpLink: String?
    public let endUserId: String
}

public protocol PortalSession: PortalCredentials {
    var endUserId: String { get }
}
```

`PortalSession` is a `PortalCredentials`: `getToken()` returns the Client Session Token until the session is invalidated, then throws `PortalCredentialError.sessionInvalidated`; `invalidate()` clears the in-memory token and deletes the persisted copy. `endUserId` is never secret and is the identifier to log and to key per-user state on.

**Errors**

`PortalAuthError` covers the authentication flow itself:

| Case | When |
| - | - |
| `invalidArgument(name:)` | A required argument (`authEnvironmentId`, `redirectUrl`, `email`) was blank. |
| `magicLinkNotConfigured` | `sendMagicLink` was called on an instance built without `magicLink`. |
| `authMethodUnavailable(AuthMethod)` | A Google or Apple sign-in was started but the provider is not enabled for the environment. |
| `authenticationFailed(error:)` | The redirect carried an `error` parameter, for example `oauth_failed`. |
| `malformedResponse(path:missing:)` | A response did not carry the documented field. |
| `invalidGrantResponse` | The exchange returned neither a session token nor a `userJwt`. |
| `invalidUserJwt(detail:)` | The `userJwt` passed to `verifyTotp` could not be read. |
| `sessionStorageFailure(message:)` | The Keychain could not be read, written or deleted this time. |
| `accountAbstractionUnavailable(message:)` | `isAccountAbstracted: true` was requested but gas sponsorship is not enabled or configured. |
| `rateLimited` | The magic-link send limit for this address was hit (`429`). |
| `totpQrUnavailable` | A TOTP QR code could not be generated from `totpLink`. |

`PortalAuthSignInError` is specific to `signInWithGoogle` / `signInWithApple`. Its raw values are the Web SDK's popup codes: `closed` (`POPUP_CLOSED`), `unavailable` (`POPUP_UNAVAILABLE`), `signInInProgress` (`SIGN_IN_ALREADY_IN_PROGRESS`) and `callbackIncomplete` (`CALLBACK_INCOMPLETE`).

Transport failures are not remapped. A `401` from any `PortalAuth` call surfaces as `PortalRequestsError.unauthorized`, unchanged; see [Handle errors](../guide/client-auth#handle-errors) for what it means on each call.

**Notes**

* Hold one long-lived instance. `handleRedirect` remembers the last grant it exchanged so a redirect delivered twice resolves to the same result; that memory lives on the instance.
* The persisted session is stored in the Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`, keyed by `authEnvironmentId`, in a namespace separate from wallet shares. It never syncs through iCloud Keychain, and it survives app deletion.
* Available starting from SDK version 8.0.0.

**Example Usage**

```swift theme={null}
import PortalSwift

do {
    let auth = try PortalAuth(
        authEnvironmentId: "YOUR_AUTH_ENVIRONMENT_ID",
        redirectUrl: "myapp://auth/callback",
        magicLink: MagicLinkConfig(
            fromEmail: "hello@auth.example.com",
            templateId: "YOUR_TEMPLATE_ID"
        )
    )

    let methods = try await auth.getMethods()
    print("Enabled methods: \(methods.allowedAuthMethods.map(\.rawValue))")
} catch PortalAuthError.invalidArgument(let name) {
    print("Missing configuration value: \(name)")
} catch {
    print("Error creating PortalAuth: \(error)")
}
```

**Related Documentation**

* [Client Auth](../guide/client-auth)
* [Authentication overview](../../../resources/authentication/overview)
* [Enable authentication](../../../resources/authentication/enable-authentication)
