> ## Documentation Index
> Fetch the complete documentation index at: https://rain-sandbox-trial.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication Options

> Choose how users authenticate to their Rain wallet and authorize wallet activity.


Rain supports two authentication methods: one time codes sent by email or SMS, and passkeys. Both methods can be associated with the same wallet, so adding another authentication method does not create a new wallet.

For most integrations, we recommend starting with email authentication. It requires minimal setup, works across devices, and provides a straightforward recovery path. You can also offer passkeys for users who want faster authentication and stronger phishing resistance.

If passkeys are the primary authentication method, require users to add an email address as a backup before funding their wallet.

## Choosing an authentication method

| | One-time code (email or SMS) | Passkey |
| - | - | - |
| Best For | Simple onboarding and broad accessibility | Faster authentication and stronger phishing resistance |
| User experience | Enter a 6-digit code sent by email or SMS | Authenticate using Face ID, Touch ID, device PIN, or another supported passkey method |
| Platforms | iOS and Android | iOS and Android |
| Setup on your side | No additional app configuration | Configure your app and associated domain for passkeys |
| Using a new device | Authenticate using the same email address or phone number | Availability depends on the user’s passkey provider and whether the passkey is synced |
| Recovery | Regain access through the associated email address or phone number | Use a backup authentication method |
| Recommended configuration | Use email as the default authentication and backup method | Use with email as a backup method |

For most applications, enable email authentication and optionally offer passkeys. If you use passkeys as the primary authentication method, require users to enroll an email address as a backup before they fund their wallet.

## Accounts and identity

Rules that shape your login screens:

* **Email and phone number identify the account.** The first sign up with an email address or phone number creates an account. Future logins with that same contact return the user to the same wallet. If the same person signs up separately with email and SMS, Rain treats them as separate accounts unless one contact is added to the existing account.
* **Accounts are not automatically merged.** If a user creates two accounts, they will have two separate wallets. Returning users should use **Log in** or **Add a passkey** rather than **Create account**.
* **Passkey sign up creates an account without a contact method.** `signUpWithPasskey` creates an account whose initial authentication method is the passkey. Prompt the user to add an email address as a backup immediately after sign up before funding the wallet. See [Backup and recovery](/docs/embedded-wallets/backup-and-recovery).
* **The same account works across supported platforms.** A user who signs up on iOS can log in on Android with an authentication method already associated with their account and access the same wallet.

## Logging in with a one-time code

<Steps>
  <Step title="Send the code">
    Call `sendLoginCode` with the contact. Rain sends a 6-digit code. Calling again for the same contact replaces the pending code, which is your **Resend** button.

    <CodeGroup>
      ```swift iOS theme={null}
      try await wallet.sendLoginCode(to: .email("user@example.com"))
      try await wallet.sendLoginCode(to: .phone("+15551234567"))
      ```

      ```kotlin Android theme={null}
      wallet.sendLoginCode(RainWalletContact.Email("user@example.com"))
      wallet.sendLoginCode(RainWalletContact.Phone("+15551234567"))
      ```
    </CodeGroup>

    Phone numbers must be in international (E.164) format. Validation failures throw `RAIN_102` before any request is made.
  </Step>

  <Step title="Confirm the code">
    Call `confirmLoginCode` with what the user typed. Success logs the user in; on a first login it also creates the wallet. A wrong code throws `RAIN_203` and leaves the pending challenge intact, so the user retries without a new code. Codes expire after 5 minutes and lock after 3 wrong attempts; after either, send a new one.

    <CodeGroup>
      ```swift iOS theme={null}
      do {
          try await wallet.confirmLoginCode(code)
      } catch let error as RainError where error == .invalidLoginCode {
          showInlineError("That code didn't work. Check it and try again.")
      }
      ```

      ```kotlin Android theme={null}
      try {
          wallet.confirmLoginCode(code)
      } catch (e: RainError.InvalidLoginCode) {
          showInlineError("That code didn't work. Check it and try again.")
      }
      ```
    </CodeGroup>
  </Step>
</Steps>

## Logging in with a passkey

Passkeys are available on iOS and Android. A passkey created on one platform is bound to your domain and your app, so the same account can hold passkeys from both.

