From 12648da1193722451cd61affbbc8ff1255bbde4e Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Mon, 4 May 2026 17:54:12 +0100 Subject: [PATCH 1/5] docs: Update Apple Sign-In documentation with enhanced setup, customizations, and troubleshooting --- .../04-providers/04-apple/01-setup.md | 202 +++++++----------- ...-configuration.md => 02-customizations.md} | 96 +++++---- .../04-apple/04-troubleshooting.md | 80 ++++++- .../providers/apple/3-button.png | Bin 13544 -> 17294 bytes 4 files changed, 201 insertions(+), 177 deletions(-) rename docs/06-concepts/11-authentication/04-providers/04-apple/{02-configuration.md => 02-customizations.md} (60%) diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md b/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md index 9939f1a7..f840eae0 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md @@ -1,20 +1,10 @@ # Setup -Sign-in with Apple requires that you have a subscription to the [Apple Developer Program](https://developer.apple.com/programs/), even if you only want to test the feature in development mode. - -:::caution -You need to install the auth module before you continue, see [Setup](../../setup). -::: +Sign in with Apple requires a subscription to the [Apple Developer Program](https://developer.apple.com/programs/), even for development and testing. ## Get your credentials -All platforms require an App ID. Android and Web additionally require a Service ID. - -| Platform | App ID | Service ID | Xcode capability | Android intent filter | -| --- | --- | --- | --- | --- | -| iOS / macOS | Required | Not needed | Required | — | -| Android | Required | Required | — | Required | -| Web | Required | Required | — | — | +All platforms require an App ID and a Sign in with Apple key. Android and Web additionally require a Service ID. ### Register your App ID @@ -85,10 +75,11 @@ Each primary App ID can have a maximum of two private keys. If you reach the lim ### Store your credentials -Add the credentials to `config/passwords.yaml`: +Your server's `config/passwords.yaml` already has `development:`, `staging:`, and `production:` sections from the project template. Add the Apple credentials to the `development:` section: ```yaml development: + # ... existing keys (database, redis, serviceSecret, etc.) ... appleServiceIdentifier: 'com.example.service' appleBundleIdentifier: 'com.example.app' appleRedirectUri: 'https://example.com/auth/callback' @@ -104,88 +95,58 @@ development: appleAndroidPackageIdentifier: 'com.example.app' ``` -:::warning -**Never commit your `.p8` key to version control.** Use environment variables or a secrets manager in production. - -**Paste the raw `.p8` key contents** — the full text including `-----BEGIN PRIVATE KEY-----` and `-----END PRIVATE KEY-----`. Do not pre-generate a JWT from it. Serverpod generates the client secret JWT internally on every request. +For production, add the same keys to the `production:` section of `passwords.yaml`, or set the corresponding `SERVERPOD_PASSWORD_` environment variables on your production server. -**Carefully maintain correct indentation for YAML block scalars.** The `appleKey` block uses a `|`; any indentation error will silently break the key, resulting in authentication failures without helpful error messages. +:::tip +Paste the raw `.p8` file contents as-is. Do not pre-generate a JWT -- Serverpod handles that internally. If sign-in fails, see the [troubleshooting guide](./troubleshooting). +::: ## Server-side configuration -After creating your credentials, you need to configure the Apple identity provider on your main `server.dart` file by setting the `AppleIdpConfig` as a `identityProviderBuilders` in your `pod.initializeAuthServices()` configuration: +### Add the Apple identity provider + +Your server's `server.dart` file (e.g., `my_project_server/lib/server.dart`) should already contain a `pod.initializeAuthServices()` call if your project was created with the Serverpod project template (`serverpod create`). If it's not there, see [Setup](../../setup) first to configure the auth module and JWT settings. + +Add the Apple import and `AppleIdpConfigFromPasswords()` to the existing `identityProviderBuilders` list: ```dart -import 'package:serverpod/serverpod.dart'; -import 'package:serverpod_auth_idp_server/core.dart'; import 'package:serverpod_auth_idp_server/providers/apple.dart'; +``` -void run(List args) async { - final pod = Serverpod( - args, - Protocol(), - Endpoints(), - ); - - // Configure Apple identity provider - pod.initializeAuthServices( - tokenManagerBuilders: [ - JwtConfigFromPasswords(), - ], - identityProviderBuilders: [ - AppleIdpConfig( - serviceIdentifier: pod.getPassword('appleServiceIdentifier')!, - bundleIdentifier: pod.getPassword('appleBundleIdentifier')!, - redirectUri: pod.getPassword('appleRedirectUri')!, - teamId: pod.getPassword('appleTeamId')!, - keyId: pod.getPassword('appleKeyId')!, - key: pod.getPassword('appleKey')!, - // Optional: Required only for Web support when using server callback route. - webRedirectUri: pod.getPassword('appleWebRedirectUri'), - // Optional: Required only for Android support. - androidPackageIdentifier: pod.getPassword('appleAndroidPackageIdentifier'), - ), - ], - ); - - // Configure web routes for Apple Sign-In - // Paths must match paths configured on Apple's developer portal. - // The method's parameters are optional and defaults to the values below. - pod.configureAppleIdpRoutes( - revokedNotificationRoutePath: '/hooks/apple-notification', - webAuthenticationCallbackRoutePath: '/auth/callback', - ); - - await pod.start(); -} +```dart +pod.initializeAuthServices( + tokenManagerBuilders: [ + JwtConfigFromPasswords(), + ], + identityProviderBuilders: [ + // ... any existing providers (e.g., EmailIdpConfigFromPasswords) ... + AppleIdpConfigFromPasswords(), + ], +); ``` -:::tip -You can use the `AppleIdpConfigFromPasswords` constructor in replacement of the `AppleIdpConfig` above to automatically load the credentials from the `config/passwords.yaml` file or environment variables. It will expect either the following keys on the file: - -- `appleServiceIdentifier` -- `appleBundleIdentifier` -- `appleRedirectUri` -- `appleTeamId` -- `appleKeyId` -- `appleKey` -- `appleWebRedirectUri` (optional, for Web support when using server callback route) -- `appleAndroidPackageIdentifier` (optional, for Android support) - -Or the following environment variables: - -- `SERVERPOD_PASSWORD_appleServiceIdentifier` -- `SERVERPOD_PASSWORD_appleBundleIdentifier` -- `SERVERPOD_PASSWORD_appleRedirectUri` -- `SERVERPOD_PASSWORD_appleTeamId` -- `SERVERPOD_PASSWORD_appleKeyId` -- `SERVERPOD_PASSWORD_appleKey` -- `SERVERPOD_PASSWORD_appleWebRedirectUri` (optional, for Web support when using server callback route) -- `SERVERPOD_PASSWORD_appleAndroidPackageIdentifier` (optional, for Android support) +`AppleIdpConfigFromPasswords()` automatically loads the credentials from the Apple keys in `config/passwords.yaml` (or the corresponding `SERVERPOD_PASSWORD_` environment variables). + +### Configure web routes + +Apple Sign-In requires web routes for handling callbacks and revocation notifications. Add this call before `pod.start()`: + +```dart +pod.configureAppleIdpRoutes( + revokedNotificationRoutePath: '/hooks/apple-notification', + webAuthenticationCallbackRoutePath: '/auth/callback', +); +``` + +The `webAuthenticationCallbackRoutePath` must match the **Return URL** you registered on your Service ID. The `revokedNotificationRoutePath` is called by Apple when a user revokes access from their Apple ID settings. +:::tip +If you need more control over how the credentials are loaded, you can use `AppleIdpConfig(...)` with manual `pod.getPassword()` calls instead. See the [customizations](./customizations) page for details. ::: -Then, extend the abstract endpoint to expose it on the server: +### Create the endpoint + +Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/apple_idp_endpoint.dart`). Extending the base class registers the sign-in methods with your server so the Flutter client can call them: ```dart import 'package:serverpod_auth_idp_server/providers/apple.dart'; @@ -193,7 +154,9 @@ import 'package:serverpod_auth_idp_server/providers/apple.dart'; class AppleIdpEndpoint extends AppleIdpBaseEndpoint {} ``` -Run `serverpod generate` to generate the client code, then create and apply a database migration to initialize the provider's tables: +### Generate code and apply migrations + +Run the following commands from your server project directory (e.g., `my_project_server/`) to generate client code and apply the database migration: ```bash serverpod generate @@ -205,25 +168,9 @@ dart run bin/main.dart --apply-migrations Skipping the migration will cause the server to crash at runtime when the Apple provider tries to read or write user data. More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). ::: -### Basic configuration options - -- `serviceIdentifier`: Required. The service identifier for the Sign in with Apple project. -- `bundleIdentifier`: Required. The bundle ID of the Apple-native app using Sign in with Apple. -- `redirectUri`: Required. The redirect URL used for 3rd party platforms (e.g., Android, Web). -- `teamId`: Required. The team identifier of the parent Apple Developer account. -- `keyId`: Required. The ID of the key associated with the Sign in with Apple service. -- `key`: Required. The secret contents of the private key file received from Apple. - -When using Web or Android, you can also configure the following optional parameters: - -- `webRedirectUri`: The URL where the browser is redirected after the server receives Apple's callback on Web. Required for Web support when using the server callback route. -- `androidPackageIdentifier`: The Android package identifier for the app. Required for Apple Sign In to work on Android. When configured, the callback route automatically redirects Android clients back to the app using an intent URI. - -For more details on configuration options, see the [configuration section](./configuration). - ## Client-side configuration -The `serverpod_auth_idp_flutter` package implements the sign-in logic using [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple). The documentation for this package should in most cases also apply to the Serverpod integration. +The `serverpod_auth_idp_flutter` package uses [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) under the hood for platform-specific sign-in flows. :::note Sign in with Apple may not work correctly on all Simulator versions. If you run into issues during development, test on a physical device to confirm whether the problem is Simulator-specific. @@ -247,11 +194,7 @@ Enable the Sign in with Apple capability in your Xcode project: Apple Sign In on Android works through a web-based OAuth flow. When the user completes authentication, Apple redirects to your server's callback route, which then redirects back to your app using an Android intent URI with the `signinwithapple` scheme. -To enable this: - -1. Add the `androidPackageIdentifier` to your `AppleIdpConfig` (or the `appleAndroidPackageIdentifier` key in `passwords.yaml`). This must match your app's Android package name (e.g., `com.example.app`). -2. Configure the redirect URI in your Apple Developer Portal to point to your server's callback route (e.g., `https://example.com/auth/callback`). -3. Register the `signinwithapple` URI scheme in your `AndroidManifest.xml`: +The redirect URI and `appleAndroidPackageIdentifier` were already configured in the [Store your credentials](#store-your-credentials) and [Service ID](#create-a-service-id-android-and-web-only) steps. The only remaining step is to register the `signinwithapple` URI scheme in your `AndroidManifest.xml`: ```xml ` tag: -1. Configure the redirect URI in your Apple Developer Portal to match your server's callback route (e.g., `https://example.com/auth/callback`). -2. Set `webRedirectUri` in `AppleIdpConfig` (or `appleWebRedirectUri` in `passwords.yaml`) to the Web URL that should receive the callback parameters (e.g., `https://example.com/auth/apple-complete`). +```html + +``` -If `webRedirectUri` is not configured, Web callbacks to the server route will fail. +The redirect URI and `appleWebRedirectUri` were already configured in the [Store your credentials](#store-your-credentials) and [Service ID](#create-a-service-id-android-and-web-only) steps. :::warning All redirect URIs must use **HTTPS**. Apple rejects HTTP URLs, including `localhost`. For local development, expose your server over HTTPS using a tunnelling service, like ngrok or Cloudflare Tunnel. @@ -287,28 +229,33 @@ All redirect URIs must use **HTTPS**. Apple rejects HTTP URLs, including `localh ## Present the authentication UI -### Initializing the `AppleSignInService` +### Initialize the Apple sign-in service -To use the AppleSignInService, you need to initialize it in your main function. The initialization is done from the `initializeAppleSignIn()` extension method on the `FlutterAuthSessionManager`. +In your Flutter app's `main.dart` file (e.g., `my_project_flutter/lib/main.dart`), the template already sets up the `Client` and calls `client.auth.initialize()`. Add `client.auth.initializeAppleSignIn()` right after it: ```dart -import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; -import 'package:your_client/your_client.dart'; +client.auth.initialize(); +client.auth.initializeAppleSignIn(); +``` -final client = Client('http://localhost:8080/') - ..authSessionManager = FlutterAuthSessionManager(); +On **Web and Android**, the sign-in service needs your Service ID and redirect URI. Pass them as build-time environment variables using `--dart-define`: -void main() { - client.auth.initialize(); - client.auth.initializeAppleSignIn(); -} +```bash +flutter run \ + -d "" \ + --dart-define="APPLE_SERVICE_IDENTIFIER=com.example.service" \ + --dart-define="APPLE_REDIRECT_URI=https://example.com/auth/callback" ``` -### Using AppleSignInWidget +Use the same values you configured in the [Service ID](#create-a-service-id-android-and-web-only) and [Store your credentials](#store-your-credentials) steps. + +You can also pass the values directly as parameters instead. See the [customizations page](./customizations#configuring-apple-sign-in-on-the-app) for details. + +### Add the sign-in widget If you have configured the `SignInWidget` as described in the [setup section](../../setup#present-the-authentication-ui), the Apple identity provider will be automatically detected and displayed in the sign-in widget. -You can also use the `AppleSignInWidget` to include the Apple authentication flow in your own custom UI. +You can also use the `AppleSignInWidget` directly in your widget tree to include the Apple authentication flow in your own custom UI: ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; @@ -343,10 +290,9 @@ The widget automatically handles: For details on how to customize the Apple Sign-In UI in your Flutter app, see the [customizing the UI section](./customizing-the-ui). :::warning -**Apple sends the user's email address and full name only on the first sign-in.** On all subsequent sign-ins, neither is included in the response. If your server does not persist them during that first authentication, they cannot be retrieved later. - -Use the `sub` claim (the stable user identifier) to identify users. Do not use the email address, as it may change when a user updates their "Hide My Email" settings. For more information, see [Authenticating users with Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple/authenticating-users-with-sign-in-with-apple). - ---- +Apple sends the user's email and name only on the **first sign-in**. If your server does not persist them during that first authentication, they cannot be retrieved later. +::: +:::tip If you run into issues, see the [troubleshooting guide](./troubleshooting). +::: diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/02-configuration.md b/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md similarity index 60% rename from docs/06-concepts/11-authentication/04-providers/04-apple/02-configuration.md rename to docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md index 7ab368c8..bf631109 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/02-configuration.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md @@ -1,11 +1,36 @@ -# Configuration +# Customizations -This page covers configuration options for the Apple identity provider beyond the basic setup. +This page covers additional configuration options for the Apple identity provider beyond the basic setup. ## Configuration options Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `AppleIdpConfig` in-code documentation. +### Loading Apple credentials + +You can initialize the Apple identity provider in two ways: + +**From passwords.yaml (recommended):** + +```dart +final appleIdpConfig = AppleIdpConfigFromPasswords(); +``` + +**Manually, providing each credential explicitly:** + +```dart +final appleIdpConfig = AppleIdpConfig( + serviceIdentifier: pod.getPassword('appleServiceIdentifier')!, + bundleIdentifier: pod.getPassword('appleBundleIdentifier')!, + redirectUri: pod.getPassword('appleRedirectUri')!, + teamId: pod.getPassword('appleTeamId')!, + keyId: pod.getPassword('appleKeyId')!, + key: pod.getPassword('appleKey')!, + webRedirectUri: pod.getPassword('appleWebRedirectUri'), + androidPackageIdentifier: pod.getPassword('appleAndroidPackageIdentifier'), +); +``` + ### Reacting to account creation You can use the `onAfterAppleAccountCreated` callback to run logic after a new Apple account has been created and linked to an auth user. This callback is only invoked for new accounts, not for returning users. @@ -13,8 +38,7 @@ You can use the `onAfterAppleAccountCreated` callback to run logic after a new A This callback is complimentary to the [core `onAfterAuthUserCreated` callback](../../working-with-users#reacting-to-the-user-created-event) to perform side-effects that are specific to a login on this provider - like storing analytics, sending a welcome email, or storing additional data. ```dart -final appleIdpConfig = AppleIdpConfig( - // ... required parameters ... +final appleIdpConfig = AppleIdpConfigFromPasswords( onAfterAppleAccountCreated: ( session, authUser, @@ -34,7 +58,7 @@ This callback runs inside the same database transaction as the account creation. If you need to assign Serverpod scopes based on provider account data, note that updating the database alone (via `AuthServices.instance.authUsers.update()`) is **not enough** for the current login session. The token issuance uses the in-memory `authUser.scopes`, which is already set before this callback runs. You would need to update `authUser.scopes` as well for the scopes to be reflected in the issued tokens. For assigning scopes at creation time, consider using `onBeforeAuthUserCreated` to set scopes based on data collected earlier in the flow. ::: -## Web Routes Configuration +### Web Routes Configuration Apple Sign-In requires web routes for handling callbacks and notifications. These routes must be configured both on Apple's side and in your Serverpod server. @@ -53,14 +77,14 @@ pod.configureAppleIdpRoutes( - `webAuthenticationCallbackRoutePath` (default: `'/auth/callback'`): The path Apple redirects to after the user completes web-based sign-in. Must match the return URL registered on your Service ID. :::note -When a user revokes access from their Apple ID settings, Apple sends a notification to `revokedNotificationRoutePath`. Serverpod receives this notification automatically. You are responsible for invalidating any active sessions for that user in your own application logic. +When a user revokes access from their Apple ID settings, Apple sends a notification to `revokedNotificationRoutePath`. You are responsible for invalidating any active sessions for that user in your own application logic. ::: -## Configuring Apple Sign-In on the app +### Configuring Apple Sign-In on the App -Apple Sign-In requires additional configuration for web and Android platforms. On native Apple platforms (iOS/macOS), the configuration is handled automatically by the underlying `sign_in_with_apple` package through Xcode capabilities. +On web and Android platforms, you must supply a service identifier and redirect URI. If no values are provided programmatically, the provider falls back to reading from `--dart-define` build variables. To set them programmatically, you can use the following methods. -### Passing configuration in code +#### Passing Configuration in Code You can pass the configuration directly when initializing the Apple Sign-In service: @@ -71,25 +95,22 @@ client.auth.initializeAppleSignIn( ); ``` -The `serviceIdentifier` is your Apple Services ID (configured in Apple Developer Portal), and the `redirectUri` is the callback URL that Apple will redirect to after authentication (must match the URL configured on the server). - -Both parameters are optional. If not supplied, the provider falls back to the corresponding `--dart-define` build variable: +The `serviceIdentifier` is your Apple Services ID, and the `redirectUri` is the callback URL that Apple redirects to after authentication (must match the URL configured on the server). -- `serviceIdentifier` → `APPLE_SERVICE_IDENTIFIER` -- `redirectUri` → `APPLE_REDIRECT_URI` +This approach is useful when you need to manage configuration for different platforms in your Dart code. :::note -These parameters are only required for web and Android platforms. On native Apple platforms (iOS/macOS), they are ignored, and the configuration from Xcode capabilities is used instead. +These parameters are only required for web and Android platforms. On native Apple platforms (iOS/macOS), they are ignored. ::: -### Using Environment Variables +#### Using Environment Variables -Alternatively, you can pass configuration during build time using the `--dart-define` option: +Alternatively, you can pass configuration during build time using the `--dart-define` option. The Apple Sign-In provider supports the following build-time variables: -- `APPLE_SERVICE_IDENTIFIER`: The Apple Services ID. -- `APPLE_REDIRECT_URI`: The redirect URI for authentication callbacks. +- `APPLE_SERVICE_IDENTIFIER`: The Services ID used as OAuth client ID on Android and Web +- `APPLE_REDIRECT_URI`: The callback URL Apple redirects to after authentication -If you do not supply `serviceIdentifier` and `redirectUri` values when initializing the service, the provider will automatically fetch them from these environment variables. +If `serviceIdentifier` and `redirectUri` are not supplied when initializing the service, the provider will automatically read them from these variables. **Example usage:** @@ -102,36 +123,23 @@ flutter run \ This approach is useful when you need to: -- Manage configuration separately for different platforms (Android, Web) in a centralized way. -- Avoid committing sensitive configuration to version control. -- Configure different credentials for different build environments, like development, staging, and production. +- Manage configuration separately for different platforms (Android, Web) in a centralized way +- Avoid committing sensitive configuration to version control +- Configure different credentials for different build environments (development, staging, production) :::tip You can also set these environment variables in your IDE's run configuration or CI/CD pipeline to avoid passing them manually each time. ::: -## `AppleIdpConfig` parameter reference +## All configuration parameters | Parameter | Type | Required | `passwords.yaml` key | Description | | --- | --- | --- | --- | --- | -| `serviceIdentifier` | `String` | Yes (Android/Web) | `appleServiceIdentifier` | The Services ID identifier (e.g. `com.example.service`). Used as the OAuth client ID for Android and Web. Not required for iOS/macOS-only setups. | +| `serviceIdentifier` | `String` | Yes (Android/Web) | `appleServiceIdentifier` | The Services ID identifier (e.g. `com.example.service`). Used as the OAuth client ID for Android and Web. | | `bundleIdentifier` | `String` | Yes | `appleBundleIdentifier` | The App ID bundle identifier (e.g. `com.example.app`). Used as the client ID for native Apple platform sign-in. | -| `redirectUri` | `String` | Yes (Android/Web) | `appleRedirectUri` | The server callback route Apple redirects to after sign-in (e.g. `https://example.com/auth/callback`). Must be HTTPS and match the return URL registered on your Service ID. | -| `teamId` | `String` | Yes | `appleTeamId` | The 10-character Team ID from your Apple Developer account (e.g. `ABC123DEF4`). Used to sign the client secret JWT. | -| `keyId` | `String` | Yes | `appleKeyId` | The Key ID of the Sign in with Apple private key (e.g. `XYZ789ABC0`). | -| `key` | `String` | Yes | `appleKey` | The raw contents of the `.p8` private key file, including the `-----BEGIN PRIVATE KEY-----` header and footer. Serverpod uses this to generate a short-lived client secret JWT on each request. Do not pre-generate the JWT yourself. | -| `webRedirectUri` | `String?` | Web only | `appleWebRedirectUri` | The web app URL that the browser is redirected to after the server receives Apple's callback. This is required when using the server callback route for Web. | -| `androidPackageIdentifier` | `String?` | Android only | `appleAndroidPackageIdentifier` | The Android package name (e.g. `com.example.app`). When set, the callback route redirects Android clients back to the app via an intent URI using the `signinwithapple` scheme. | - -### Environment Variable equivalents - -All `passwords.yaml` keys can be set as environment variables by prefixing with `SERVERPOD_PASSWORD_`: - -- `appleServiceIdentifier` → `SERVERPOD_PASSWORD_appleServiceIdentifier` -- `appleBundleIdentifier` → `SERVERPOD_PASSWORD_appleBundleIdentifier` -- `appleRedirectUri` → `SERVERPOD_PASSWORD_appleRedirectUri` -- `appleTeamId` → `SERVERPOD_PASSWORD_appleTeamId` -- `appleKeyId` → `SERVERPOD_PASSWORD_appleKeyId` -- `appleKey` → `SERVERPOD_PASSWORD_appleKey` -- `appleWebRedirectUri` → `SERVERPOD_PASSWORD_appleWebRedirectUri` -- `appleAndroidPackageIdentifier` → `SERVERPOD_PASSWORD_appleAndroidPackageIdentifier` +| `redirectUri` | `String` | Yes (Android/Web) | `appleRedirectUri` | The server callback route Apple redirects to after sign-in. Must be HTTPS and match the return URL registered on your Service ID. | +| `teamId` | `String` | Yes | `appleTeamId` | The 10-character Team ID from your Apple Developer account. Used to sign the client secret JWT. | +| `keyId` | `String` | Yes | `appleKeyId` | The Key ID of the Sign in with Apple private key. | +| `key` | `String` | Yes | `appleKey` | The raw contents of the `.p8` private key file, including the `-----BEGIN PRIVATE KEY-----` header and footer. Do not pre-generate the JWT yourself. | +| `webRedirectUri` | `String?` | Web only | `appleWebRedirectUri` | The web app URL the browser is redirected to after the server receives Apple's callback. | +| `androidPackageIdentifier` | `String?` | Android only | `appleAndroidPackageIdentifier` | The Android package name (e.g. `com.example.app`). When set, the callback route redirects Android clients back to the app via an intent URI. | diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md b/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md index 535ef46f..740fb789 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md @@ -6,14 +6,28 @@ This page helps you identify common Sign in with Apple failures, explains why th Go through this before investigating a specific error. Most problems come from a missed step. +#### Apple Developer Portal + * [ ] Enable **Sign in with Apple** on your App ID at [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list). -* [ ] Add **Sign in with Apple** under Signing & Capabilities in Xcode (*iOS/macOS only*). * [ ] Create a **Service ID** and link it to your App ID (*Android and Web only*). * [ ] Confirm the **return URL** on the Service ID uses `https://` (not `http://` or `localhost`). -* [ ] Make sure **`appleKey`** in your config holds the raw `.p8` file contents (not a pre-generated JWT). -* [ ] Double-check the **`.p8` key** is indented consistently under `appleKey: |` in `passwords.yaml`. -* [ ] Run **`serverpod generate`** after adding the Apple provider, and apply migrations using `--apply-migrations`. +* [ ] Create a **Sign in with Apple key** and download the `.p8` file. + +#### Server + +* [ ] Add the Apple credentials to `config/passwords.yaml` with the raw `.p8` file contents (not a pre-generated JWT). +* [ ] Double-check the **`.p8` key** is indented consistently under `appleKey: |`. +* [ ] Add `AppleIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. * [ ] Call **`pod.configureAppleIdpRoutes(...)`** on the server before the pod starts. +* [ ] Create an `AppleIdpEndpoint` file in `lib/src/auth/`. +* [ ] Run **`serverpod generate`**, then apply migrations using `--apply-migrations`. + +#### Client + +* [ ] Add `client.auth.initializeAppleSignIn()` after `client.auth.initialize()` in your Flutter app's `main.dart`. +* [ ] Add **Sign in with Apple** under Signing & Capabilities in Xcode (*iOS/macOS only*). +* [ ] Add the **Apple JS SDK** script to `web/index.html` (*Web only*). +* [ ] Pass **`APPLE_SERVICE_IDENTIFIER`** and **`APPLE_REDIRECT_URI`** via `--dart-define` (*Web and Android only*). * [ ] Add the **`signinwithapple`** intent filter to `AndroidManifest.xml` (*Android only*). * [ ] Add **Apple's mail servers** to your SPF record if you email users who might use Hide My Email. @@ -32,7 +46,7 @@ appleKey: | -----END PRIVATE KEY----- ``` -Alternatively, set `appleKey` as an environment variable to avoid YAML indentation entirely. See [Environment Variable equivalents](./configuration#environment-variable-equivalents) in the configuration page. +Alternatively, set `appleKey` via the `SERVERPOD_PASSWORD_appleKey` environment variable to avoid YAML indentation entirely. ## Sign-in starts failing with `invalid_client` after months of success @@ -42,6 +56,40 @@ Alternatively, set `appleKey` as an environment variable to avoid YAML indentati **Resolution:** Replace any JWT in `appleKey` with the raw `.p8` private key (include the full header and footer). Serverpod will create fresh short-lived JWTs automatically. No need to handle JWTs yourself. See [Creating a client secret](https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret). +## Sign-in fails with `invalid_grant` + +**Problem:** Authentication fails with an `invalid_grant` error from Apple. + +**Cause:** Apple's authorization codes are single-use and expire after approximately 10 minutes. This error occurs when: + +* The authorization code was already exchanged (e.g. the request was retried after a network failure). +* The server clock is significantly out of sync, causing the client secret JWT to appear expired before Apple processes it. +* The identity token nonce does not match what the server expects. + +**Resolution:** + +* Do not retry requests that carry an Apple authorization code. If the flow fails, restart it from the beginning. +* Ensure your server's system clock is synchronized via NTP. A drift of more than a few seconds will cause JWT validation to fail on Apple's side. +* If the nonce mismatch is the cause, verify that the nonce generated on the client matches what the server uses during token validation. + +## Wrong identifier passed for web or Android sign-in + +**Problem:** Sign-in on Android or Web fails immediately, or Apple returns `invalid_client` / `invalid_request` even though credentials look correct. + +**Cause:** There are two separate identifiers in Apple's system and they are easy to mix up: + +* **App ID** (`bundleIdentifier`) -- the bundle identifier of your iOS/macOS app (e.g. `com.example.app`). Used for native Apple platform sign-in only. +* **Services ID** (`serviceIdentifier`) -- a separate identifier you create in the Apple Developer Portal specifically for web and Android OAuth (e.g. `com.example.service`). This acts as the OAuth client ID. + +Passing the App ID bundle identifier where the Services ID is expected will cause Apple to reject the request. + +**Resolution:** Check `passwords.yaml` and confirm: + +* `appleServiceIdentifier` is set to your **Services ID** (the one created under Identifiers → Services IDs). +* `appleBundleIdentifier` is set to your **App ID** bundle identifier. + +If you use `--dart-define`, confirm `APPLE_SERVICE_IDENTIFIER` is the Services ID, not the bundle ID. + ## Sign-in hangs on Android **Problem:** The OAuth flow opens a browser, but never returns to the app. Sign-in seems to finish but the app doesn't get the callback. @@ -120,6 +168,28 @@ dart run bin/main.dart --apply-migrations **Resolution:** Test on a physical device to confirm the problem is Simulator-specific. If sign-in works on a real device, no changes are needed. +## Web sign-in fails with `TypeError: type ... is not a subtype of type 'JSObject'` + +**Problem:** Clicking the Apple button on Web throws a `TypeError` mentioning `JSObject` or a minified type like `minified:CM`. + +**Cause:** The Apple JS SDK is not loaded. The `sign_in_with_apple` package calls `AppleID.auth.init()` on the page, but that function only exists after Apple's script is loaded in the HTML. + +**Resolution:** Add the Apple JS SDK to your Flutter app's `web/index.html` inside the `` tag: + +```html + +``` + +The `crossorigin="anonymous"` attribute is needed because Flutter's service worker sets a strict Cross-Origin Embedder Policy that blocks scripts without it. + +## macOS sign-in shows "Sign Up Not Completed" + +**Problem:** The native Sign in with Apple sheet appears on macOS, but immediately shows "Sign Up Not Completed" without completing authentication. + +**Cause:** The macOS app sandbox entitlement conflicts with `ASAuthorizationController`. When `com.apple.security.app-sandbox` is enabled alongside `com.apple.developer.applesignin`, the authorization flow fails silently. + +**Resolution:** In your macOS entitlements file (e.g., `macos/Runner/DebugProfile.entitlements`), remove the app sandbox entitlement or ensure it does not block the Sign in with Apple flow. Test without the sandbox first to confirm it is the cause, then re-add only the sandbox entitlements you need. + ## User stays signed in after removing Apple access **Problem:** A user removes your app from Apple ID settings (`Settings > [your name] > Sign-In & Security > Sign in with Apple > Stop Using Apple ID`) but is still logged in to your app. diff --git a/static/img/authentication/providers/apple/3-button.png b/static/img/authentication/providers/apple/3-button.png index 49f90eff58ddc400020bdde276a0ac6ea7ef9747..51405fea27192ae976365b0ed233acf64df9e900 100644 GIT binary patch literal 17294 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℞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 Date: Mon, 4 May 2026 19:12:03 +0100 Subject: [PATCH 2/5] docs: Clarify Apple Sign-In setup and customization details, including domain requirements in troubleshooting --- .../11-authentication/04-providers/04-apple/01-setup.md | 2 +- .../04-providers/04-apple/02-customizations.md | 4 ++-- .../04-providers/04-apple/04-troubleshooting.md | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md b/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md index f840eae0..b6264205 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md @@ -105,7 +105,7 @@ Paste the raw `.p8` file contents as-is. Do not pre-generate a JWT -- Serverpod ### Add the Apple identity provider -Your server's `server.dart` file (e.g., `my_project_server/lib/server.dart`) should already contain a `pod.initializeAuthServices()` call if your project was created with the Serverpod project template (`serverpod create`). If it's not there, see [Setup](../../setup) first to configure the auth module and JWT settings. +Your server's `server.dart` file (e.g., `my_project_server/lib/server.dart`) should already contain a `pod.initializeAuthServices()` call if your project was created with the Serverpod project template (`serverpod create`). If it's not there, verify that `serverpod_auth_idp_server` is in your server's `pubspec.yaml` and see [Setup](../../setup) to configure the auth module and JWT settings. Add the Apple import and `AppleIdpConfigFromPasswords()` to the existing `identityProviderBuilders` list: diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md b/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md index bf631109..720e2a33 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md @@ -35,7 +35,7 @@ final appleIdpConfig = AppleIdpConfig( You can use the `onAfterAppleAccountCreated` callback to run logic after a new Apple account has been created and linked to an auth user. This callback is only invoked for new accounts, not for returning users. -This callback is complimentary to the [core `onAfterAuthUserCreated` callback](../../working-with-users#reacting-to-the-user-created-event) to perform side-effects that are specific to a login on this provider - like storing analytics, sending a welcome email, or storing additional data. +This callback is complimentary to the core [`onAfterAuthUserCreated`](../../working-with-users#reacting-to-the-user-created-event) callback to perform side-effects that are specific to a login on this provider - like storing analytics, sending a welcome email, or storing additional data. ```dart final appleIdpConfig = AppleIdpConfigFromPasswords( @@ -131,7 +131,7 @@ This approach is useful when you need to: You can also set these environment variables in your IDE's run configuration or CI/CD pipeline to avoid passing them manually each time. ::: -## All configuration parameters +## AppleIdpConfig parameters | Parameter | Type | Required | `passwords.yaml` key | Description | | --- | --- | --- | --- | --- | diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md b/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md index 740fb789..db02b67e 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md @@ -10,6 +10,7 @@ Go through this before investigating a specific error. Most problems come from a * [ ] Enable **Sign in with Apple** on your App ID at [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list). * [ ] Create a **Service ID** and link it to your App ID (*Android and Web only*). +* [ ] Add your **Domains and Subdomains** (e.g. `example.com`) and **Return URLs** on the Service ID. * [ ] Confirm the **return URL** on the Service ID uses `https://` (not `http://` or `localhost`). * [ ] Create a **Sign in with Apple key** and download the `.p8` file. From 7922026f42618c53f9578997773ddb5ac191436f Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Wed, 6 May 2026 20:47:42 +0100 Subject: [PATCH 3/5] docs: Refine Sign in with Apple documentation, clarifying setup, customization options, and troubleshooting details --- .../04-providers/04-apple/01-setup.md | 123 +++++++++++++----- .../04-apple/02-customizations.md | 82 +++++------- .../04-apple/04-troubleshooting.md | 19 ++- 3 files changed, 139 insertions(+), 85 deletions(-) diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md b/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md index b6264205..02ca7d95 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md @@ -1,6 +1,12 @@ # Setup -Sign in with Apple requires a subscription to the [Apple Developer Program](https://developer.apple.com/programs/), even for development and testing. +## Prerequisites + +Before you start, make sure you have: + +- A Flutter project created with `serverpod create` (so the auth module and project template are in place). +- An active subscription to the [Apple Developer Program](https://developer.apple.com/programs/). Sign in with Apple requires this even for local development. +- Xcode installed if you target iOS or macOS. ## Get your credentials @@ -48,7 +54,7 @@ Skip this section if you are building for iOS or macOS only. 6. Click **Next**, then **Done**, then **Save**. :::warning -All return URLs must use **HTTPS**. Apple rejects HTTP redirect URIs. For local development, expose your server over HTTPS using a tunnelling service. +All return URLs must use **HTTPS**. Apple rejects HTTP URLs, including `localhost`. For local development, expose your server over HTTPS using a tunnelling service, like ngrok or Cloudflare Tunnel. ::: ### Create a Sign in with Apple key @@ -89,31 +95,35 @@ development: -----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg... -----END PRIVATE KEY----- - # Optional: Required only for Web support when using server callback route. + # Web only (server callback route). appleWebRedirectUri: 'https://example.com/auth/apple-complete' - # Optional: Required only if you want Apple Sign In to work on Android. + # Android only. appleAndroidPackageIdentifier: 'com.example.app' ``` -For production, add the same keys to the `production:` section of `passwords.yaml`, or set the corresponding `SERVERPOD_PASSWORD_` environment variables on your production server. - :::tip -Paste the raw `.p8` file contents as-is. Do not pre-generate a JWT -- Serverpod handles that internally. If sign-in fails, see the [troubleshooting guide](./troubleshooting). +Paste the raw `.p8` file contents as-is. Do not pre-generate a JWT. Serverpod handles that internally. If sign-in fails, see the [troubleshooting guide](./troubleshooting). ::: +When you are ready to ship, see [Going to production](#going-to-production) for the production credential setup. + ## Server-side configuration ### Add the Apple identity provider -Your server's `server.dart` file (e.g., `my_project_server/lib/server.dart`) should already contain a `pod.initializeAuthServices()` call if your project was created with the Serverpod project template (`serverpod create`). If it's not there, verify that `serverpod_auth_idp_server` is in your server's `pubspec.yaml` and see [Setup](../../setup) to configure the auth module and JWT settings. +In your server project directory, add the auth IDP server package: + +```bash +dart pub add serverpod_auth_idp_server +``` -Add the Apple import and `AppleIdpConfigFromPasswords()` to the existing `identityProviderBuilders` list: +`AppleIdpConfigFromPasswords()` reads the eight `apple*` keys from `config/passwords.yaml` (or the corresponding `SERVERPOD_PASSWORD_` environment variables), so you do not have to wire up each credential manually. In your server's `server.dart`, import it and add it to the existing `identityProviderBuilders` list on `pod.initializeAuthServices()`: ```dart import 'package:serverpod_auth_idp_server/providers/apple.dart'; -``` -```dart +// ... + pod.initializeAuthServices( tokenManagerBuilders: [ JwtConfigFromPasswords(), @@ -125,11 +135,9 @@ pod.initializeAuthServices( ); ``` -`AppleIdpConfigFromPasswords()` automatically loads the credentials from the Apple keys in `config/passwords.yaml` (or the corresponding `SERVERPOD_PASSWORD_` environment variables). - ### Configure web routes -Apple Sign-In requires web routes for handling callbacks and revocation notifications. Add this call before `pod.start()`: +Sign in with Apple requires web routes for handling callbacks and revocation notifications. Add this call before `pod.start()`: ```dart pod.configureAppleIdpRoutes( @@ -170,7 +178,13 @@ Skipping the migration will cause the server to crash at runtime when the Apple ## Client-side configuration -The `serverpod_auth_idp_flutter` package uses [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) under the hood for platform-specific sign-in flows. +In your Flutter app's directory, add the auth IDP Flutter package: + +```bash +flutter pub add serverpod_auth_idp_flutter +``` + +It uses [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) under the hood for platform-specific sign-in flows. :::note Sign in with Apple may not work correctly on all Simulator versions. If you run into issues during development, test on a physical device to confirm whether the problem is Simulator-specific. @@ -180,11 +194,11 @@ Sign in with Apple may not work correctly on all Simulator versions. If you run Enable the Sign in with Apple capability in your Xcode project: -1. Open your project in Xcode -2. Select your target -3. Go to "Signing & Capabilities" -4. Click "+ Capability" -5. Add "Sign in with Apple" +1. Open your project in Xcode. +2. Select your target. +3. Go to **Signing & Capabilities**. +4. Click **+ Capability**. +5. Add **Sign in with Apple**. ![Add capabilities](/img/authentication/providers/apple/1-xcode-add.png) @@ -192,7 +206,7 @@ Enable the Sign in with Apple capability in your Xcode project: ### Android -Apple Sign In on Android works through a web-based OAuth flow. When the user completes authentication, Apple redirects to your server's callback route, which then redirects back to your app using an Android intent URI with the `signinwithapple` scheme. +Sign in with Apple on Android works through a web-based OAuth flow. When the user completes authentication, Apple redirects to your server's callback route, which then redirects back to your app using an Android intent URI with the `signinwithapple` scheme. The redirect URI and `appleAndroidPackageIdentifier` were already configured in the [Store your credentials](#store-your-credentials) and [Service ID](#create-a-service-id-android-and-web-only) steps. The only remaining step is to register the `signinwithapple` URI scheme in your `AndroidManifest.xml`: @@ -215,7 +229,7 @@ This intent filter is required. Without it, the OAuth callback never returns to ### Web -Apple Sign In on Web requires the Apple JS SDK. Add the following script to your Flutter app's `web/index.html` inside the `` tag: +Sign in with Apple on Web requires the Apple JS SDK. Add the following script to your Flutter app's `web/index.html` inside the `` tag: ```html @@ -223,13 +237,9 @@ Apple Sign In on Web requires the Apple JS SDK. Add the following script to your The redirect URI and `appleWebRedirectUri` were already configured in the [Store your credentials](#store-your-credentials) and [Service ID](#create-a-service-id-android-and-web-only) steps. -:::warning -All redirect URIs must use **HTTPS**. Apple rejects HTTP URLs, including `localhost`. For local development, expose your server over HTTPS using a tunnelling service, like ngrok or Cloudflare Tunnel. -::: - ## Present the authentication UI -### Initialize the Apple sign-in service +### Initialize the Sign in with Apple service In your Flutter app's `main.dart` file (e.g., `my_project_flutter/lib/main.dart`), the template already sets up the `Client` and calls `client.auth.initialize()`. Add `client.auth.initializeAppleSignIn()` right after it: @@ -249,7 +259,7 @@ flutter run \ Use the same values you configured in the [Service ID](#create-a-service-id-android-and-web-only) and [Store your credentials](#store-your-credentials) steps. -You can also pass the values directly as parameters instead. See the [customizations page](./customizations#configuring-apple-sign-in-on-the-app) for details. +You can also pass the values directly as parameters instead. See the [customizations page](./customizations#configuring-sign-in-with-apple-on-the-app) for details. ### Add the sign-in widget @@ -277,22 +287,69 @@ AppleSignInWidget( ) ``` -This renders an Apple sign-in button like this: +This renders a Sign in with Apple button like this: -![Apple sign-in button](/img/authentication/providers/apple/3-button.png) +![Sign in with Apple button](/img/authentication/providers/apple/3-button.png) The widget automatically handles: -- Apple Sign-In flow for iOS, macOS, Android, and Web. +- Sign in with Apple flow for iOS, macOS, Android, and Web. - Token management. -- Underlying Apple Sign-In package error handling. +- Underlying `sign_in_with_apple` package error handling. -For details on how to customize the Apple Sign-In UI in your Flutter app, see the [customizing the UI section](./customizing-the-ui). +For details on how to customize the Sign in with Apple UI in your Flutter app, see the [customizing the UI section](./customizing-the-ui). :::warning Apple sends the user's email and name only on the **first sign-in**. If your server does not persist them during that first authentication, they cannot be retrieved later. ::: +## Going to production + +### Update the Apple Developer Portal + +Add your production domain and callback URL to the Service ID. The development tunnel URL and the production URL can stay registered at the same time. + +1. In [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list), open your Service ID. +2. Under **Sign in with Apple**, click **Configure**. +3. Add your production domain to **Domains and Subdomains** (e.g. `example.com`). +4. Add your production callback to **Return URLs** (e.g. `https://example.com/auth/callback`). +5. Click **Next**, **Done**, then **Save**. + +### Set production credentials + +Switch from the `development:` values you used during setup to production ones. Pick the path that matches your deployment: + +#### Self-hosted + +Add the `apple*` keys to the `production:` section of `passwords.yaml` with production values, or set them as environment variables on the production server using the `SERVERPOD_PASSWORD_` prefix (e.g. `SERVERPOD_PASSWORD_appleServiceIdentifier`, `SERVERPOD_PASSWORD_appleKey`). + +#### Serverpod Cloud + +Use `scloud password set` for each credential. The `appleKey` value spans multiple lines, so pass it via `--from-file`: + +```bash +scloud password set appleServiceIdentifier "com.example.service" +scloud password set appleBundleIdentifier "com.example.app" +scloud password set appleRedirectUri "https://example.com/auth/callback" +scloud password set appleTeamId "ABC123DEF4" +scloud password set appleKeyId "XYZ789ABC0" +scloud password set appleKey --from-file ./AuthKey_XYZ789ABC0.p8 +scloud password set appleWebRedirectUri "https://example.com/auth/apple-complete" +scloud password set appleAndroidPackageIdentifier "com.example.app" +``` + +Run these from your linked server project directory, or pass `--project ` on each call. See the [Serverpod Cloud passwords guide](https://docs.serverpod.dev/cloud/guides/passwords) for project linking and other options. + +### Update client builds + +For Web and Android release builds, pass the production Service ID and redirect URI via `--dart-define`: + +```bash +flutter build web \ + --dart-define="APPLE_SERVICE_IDENTIFIER=com.example.service" \ + --dart-define="APPLE_REDIRECT_URI=https://example.com/auth/callback" +``` + :::tip If you run into issues, see the [troubleshooting guide](./troubleshooting). ::: diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md b/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md index 720e2a33..4f85f858 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md @@ -8,15 +8,13 @@ Below is a non-exhaustive list of some of the most common configuration options. ### Loading Apple credentials -You can initialize the Apple identity provider in two ways: - -**From passwords.yaml (recommended):** +`AppleIdpConfigFromPasswords()` reads the eight `apple*` keys from `config/passwords.yaml` (or the matching `SERVERPOD_PASSWORD_` environment variables) for you. This is the path used in the [setup guide](./setup#add-the-apple-identity-provider) and is the recommended default: ```dart final appleIdpConfig = AppleIdpConfigFromPasswords(); ``` -**Manually, providing each credential explicitly:** +Use `AppleIdpConfig(...)` directly when you need to pull credentials from a custom source, transform them at startup, or omit `passwords.yaml` entirely. You are responsible for resolving each value: ```dart final appleIdpConfig = AppleIdpConfig( @@ -33,34 +31,33 @@ final appleIdpConfig = AppleIdpConfig( ### Reacting to account creation -You can use the `onAfterAppleAccountCreated` callback to run logic after a new Apple account has been created and linked to an auth user. This callback is only invoked for new accounts, not for returning users. - -This callback is complimentary to the core [`onAfterAuthUserCreated`](../../working-with-users#reacting-to-the-user-created-event) callback to perform side-effects that are specific to a login on this provider - like storing analytics, sending a welcome email, or storing additional data. +The Apple provider does not expose its own account-creation callback. To run logic after a user signs in with Apple for the first time, use the user-level [`onAfterAuthUserCreated`](../../working-with-users#reacting-to-the-user-created-event) callback on `AuthUsersConfig`. It fires the first time any provider creates an auth user, including Apple. ```dart -final appleIdpConfig = AppleIdpConfigFromPasswords( - onAfterAppleAccountCreated: ( - session, - authUser, - appleAccount, { - required transaction, - }) async { - // e.g. store additional data, send a welcome email, or log for analytics - }, +pod.initializeAuthServices( + tokenManagerBuilders: [ + JwtConfigFromPasswords(), + ], + identityProviderBuilders: [ + AppleIdpConfigFromPasswords(), + ], + authUsersConfig: AuthUsersConfig( + onAfterAuthUserCreated: (session, authUser, {required transaction}) async { + // authUser.id is the new user's UUID — use it to create any + // app-specific records that must exist before the user's first request. + await UserData.db.insertRow( + session, + UserData(authUserId: authUser.id, createdAt: authUser.createdAt), + transaction: transaction, + ); + }, + ), ); ``` -:::info -This callback runs inside the same database transaction as the account creation. Throwing an exception inside this callback will abort the process. If you perform external side-effects, make sure to safeguard them with a try/catch to prevent unwanted failures. -::: - -:::caution -If you need to assign Serverpod scopes based on provider account data, note that updating the database alone (via `AuthServices.instance.authUsers.update()`) is **not enough** for the current login session. The token issuance uses the in-memory `authUser.scopes`, which is already set before this callback runs. You would need to update `authUser.scopes` as well for the scopes to be reflected in the issued tokens. For assigning scopes at creation time, consider using `onBeforeAuthUserCreated` to set scopes based on data collected earlier in the flow. -::: - -### Web Routes Configuration +### Web routes configuration -Apple Sign-In requires web routes for handling callbacks and notifications. These routes must be configured both on Apple's side and in your Serverpod server. +Sign in with Apple requires web routes for handling callbacks and notifications. These routes must be configured both on Apple's side and in your Serverpod server. The `revokedNotificationRoutePath` is the path that Apple will call when a user revokes their authorization. The `webAuthenticationCallbackRoutePath` is the path that Apple will call when a user completes the sign-in process. @@ -80,44 +77,35 @@ pod.configureAppleIdpRoutes( When a user revokes access from their Apple ID settings, Apple sends a notification to `revokedNotificationRoutePath`. You are responsible for invalidating any active sessions for that user in your own application logic. ::: -### Configuring Apple Sign-In on the App - -On web and Android platforms, you must supply a service identifier and redirect URI. If no values are provided programmatically, the provider falls back to reading from `--dart-define` build variables. To set them programmatically, you can use the following methods. - -#### Passing Configuration in Code +### Configuring Sign in with Apple on the app -You can pass the configuration directly when initializing the Apple Sign-In service: +On web and Android, the Flutter client needs the Service ID and the server callback URL. The setup guide passes them via `--dart-define`. If you would rather hardcode them or resolve them at runtime, pass them directly to `initializeAppleSignIn()` instead: ```dart client.auth.initializeAppleSignIn( - serviceIdentifier: 'com.example.app', + serviceIdentifier: 'com.example.service', redirectUri: 'https://example.com/auth/callback', ); ``` -The `serviceIdentifier` is your Apple Services ID, and the `redirectUri` is the callback URL that Apple redirects to after authentication (must match the URL configured on the server). - -This approach is useful when you need to manage configuration for different platforms in your Dart code. +When both are passed, they take precedence over the `APPLE_SERVICE_IDENTIFIER` and `APPLE_REDIRECT_URI` build variables. The `redirectUri` must match the **Return URL** registered on your Apple Service ID and the value used by `pod.configureAppleIdpRoutes()`. :::note -These parameters are only required for web and Android platforms. On native Apple platforms (iOS/macOS), they are ignored. +These parameters are only used on web and Android. On native Apple platforms (iOS/macOS), the values come from your Xcode capability and are ignored here. ::: -#### Using Environment Variables - -Alternatively, you can pass configuration during build time using the `--dart-define` option. The Apple Sign-In provider supports the following build-time variables: +#### Using environment variables -- `APPLE_SERVICE_IDENTIFIER`: The Services ID used as OAuth client ID on Android and Web -- `APPLE_REDIRECT_URI`: The callback URL Apple redirects to after authentication +`APPLE_SERVICE_IDENTIFIER` and `APPLE_REDIRECT_URI` are the two build variables read by `initializeAppleSignIn()` on web and Android: -If `serviceIdentifier` and `redirectUri` are not supplied when initializing the service, the provider will automatically read them from these variables. +- `APPLE_SERVICE_IDENTIFIER`: your Services ID identifier (e.g. `com.example.service`) +- `APPLE_REDIRECT_URI`: the server callback URL (e.g. `https://example.com/auth/callback`) -**Example usage:** +Pass them at build time with `--dart-define`: ```bash flutter run \ - -d "" \ - --dart-define="APPLE_SERVICE_IDENTIFIER=com.example.app" \ + --dart-define="APPLE_SERVICE_IDENTIFIER=com.example.service" \ --dart-define="APPLE_REDIRECT_URI=https://example.com/auth/callback" ``` @@ -128,7 +116,7 @@ This approach is useful when you need to: - Configure different credentials for different build environments (development, staging, production) :::tip -You can also set these environment variables in your IDE's run configuration or CI/CD pipeline to avoid passing them manually each time. +You can set `--dart-define` values in your IDE run configuration or CI/CD pipeline instead of passing them on every `flutter run` command. ::: ## AppleIdpConfig parameters diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md b/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md index db02b67e..cea085a7 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/04-troubleshooting.md @@ -9,13 +9,15 @@ Go through this before investigating a specific error. Most problems come from a #### Apple Developer Portal * [ ] Enable **Sign in with Apple** on your App ID at [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list). -* [ ] Create a **Service ID** and link it to your App ID (*Android and Web only*). +* [ ] Create a **Service ID** for OAuth (*Android and Web only*). +* [ ] On the Service ID, check **Sign in with Apple**, click **Configure**, and select your **Primary App ID** (*Android and Web only*). * [ ] Add your **Domains and Subdomains** (e.g. `example.com`) and **Return URLs** on the Service ID. * [ ] Confirm the **return URL** on the Service ID uses `https://` (not `http://` or `localhost`). * [ ] Create a **Sign in with Apple key** and download the `.p8` file. #### Server +* [ ] Add `serverpod_auth_idp_server` to your server's `pubspec.yaml`. * [ ] Add the Apple credentials to `config/passwords.yaml` with the raw `.p8` file contents (not a pre-generated JWT). * [ ] Double-check the **`.p8` key** is indented consistently under `appleKey: |`. * [ ] Add `AppleIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. @@ -25,6 +27,7 @@ Go through this before investigating a specific error. Most problems come from a #### Client +* [ ] Add `serverpod_auth_idp_flutter` to your Flutter app's `pubspec.yaml`. * [ ] Add `client.auth.initializeAppleSignIn()` after `client.auth.initialize()` in your Flutter app's `main.dart`. * [ ] Add **Sign in with Apple** under Signing & Capabilities in Xcode (*iOS/macOS only*). * [ ] Add the **Apple JS SDK** script to `web/index.html` (*Web only*). @@ -79,8 +82,8 @@ Alternatively, set `appleKey` via the `SERVERPOD_PASSWORD_appleKey` environment **Cause:** There are two separate identifiers in Apple's system and they are easy to mix up: -* **App ID** (`bundleIdentifier`) -- the bundle identifier of your iOS/macOS app (e.g. `com.example.app`). Used for native Apple platform sign-in only. -* **Services ID** (`serviceIdentifier`) -- a separate identifier you create in the Apple Developer Portal specifically for web and Android OAuth (e.g. `com.example.service`). This acts as the OAuth client ID. +* **App ID** (`bundleIdentifier`): the bundle identifier of your iOS/macOS app (e.g. `com.example.app`). Used for native Apple platform sign-in only. +* **Services ID** (`serviceIdentifier`): a separate identifier you create in the Apple Developer Portal specifically for web and Android OAuth (e.g. `com.example.service`). This acts as the OAuth client ID. Passing the App ID bundle identifier where the Services ID is expected will cause Apple to reject the request. @@ -122,6 +125,7 @@ If you use `--dart-define`, confirm `APPLE_SERVICE_IDENTIFIER` is the Services I ```bash serverpod generate +serverpod create-migration dart run bin/main.dart --apply-migrations ``` @@ -187,9 +191,14 @@ The `crossorigin="anonymous"` attribute is needed because Flutter's service work **Problem:** The native Sign in with Apple sheet appears on macOS, but immediately shows "Sign Up Not Completed" without completing authentication. -**Cause:** The macOS app sandbox entitlement conflicts with `ASAuthorizationController`. When `com.apple.security.app-sandbox` is enabled alongside `com.apple.developer.applesignin`, the authorization flow fails silently. +**Cause:** This is almost always a signing or entitlements mismatch on the macOS target. Sign in with Apple needs the bundle ID, Team ID, and entitlements to line up with an App ID that has the capability enabled. -**Resolution:** In your macOS entitlements file (e.g., `macos/Runner/DebugProfile.entitlements`), remove the app sandbox entitlement or ensure it does not block the Sign in with Apple flow. Test without the sandbox first to confirm it is the cause, then re-add only the sandbox entitlements you need. +**Resolution:** Check, in this order: + +1. In Xcode, open the macOS target's **Signing & Capabilities** tab and confirm that **Sign in with Apple** is listed. If not, click **+ Capability** to add it. +2. Confirm the macOS bundle ID matches the App ID that has Sign in with Apple enabled in the Apple Developer Portal, and that the signing **Team** matches the same Apple Developer account. +3. If your macOS app is sandboxed, make sure the sandbox entitlements include `com.apple.security.network.client`. Without outbound network access, the request to Apple's servers fails silently. +4. Regenerate or re-download the provisioning profile after any of the above changes so the new entitlements are picked up. ## User stays signed in after removing Apple access From 39f870ea5cd747adbf9dafaa71c811df01fdb2fd Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Mon, 11 May 2026 03:25:41 +0100 Subject: [PATCH 4/5] docs: Enhance Apple Sign-In documentation with updated setup instructions, customization tips, and additional notes on credential management --- .../04-providers/04-apple/01-setup.md | 28 +++++++++++++------ .../04-apple/02-customizations.md | 6 +++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md b/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md index 02ca7d95..1422bb62 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md @@ -4,7 +4,7 @@ Before you start, make sure you have: -- A Flutter project created with `serverpod create` (so the auth module and project template are in place). +- A Serverpod project with the new auth module installed. New projects created with `serverpod create` (Serverpod 3.4 and later) include it by default. If you are upgrading an older project, follow the [auth module setup guide](../../setup) first. - An active subscription to the [Apple Developer Program](https://developer.apple.com/programs/). Sign in with Apple requires this even for local development. - Xcode installed if you target iOS or macOS. @@ -57,6 +57,10 @@ Skip this section if you are building for iOS or macOS only. All return URLs must use **HTTPS**. Apple rejects HTTP URLs, including `localhost`. For local development, expose your server over HTTPS using a tunnelling service, like ngrok or Cloudflare Tunnel. ::: +:::note +If you plan to support web sign-in, also register the value you will use for `appleWebRedirectUri` (e.g. `https://example.com/auth/apple-complete`) under **Return URLs**. Without it, the web flow will fail when Apple validates the redirect. +::: + ### Create a Sign in with Apple key 1. In Certificates, Identifiers & Profiles, click **Keys → +**. @@ -111,13 +115,13 @@ When you are ready to ship, see [Going to production](#going-to-production) for ### Add the Apple identity provider -In your server project directory, add the auth IDP server package: +The auth module ships with the core authentication services, but each identity provider lives in its own package. Add the IDP server package, which contains the Apple provider, in your server project directory: ```bash dart pub add serverpod_auth_idp_server ``` -`AppleIdpConfigFromPasswords()` reads the eight `apple*` keys from `config/passwords.yaml` (or the corresponding `SERVERPOD_PASSWORD_` environment variables), so you do not have to wire up each credential manually. In your server's `server.dart`, import it and add it to the existing `identityProviderBuilders` list on `pod.initializeAuthServices()`: +In your server's `server.dart`, import the Apple provider and add it to the existing `identityProviderBuilders` list on `pod.initializeAuthServices()`: ```dart import 'package:serverpod_auth_idp_server/providers/apple.dart'; @@ -135,6 +139,12 @@ pod.initializeAuthServices( ); ``` +`AppleIdpConfigFromPasswords()` reads the eight `apple*` keys from `config/passwords.yaml` (or the corresponding `SERVERPOD_PASSWORD_` environment variables), so you do not have to wire up each credential manually. + +:::tip +If you need more control over how the credentials are loaded, you can use `AppleIdpConfig(...)` with manual `pod.getPassword()` calls instead. See the [customizations](./customizations) page for details. +::: + ### Configure web routes Sign in with Apple requires web routes for handling callbacks and revocation notifications. Add this call before `pod.start()`: @@ -148,10 +158,6 @@ pod.configureAppleIdpRoutes( The `webAuthenticationCallbackRoutePath` must match the **Return URL** you registered on your Service ID. The `revokedNotificationRoutePath` is called by Apple when a user revokes access from their Apple ID settings. -:::tip -If you need more control over how the credentials are loaded, you can use `AppleIdpConfig(...)` with manual `pod.getPassword()` calls instead. See the [customizations](./customizations) page for details. -::: - ### Create the endpoint Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/apple_idp_endpoint.dart`). Extending the base class registers the sign-in methods with your server so the Flutter client can call them: @@ -178,7 +184,7 @@ Skipping the migration will cause the server to crash at runtime when the Apple ## Client-side configuration -In your Flutter app's directory, add the auth IDP Flutter package: +The auth module gives you the base sign-in widget, but each identity provider lives in its own client package. In your Flutter app's directory, add the IDP Flutter package, which contains the Apple widget: ```bash flutter pub add serverpod_auth_idp_flutter @@ -317,7 +323,11 @@ Add your production domain and callback URL to the Service ID. The development t ### Set production credentials -Switch from the `development:` values you used during setup to production ones. Pick the path that matches your deployment: +Production runs out of the `production:` section of `passwords.yaml`, which is separate from the `development:` section you populated during setup. Adding production credentials does not replace your development ones, both stay in place and Serverpod picks the right set based on the run mode. + +Most credentials, like the Team ID, Key ID, and `.p8` private key, can be reused from development. The values that typically differ are the URLs (`appleRedirectUri` and `appleWebRedirectUri`), which should point at your production domain rather than your development tunnel. If you use a different App ID or Service ID for production, register them in the [Apple Developer Portal](https://developer.apple.com/account/resources/identifiers/list) first and use those identifiers below. + +Pick the path that matches your deployment: #### Self-hosted diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md b/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md index 4f85f858..b608da0f 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md @@ -43,7 +43,7 @@ pod.initializeAuthServices( ], authUsersConfig: AuthUsersConfig( onAfterAuthUserCreated: (session, authUser, {required transaction}) async { - // authUser.id is the new user's UUID — use it to create any + // authUser.id is the new user's UUID, use it to create any // app-specific records that must exist before the user's first request. await UserData.db.insertRow( session, @@ -55,6 +55,10 @@ pod.initializeAuthServices( ); ``` +:::info +This callback runs inside the same database transaction as the auth user creation. Throwing an exception inside it will abort the entire process and the user will not be created. If you perform external side-effects (e.g. analytics, sending emails), wrap them in a try/catch so an unrelated failure does not block sign-in. +::: + ### Web routes configuration Sign in with Apple requires web routes for handling callbacks and notifications. These routes must be configured both on Apple's side and in your Serverpod server. From 0a60858f759082cf832433dff84cb1a6984b332f Mon Sep 17 00:00:00 2001 From: Chiziaruhoma Ogbonda Date: Mon, 11 May 2026 03:50:20 +0100 Subject: [PATCH 5/5] docs: Frame IDP package install as upgrade fallback in Apple setup `serverpod create` already ships `serverpod_auth_idp_server` and `serverpod_auth_idp_flutter` in the generated pubspecs, so the bare `pub add` steps were redundant for users on the standard template. --- .../11-authentication/04-providers/04-apple/01-setup.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md b/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md index 1422bb62..6659a60a 100644 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/01-setup.md @@ -115,7 +115,7 @@ When you are ready to ship, see [Going to production](#going-to-production) for ### Add the Apple identity provider -The auth module ships with the core authentication services, but each identity provider lives in its own package. Add the IDP server package, which contains the Apple provider, in your server project directory: +Projects created with `serverpod create` already have `serverpod_auth_idp_server` in `pubspec.yaml`. If your project doesn't (e.g. you're upgrading an older project), add it: ```bash dart pub add serverpod_auth_idp_server @@ -184,7 +184,7 @@ Skipping the migration will cause the server to crash at runtime when the Apple ## Client-side configuration -The auth module gives you the base sign-in widget, but each identity provider lives in its own client package. In your Flutter app's directory, add the IDP Flutter package, which contains the Apple widget: +The Flutter app created with `serverpod create` already has `serverpod_auth_idp_flutter` in `pubspec.yaml`. If your app doesn't, add it: ```bash flutter pub add serverpod_auth_idp_flutter