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..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 @@ -1,20 +1,16 @@ # 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. +## Prerequisites -:::caution -You need to install the auth module before you continue, see [Setup](../../setup). -::: +Before you start, make sure you have: -## Get your credentials +- 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. -All platforms require an App ID. Android and Web additionally require a Service ID. +## Get your credentials -| 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 @@ -58,7 +54,11 @@ 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. +::: + +:::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 @@ -85,10 +85,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' @@ -98,94 +99,68 @@ 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' ``` -:::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. +:::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). +::: -**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. +When you are ready to ship, see [Going to production](#going-to-production) for the production credential setup. ## 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 + +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 +``` + +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/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(); -} +// ... + +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()` 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. ::: -Then, extend the abstract endpoint to expose it on the server: +### Configure web routes + +Sign in with Apple 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. + +### 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 +168,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 +182,15 @@ 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. +## Client-side configuration -For more details on configuration options, see the [configuration section](./configuration). +The Flutter app created with `serverpod create` already has `serverpod_auth_idp_flutter` in `pubspec.yaml`. If your app doesn't, add it: -## Client-side configuration +```bash +flutter pub add serverpod_auth_idp_flutter +``` -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. +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. @@ -233,11 +200,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) @@ -245,13 +212,9 @@ 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. - -To enable this: +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. -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: -To enable this: - -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`). - -If `webRedirectUri` is not configured, Web callbacks to the server route will fail. +```html + +``` -:::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. -::: +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. ## Present the authentication UI -### Initializing the `AppleSignInService` +### Initialize the Sign in with Apple 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-sign-in-with-apple-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'; @@ -330,23 +293,73 @@ 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 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. +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 -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). +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 + +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-configuration.md b/docs/06-concepts/11-authentication/04-providers/04-apple/02-configuration.md deleted file mode 100644 index 7ab368c8..00000000 --- a/docs/06-concepts/11-authentication/04-providers/04-apple/02-configuration.md +++ /dev/null @@ -1,137 +0,0 @@ -# Configuration - -This page covers 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. - -### 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` 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 ... - onAfterAppleAccountCreated: ( - session, - authUser, - appleAccount, { - required transaction, - }) async { - // e.g. store additional data, send a welcome email, or log for analytics - }, -); -``` - -:::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 - -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. - -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. - -These routes are configured in the `pod.configureAppleIdpRoutes()` method: - -```dart -pod.configureAppleIdpRoutes( - revokedNotificationRoutePath: '/hooks/apple-notification', - webAuthenticationCallbackRoutePath: '/auth/callback', -); -``` - -- `revokedNotificationRoutePath` (default: `'/hooks/apple-notification'`): The path Apple calls when a user revokes authorization. Register this URL in your Apple Developer Portal for server-to-server notifications. -- `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. -::: - -## 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. - -### Passing configuration in code - -You can pass the configuration directly when initializing the Apple Sign-In service: - -```dart -client.auth.initializeAppleSignIn( - serviceIdentifier: 'com.example.app', - redirectUri: 'https://example.com/auth/callback', -); -``` - -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: - -- `serviceIdentifier` → `APPLE_SERVICE_IDENTIFIER` -- `redirectUri` → `APPLE_REDIRECT_URI` - -:::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. -::: - -### Using Environment Variables - -Alternatively, you can pass configuration during build time using the `--dart-define` option: - -- `APPLE_SERVICE_IDENTIFIER`: The Apple Services ID. -- `APPLE_REDIRECT_URI`: The redirect URI for authentication callbacks. - -If you do not supply `serviceIdentifier` and `redirectUri` values when initializing the service, the provider will automatically fetch them from these environment variables. - -**Example usage:** - -```bash -flutter run \ - -d "" \ - --dart-define="APPLE_SERVICE_IDENTIFIER=com.example.app" \ - --dart-define="APPLE_REDIRECT_URI=https://example.com/auth/callback" -``` - -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. - -:::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 - -| 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. | -| `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` 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 new file mode 100644 index 00000000..b608da0f --- /dev/null +++ b/docs/06-concepts/11-authentication/04-providers/04-apple/02-customizations.md @@ -0,0 +1,137 @@ +# Customizations + +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 + +`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(); +``` + +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( + 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 + +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 +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 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. + +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. + +These routes are configured in the `pod.configureAppleIdpRoutes()` method: + +```dart +pod.configureAppleIdpRoutes( + revokedNotificationRoutePath: '/hooks/apple-notification', + webAuthenticationCallbackRoutePath: '/auth/callback', +); +``` + +- `revokedNotificationRoutePath` (default: `'/hooks/apple-notification'`): The path Apple calls when a user revokes authorization. Register this URL in your Apple Developer Portal for server-to-server notifications. +- `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`. You are responsible for invalidating any active sessions for that user in your own application logic. +::: + +### Configuring Sign in with Apple on the app + +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.service', + redirectUri: 'https://example.com/auth/callback', +); +``` + +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 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 + +`APPLE_SERVICE_IDENTIFIER` and `APPLE_REDIRECT_URI` are the two build variables read by `initializeAppleSignIn()` on web and Android: + +- `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`) + +Pass them at build time with `--dart-define`: + +```bash +flutter run \ + --dart-define="APPLE_SERVICE_IDENTIFIER=com.example.service" \ + --dart-define="APPLE_REDIRECT_URI=https://example.com/auth/callback" +``` + +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 (development, staging, production) + +:::tip +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 + +| 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. | +| `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. 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..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 @@ -6,14 +6,32 @@ 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*). +* [ ] 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`). -* [ ] 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 `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`. * [ ] 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 `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*). +* [ ] 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 +50,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 +60,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. @@ -73,6 +125,7 @@ Alternatively, set `appleKey` as an environment variable to avoid YAML indentati ```bash serverpod generate +serverpod create-migration dart run bin/main.dart --apply-migrations ``` @@ -120,6 +173,33 @@ 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:** 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:** 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 **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 49f90eff..51405fea 100644 Binary files a/static/img/authentication/providers/apple/3-button.png and b/static/img/authentication/providers/apple/3-button.png differ