<Steps>
  <Step title="Set up your domain">
    Choose a domain you'll keep. Passkeys are scoped to it: users' passkeys won't work if you change it later, and the same account can hold passkeys from more than one domain. Rain runs no shared domain; you host the association files yourself.

    * **iOS:** Host `https://<domain>/.well-known/apple-app-site-association` listing your app under `webcredentials`, and add `webcredentials:<domain>` to your app's Associated Domains.
    * **Android:** Host `https://<domain>/.well-known/assetlinks.json` with two statements: one for the site itself granting `delegate_permission/common.get_login_creds`, and an `android_app` statement granting both `delegate_permission/common.handle_all_urls` and `delegate_permission/common.get_login_creds` that lists your package name and the SHA-256 fingerprint of every certificate that signs your app (debug, upload, and Play App Signing). The device refuses a file that carries the app statement alone.

    Then pass the domain to the SDK:

    <CodeGroup>
      ```swift iOS theme={null}
      let wallet = RainProvider(RainWalletConfig(passkeyDomain: "example.com"))
      ```

      ```kotlin Android theme={null}
      val wallet = RainProvider(application, RainWalletConfig(passkeyDomain = "example.com"))
      ```
    </CodeGroup>

    The domain is applied once per app launch. Without it, every passkey call throws `RAIN_102`. On Android, a value that isn't a registrable domain of at least two labels (a scheme, port, path, or `localhost`) throws `RAIN_102` from the constructor.
  </Step>

  <Step title="Offer the right action">
    Three calls cover the flows. On iOS each takes an `anchor`, the window the system sheet presents from; on Android each takes the foreground `Activity` and suspends until the sheet closes.

    <CodeGroup>
      ```swift iOS theme={null}
      try await wallet.signUpWithPasskey(anchor: window)   // brand-new user: new account + new wallet
      try await wallet.loginWithPasskey(anchor: window)    // returning user with a passkey
      try await wallet.addPasskey(anchor: window)          // logged-in user (by code) adds a passkey
      ```

      ```kotlin Android theme={null}
      wallet.signUpWithPasskey(activity)   // brand-new user: new account + new wallet
      wallet.loginWithPasskey(activity)    // returning user with a passkey
      wallet.addPasskey(activity)          // logged-in user (by code) adds a passkey
      ```
    </CodeGroup>

    Show **Sign in with passkey** and **Create account** as distinct choices, and put **Add a passkey** inside the app for users who logged in with a code. Call `logout()` before starting a passkey sign-up while a session is live: the SDK refuses with `RAIN_102` otherwise. On iOS the same applies to a passkey login over a live passkey session; on Android a passkey login replaces the current session. A dismissed sheet throws `RAIN_401` and leaves the current session untouched.
  </Step>

  <Step title="Ask for a backup contact">
    A passkey-only account is one lost device away from being unreachable if the passkey isn't synced. After sign-up, collect an email address and verify it with `sendContactVerificationCode` and `confirmContactVerification`. The verified contact becomes a login method for the account. Details in [Backup and recovery](/docs/embedded-wallets/backup-and-recovery).
  </Step>
</Steps>

## Sessions

A successful login stores a session in the device's secure storage. The SDK manages it from there:

* **Restore.** On launch, `awaitSessionRestore()` waits (up to 5 seconds) for the stored session to load. Then `hasActiveSession()` tells you whether to show the login screen.
* **Observe.** `authState` reports `loading`, `authenticated`, or `unauthenticated`; bind your login gate to it. A finer `sessionState` adds `active(expiresAt)` and `expired` if you want to show a countdown or pre-empt expiry.
* **Refresh.** Sessions refresh automatically ahead of expiry (`RainWalletSessionPolicy`: 60-second buffer, two transient retries with backoff). Reads are retried on transient failures; sends never are. Call `refreshSession()` to force one, for example when the app returns to the foreground.
* **Expiry.** When a session can't be refreshed, or the user logged in on another device, the SDK fires `onSessionExpired` once and calls throw `RAIN_201` until the user logs in again. Ending a session with `logout()` doesn't fire the hook.
* **One device at a time.** A login ends the user's sessions on other devices. The signed-out device finds out on its next call.

<Warning>
  `onSessionExpired` is called from a background context. Switch to the main thread before touching UI, and don't capture short-lived objects in it: the provider holds the hook for its whole life.
</Warning>

## What's next

<Columns cols={2}>
  <Card title="Wallet creation" icon="wallet" href="/docs/embedded-wallets/wallet-creation">
    What a first login creates, and what to do with the addresses.
  </Card>

  <Card title="Backup and recovery" icon="life-ring" href="/docs/embedded-wallets/backup-and-recovery">
    Keep every account reachable.
  </Card>
</Columns>
