diff --git a/docs/06-concepts/04-authentication/02-setup.md b/docs/06-concepts/04-authentication/02-setup.md index 76bd6799..862d9048 100644 --- a/docs/06-concepts/04-authentication/02-setup.md +++ b/docs/06-concepts/04-authentication/02-setup.md @@ -103,6 +103,9 @@ By default, endpoints for all providers are disabled. To enable a provider: ```dart pod.initializeAuthServices( + tokenManagerBuilders: [ + JwtConfigFromPasswords(), + ], identityProviderBuilders: [ EmailIdpConfig( /* configuration options */ ), ], @@ -127,7 +130,7 @@ By default, endpoints for all providers are disabled. To enable a provider: $ serverpod start ``` -4. Create and apply the migration that initializes the database for the provider. In the `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +4. Create and apply the migration that initializes the database for the provider. In the `serverpod start` terminal, press **M**. The migration is created and applied in one step. If applying fails, press **A** to retry it. :::info If this is the first time creating migrations after adding the module, besides the provider tables, all authentication module tables will also be created. More detailed migration instructions can be found in the [migration guide](../data-and-the-database/database/migrations). diff --git a/docs/06-concepts/04-authentication/05-providers/01-anonymous/01-setup.md b/docs/06-concepts/04-authentication/05-providers/01-anonymous/01-setup.md index 25a00824..64ad87e3 100644 --- a/docs/06-concepts/04-authentication/05-providers/01-anonymous/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/01-anonymous/01-setup.md @@ -22,6 +22,9 @@ In your main `server.dart` file, configure the anonymous identity provider using ```dart import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; + +import 'src/generated/endpoints.dart'; +import 'src/generated/protocol.dart'; import 'package:serverpod_auth_idp_server/providers/anonymous.dart'; void run(List args) async { @@ -53,7 +56,7 @@ import 'package:serverpod_auth_idp_server/providers/anonymous.dart'; class AnonymousIdpEndpoint extends AnonymousIdpBaseEndpoint {} ``` -Then, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**, then **A**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). +Then, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). ### Basic configuration options diff --git a/docs/06-concepts/04-authentication/05-providers/01-anonymous/02-configuration.md b/docs/06-concepts/04-authentication/05-providers/01-anonymous/02-configuration.md index cf199d6f..7a55911f 100644 --- a/docs/06-concepts/04-authentication/05-providers/01-anonymous/02-configuration.md +++ b/docs/06-concepts/04-authentication/05-providers/01-anonymous/02-configuration.md @@ -36,7 +36,7 @@ AnonymousSignInWidget( ) ``` -### Configuring the Server +### Configuring the server In `onBeforeAnonymousAccountCreated`, receive the optional `token` and verify it with your app-check provider. If verification fails or the token is missing (when you require it), throw an `AnonymousAccountBlockedException` with reason `denied` to block account creation. @@ -69,7 +69,7 @@ For Firebase App Check, you can verify the token from a custom backend using the ## Reacting to anonymous account creation -Beside the `onBeforeAnonymousAccountCreated` callback to allow or deny creation, you can also use the `onAfterAnonymousAccountCreated` callback to run logic after a new anonymous account has been created (e.g. analytics or side effects). +Besides the `onBeforeAnonymousAccountCreated` callback to allow or deny creation, you can also use the `onAfterAnonymousAccountCreated` callback to run logic after a new anonymous account has been created (e.g. analytics or side effects). ```dart AnonymousIdpConfig( diff --git a/docs/06-concepts/04-authentication/05-providers/01-anonymous/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/01-anonymous/03-customizing-the-ui.md index c6e04b87..a6ebec4c 100644 --- a/docs/06-concepts/04-authentication/05-providers/01-anonymous/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/01-anonymous/03-customizing-the-ui.md @@ -16,8 +16,11 @@ SignInWidget( anonymousSignInWidget: AnonymousSignInWidget( client: client, createAnonymousToken: () async => await getAppCheckToken(), - size: AnonymousButtonSize.medium, - shape: AnonymousButtonShape.rectangular, + size: SignInButtonSize.medium, + shape: SignInButtonShape.rectangular, + // A custom widget replaces the built-in handling, so pass your own callbacks. + onAuthenticated: () { /* ... */ }, + onError: (error) { /* ... */ }, ), ) ``` @@ -45,40 +48,38 @@ AnonymousSignInWidget( onError: (error) { // Handle errors }, - size: AnonymousButtonSize.large, // large (default), medium, or small - shape: AnonymousButtonShape.pill, // pill (default) or rectangular + // Button customization. The values shown are the defaults. + size: SignInButtonSize.large, // or medium, small + shape: SignInButtonShape.pill, // or rounded, rectangular ) ``` -Optionally, you can provide an externally managed `AnonymousAuthController` instance to the widget. When a controller is provided, `client`, `onAuthenticated`, and `onError` are ignored in favor of the controller's configuration. +Optionally, you can provide an externally managed `AnonymousAuthController` instance to the widget. A controller and a `client` are mutually exclusive, and `onAuthenticated` and `onError` belong on the controller in that case. Passing them alongside a controller trips an assertion, so a debug build throws. ```dart AnonymousSignInWidget( controller: controller, - size: AnonymousButtonSize.medium, - shape: AnonymousButtonShape.rectangular, + size: SignInButtonSize.medium, + shape: SignInButtonShape.rectangular, ) ``` ### Customizing the button appearance -The widget renders a single **TextButton** with the label "Continue without account". The button uses Flutter's material design system, so it reacts to your app's `Theme`. You can wrap the widget in a `Theme` (or `ThemeData`) to change colors and typography: +The button renders flat, with no background fill and no border, and follows your app's theme brightness for its label color. It sets its own colors and corner radius, so a `TextButtonThemeData` does not reach it and an `ElevatedButtonThemeData` cannot change those. Properties the button leaves unset, such as `side` and `textStyle`, still fall through from that theme. + +To change the label's text style, pass `textStyle` to the widget: ```dart -Theme( - data: Theme.of(context).copyWith( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - foregroundColor: Colors.blue, - ), - ), - ), - child: AnonymousSignInWidget(client: client), +AnonymousSignInWidget( + client: client, + textStyle: const TextStyle(fontWeight: FontWeight.w600), ) ``` -The widget constrains the button to a minimum width of 240 and maximum width of 400; you can place it in a `SizedBox`, `Expanded`, or `Flex` to control layout. +Inside a `SignInWidget`, style every provider button at once with `buttonStyle` instead. See [Styling the buttons](../../ui-components#styling-the-buttons). + +The button is at least 240 pixels wide and at most 400. Place it in a `SizedBox`, `Expanded`, or `Flex` to control layout. ## Building a custom UI with the `AnonymousAuthController` diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md b/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md index 877a0957..f3776635 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md @@ -38,7 +38,7 @@ If a code cannot be sent, the failure is recorded in the session log and the sig Newly generated projects already include the email endpoint at `lib/src/auth/email_idp_endpoint.dart` and the migration that initializes the database, so running `serverpod start` is all that is needed. -If you are adding the auth module to an existing project, extend the abstract endpoint yourself. Create the file anywhere under your server's `lib/` directory (for example, `_server/lib/src/endpoints/`); the generator picks it up: +If you are adding the auth module to an existing project, extend the abstract endpoint yourself. Create the file anywhere under your server's `lib/` directory (for example, `_server/lib/src/endpoints/`). The generator picks it up: ```dart import 'package:serverpod_auth_idp_server/providers/email.dart'; @@ -46,17 +46,20 @@ import 'package:serverpod_auth_idp_server/providers/email.dart'; class EmailIdpEndpoint extends EmailIdpBaseEndpoint {} ``` -Then start the server with `serverpod start` to generate the client code, and create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**, then **A**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). +Then start the server with `serverpod start` to generate the client code, and create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). ### Use your own email provider Serverpod Cloud delivery is there to get sign-in working quickly, and it sends a standard message carrying your `appDisplayName`. You might prefer using a custom email provider to have full control over the body, layout, and language of the emails. For servers hosted outside of Serverpod Cloud, it is the only option. -Changing the email provider is done by replacing `ServerpodCloudEmailIdpConfig` with `EmailIdpConfigFromPasswords`, which requires you to pass your own callbacks for the two codes. One convenient option is the [mailer](https://pub.dev/packages/mailer) package, which can send emails through any SMTP service. Most email providers, such as Resend, Sendgrid or Mandrill, support SMTP. +Changing the email provider is done by replacing `ServerpodCloudEmailIdpConfig` with `EmailIdpConfigFromPasswords`, which requires you to pass your own callbacks for the two codes. One convenient option is the [mailer](https://pub.dev/packages/mailer) package, which can send emails through any SMTP service. Most email providers, such as Resend, SendGrid, or Mandrill, support SMTP. ```dart import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; + +import 'src/generated/endpoints.dart'; +import 'src/generated/protocol.dart'; import 'package:serverpod_auth_idp_server/providers/email.dart'; void run(List args) async { diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md b/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md index 06a4be86..4ddf5803 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md @@ -13,7 +13,7 @@ Below is a non-exhaustive list of some of the most common configuration options. ### Peppering -A pepper is a server-side secret that is added, along with a unique salt, to a password before it is hashed and stored. The pepper makes it harder for an attacker to crack password hashes if they have only gained access to the database. +A pepper is a server-side secret mixed into a password before it is hashed and stored, so a database leak alone is not enough to crack the hashes. See [storing secrets](../../setup#storing-secrets) for where to keep it. The pepper is configured via the `secretHashPepper` property in `EmailIdpConfig`, or read from the `emailSecretHashPepper` password by `EmailIdpConfigFromPasswords`, as described in the [server-side configuration](./setup#server-side-configuration) section. Its [recommended pepper length](https://www.ietf.org/archive/id/draft-ietf-kitten-password-storage-04.html#name-storage-2) is 32 bytes. @@ -33,7 +33,7 @@ final emailIdpConfig = EmailIdpConfigFromPasswords( ); ``` -### Customizing Password Requirements +### Customizing password requirements By default, the minimum password length is set to 8 characters. If you wish to modify this requirement, you can use the `passwordValidationFunction` configuration option. @@ -54,7 +54,7 @@ final emailIdpConfig = EmailIdpConfigFromPasswords( This is useful to ensure password policies on the server-side. It is a best practice to pair it with a configuration on the client-side to provide a better UX when creating a new password. The `EmailSignInWidget` and `EmailAuthController` have a `passwordRequirements` parameter that can be used to configure the password requirements. ::: -### Custom Verification Code Generation +### Custom verification code generation You can customize how verification codes are generated: @@ -62,14 +62,14 @@ You can customize how verification codes are generated: final emailIdpConfig = EmailIdpConfigFromPasswords( registrationVerificationCodeGenerator: () { // Generate a 6-digit numeric code - final random = Random(); + final random = Random.secure(); return List.generate(6, (_) => random.nextInt(10)).join(); }, ); ``` :::warning -Remember to configure the `verificationCodeConfig` parameter on the `EmailSignInWidget` to match the length of the verification code you generate. Otherwise, users will never be able to enter the verification code correctly. See the [customizing the UI section](./customizing-the-ui) for more details. +Remember to configure the `verificationCodeConfig` parameter on the `EmailSignInWidget` to match the length and allowed characters of the verification code you generate. Otherwise, users will never be able to enter the verification code correctly. See the [customizing the UI section](./customizing-the-ui) for more details. ::: #### Bypassing verification code in development @@ -84,7 +84,7 @@ pod.initializeAuthServices( identityProviderBuilders: [ EmailIdpConfigFromPasswords( registrationVerificationCodeGenerator: pod.runMode == 'development' - ? () => 'aaaaaaaa' // Be sure to match the length used in production. + ? () => '11111111' // Digits only, and the same length as production. : defaultVerificationCodeGenerator, ), ], diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md index 7a1a5987..a5cb6426 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md @@ -15,8 +15,11 @@ SignInWidget( client: client, emailSignInWidget: EmailSignInWidget( client: client, - // Change the initial screen to start registration - startScreen: EmailFlowScreen.startRegistration, + // Open on the login screen instead of the default registration screen + startScreen: EmailFlowScreen.login, + // A custom widget replaces the built-in handling, so pass your own callbacks. + onAuthenticated: () { /* ... */ }, + onError: (error) { /* ... */ }, ), ) ``` @@ -37,14 +40,15 @@ EmailSignInWidget( verificationCodeConfig: VerificationCodeConfig( length: 6, keyboardType: TextInputType.number, - allowedLetterCase: LetterCase.lowercase, allowedCharactersPattern: RegExp(r'[0-9]'), + // The wait before the user can request a new code. resendCountdownDuration: Duration(minutes: 1), ), - // Custom email validation function + // Custom email validation function. Throw InvalidEmailException so the + // widget shows the message. Other exceptions block sign-in silently. emailValidation: (email) { if (!email.contains('@example.com')) { - throw FormatException('Only @example.com emails allowed'); + throw const InvalidEmailException('Only @example.com emails allowed'); } }, // Customize the password requirements @@ -71,27 +75,24 @@ EmailSignInWidget( onError: (error) { // Handle errors }, - // Change the wait time before a user can request a new verification code - resendCountdownDuration: Duration(minutes: 2), ) ``` -Optionally, you can provide an externally managed `EmailAuthController` instance to the widget, which will ignore all other configuration options in favor of the controller's state. The controller contains all options above - with the exception of the `verificationCodeConfig` option, which is only used by the widget. +Optionally, you can provide an externally managed `EmailAuthController` instance to the widget. A controller and a `client` are mutually exclusive, and `onAuthenticated` and `onError` belong on the controller in that case. Passing either alongside a controller trips an assertion, so a debug build throws. The controller carries the sign-in options, while `verificationCodeConfig`, `onTermsAndConditionsPressed`, and `onPrivacyPolicyPressed` stay on the widget. ```dart EmailSignInWidget( - client: client, controller: controller, ) ``` :::info -The terms and conditions and privacy policy checkbox on the registration screen are optional and disabled by default. The checkbox will only be shown if you provide both `onTermsAndConditionsPressed` and `onPrivacyPolicyPressed` callbacks. +The terms and conditions and privacy policy checkbox on the registration screen are optional and disabled by default. The checkbox is shown as soon as you provide either `onTermsAndConditionsPressed` or `onPrivacyPolicyPressed`. ::: ### Customizing the default widget's appearance -Since the `EmailSignInWidget` uses the material design system, it will react to your app's material theme. You can also wrap it in a `Theme` widget to apply a custom theme. +Since the `EmailSignInWidget` uses the Material Design system, it will react to your app's Material theme. You can also wrap it in a `Theme` widget to apply a custom theme. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; @@ -101,10 +102,8 @@ Theme( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), // Use the AuthIdpTheme to customize the verification code input look extensions: >[ - AuthIdpTheme( - defaultPinTheme: PinTheme(...), + AuthIdpTheme.defaultTheme( errorPinTheme: PinTheme(...), - successPinTheme: PinTheme(...), ), ], ), @@ -134,7 +133,7 @@ final controller = EmailAuthController( ); ``` -### EmailAuthController State Management +### EmailAuthController state management Your widget should render the appropriate screen based on the `currentScreen` property of the controller. You can also use the below state properties to build your UI: @@ -185,7 +184,7 @@ if (controller.canNavigateBack) { } ``` -### EmailAuthController Methods +### EmailAuthController methods The controller provides methods for each step of the authentication flow: @@ -204,7 +203,7 @@ controller.passwordController.text = 'password123'; await controller.login(); ``` -#### Registration Flow +#### Registration flow The registration flow consists of three steps: @@ -232,7 +231,7 @@ await controller.finishRegistration(); // User is now authenticated ``` -#### Password Reset Flow +#### Password reset flow The password reset flow also consists of three steps: @@ -260,7 +259,7 @@ await controller.finishPasswordReset(); // User is authenticated with new password ``` -### Resending Verification Codes +### Resending verification codes To resend a verification code: diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md b/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md index 973b9c44..050dc891 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md @@ -1,5 +1,5 @@ --- -sidebar_label: Admin Operations +sidebar_label: Admin operations description: Email admin operations manage email accounts and clean up expired or dangling verification requests through the EmailIdpAdmin server-side API. --- @@ -22,11 +22,11 @@ final emailIdp = AuthServices.instance.emailIdp; final admin = emailIdp.admin; ``` -## Account Management +## Account management The admin API provides methods for managing email accounts: -### Finding Accounts +### Finding accounts ```dart // Find an account by email @@ -36,7 +36,7 @@ final account = await admin.findAccount( ); ``` -### Creating Accounts +### Creating accounts ```dart // Create an email authentication account @@ -48,7 +48,7 @@ final emailAccountId = await admin.createEmailAuthentication( ); ``` -### Deleting Accounts +### Deleting accounts ```dart // Delete an account by email @@ -64,7 +64,7 @@ await admin.deleteEmailAccountByAuthUserId( ); ``` -### Setting Passwords +### Setting passwords ```dart // Set or update a password for an account @@ -79,7 +79,7 @@ await admin.setPassword( The `setPassword` method does not validate the password against the configured password policy. Make sure to validate the password before calling this method if needed. ::: -## Finding Active Account Requests +## Finding active account requests You can also check for active account requests: @@ -92,11 +92,11 @@ final accountRequest = await admin.findActiveEmailAccountRequest( This is useful for checking the status of a registration request or verifying if a request is still valid. -## Cleanup Operations +## Cleanup operations Over time, expired account requests, password reset requests, and failed login attempts can accumulate in the database, leading to database bloat and potential performance issues. It's important to periodically clean these up to prevent database bloat. Such requests are not automatically cleaned up since they can be useful for auditing purposes, so it is up to each application to decide when to clean them up. -### Cleaning Up Expired Account Requests +### Cleaning up expired account requests Account requests that have expired (users who started registration but never completed it) should be cleaned up: @@ -107,11 +107,11 @@ await admin.deleteExpiredAccountRequests(session); // Delete a specific account request await admin.deleteEmailAccountRequestById( session, - accountRequestId: requestId, + requestId, ); ``` -### Cleaning Up Expired Password Reset Requests +### Cleaning up expired password reset requests Password reset requests that have expired (users who requested a password reset but never completed it) should be cleaned up: @@ -128,7 +128,7 @@ await admin.deletePasswordResetRequestsAttemptsForEmail( ); ``` -### Cleaning Up Failed Login Attempts +### Cleaning up failed login attempts Failed login attempts, tracked for rate limiting, should also be cleaned up when no longer useful for auditing purposes: diff --git a/docs/06-concepts/04-authentication/05-providers/03-google/01-setup.md b/docs/06-concepts/04-authentication/05-providers/03-google/01-setup.md index d836069c..82d1df89 100644 --- a/docs/06-concepts/04-authentication/05-providers/03-google/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/03-google/01-setup.md @@ -25,16 +25,6 @@ All platforms require a Web application OAuth client (used by the server). iOS a 2. Enter a **Project name** (e.g. `My Serverpod App`) and click **Create**. -### Enable People API - -The People API is required for Serverpod to access basic user profile data during sign-in. - -1. Navigate to the [People API page](https://console.cloud.google.com/apis/library/people.googleapis.com) in your project. - -2. Click **Enable**. - -![Enable People API](/img/authentication/providers/google/6-people-api.png) - ### Configure Google Auth Platform 1. Navigate to the [Google Auth Platform overview](https://console.cloud.google.com/auth/overview) and click **Get started** if you haven't enabled it yet. @@ -107,7 +97,7 @@ Replace `your-client-id` and `your-client-secret` with the values from the Googl For production, add the same `googleClientSecret` entry to the `production:` section of `passwords.yaml` (with your production redirect URI), or set the `SERVERPOD_PASSWORD_googleClientSecret` environment variable on your production server. :::note -**Carefully maintain correct indentation for YAML block scalars.** The `googleClientSecret` block uses a `|`; any indentation error will silently break the JSON, resulting in authentication failures. +**Carefully maintain correct indentation for YAML block scalars.** The `googleClientSecret` block uses a `|`. Any indentation error makes the JSON fail to parse, and the server throws at startup when `GoogleIdpConfigFromPasswords()` loads the secret. ::: ## Server-side configuration @@ -142,7 +132,7 @@ If you need more control over how the client secret is loaded, you can use `Goog ### Create the endpoint -Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/google_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so the Flutter client can call them to complete the authentication flow: +Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/google_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so your app can call them to complete the authentication flow: ```dart import 'package:serverpod_auth_idp_server/providers/google.dart'; @@ -158,7 +148,7 @@ Start the server from your server project directory (e.g., `my_project_server/`) serverpod start ``` -Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create and apply the migration. :::warning Skipping the migration will cause the server to crash at runtime when the Google 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). @@ -237,7 +227,7 @@ Without the URL scheme, the OAuth callback never returns to your app and sign-in ![Create Android OAuth client](/img/authentication/providers/google/9-android-client-create.png) -5. On Android, the sign-in SDK also needs to know your server's client ID. Pass the [Web application OAuth client](#create-the-server-oauth-client-web-application)'s ID as `serverClientId` when you initialize the client (covered in [Initialize the Google sign-in service](#initialize-the-google-sign-in-service) below). You can also pass it at build time with `--dart-define`; see [Configuring Client IDs on the App](./customizations#configuring-client-ids-on-the-app). +5. On Android, the sign-in SDK also needs to know your server's client ID. Pass the [Web application OAuth client](#create-the-server-oauth-client-web-application)'s ID as `serverClientId` when you initialize the client (covered in [Initialize the Google sign-in service](#initialize-the-google-sign-in-service) below). You can also pass it at build time with `--dart-define`. See [Configuring client IDs on the app](./customizations#configuring-client-ids-on-the-app). :::note If your app uses Firebase (the `com.google.gms.google-services` Gradle plugin), you can skip step 5: the plugin supplies the server client ID from `google-services.json`. Re-download that file after creating the Web application client so it includes the web client entry. @@ -252,7 +242,7 @@ When testing against a local server, the Android emulator cannot reach `localhos On web, Google completes sign-in by redirecting the browser to a callback URL you control. This flow requires Serverpod to serve your Flutter web app on the **same origin** (same scheme, host, and port) as the callback route. :::warning -The web flow only works from the **built** app served by Serverpod (`http://localhost:8082/app` locally). Running the app with `flutter run -d chrome` fails, because Flutter's dev server is a different origin than Serverpod and the browser blocks the sign-in callback; see [troubleshooting](./troubleshooting#sign-in-callback-fails-locally-with-flutter-run--d-chrome). For a hot-reload workflow, use the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) flow instead. +The web flow only works from the **built** app served by Serverpod (`http://localhost:8082/app` locally). Running the app with `flutter run -d chrome` fails, because Flutter's dev server is a different origin than Serverpod and the browser blocks the sign-in callback. See [troubleshooting](./troubleshooting#sign-in-callback-fails-locally-with-flutter-run--d-chrome). For a hot-reload workflow, use the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) flow instead. ::: To test locally, build your Flutter web app into Serverpod's `web/app/` directory and start the server: @@ -306,7 +296,7 @@ The examples below use port `8082` (Serverpod's default from `config/development 3. Pass the same URL to `initializeGoogleSignIn` via the `redirectUri` argument when you initialize the client (covered in [Initialize the Google sign-in service](#initialize-the-google-sign-in-service) below). :::tip - You can also pass the redirect URI via `--dart-define`. See [Configuring the Web redirect URI](./customizations#configuring-the-web-redirect-uri) for the pattern. + You can also pass the redirect URI via `--dart-define`. See [Configuring the web redirect URI](./customizations#configuring-the-web-redirect-uri) for the pattern. ::: ## Present the authentication UI @@ -335,7 +325,7 @@ if (kIsWeb) { } ``` -Swap the redirect URI for your production URL when deploying. See [Configuring the Web redirect URI](./customizations#configuring-the-web-redirect-uri) to avoid hard-coding it per environment. +Swap the redirect URI for your production URL when deploying. See [Configuring the web redirect URI](./customizations#configuring-the-web-redirect-uri) to avoid hard-coding it per environment. :::warning On web, the app served at `/app` is the build you created in [Web setup](#web). After changing `main.dart` (for example the `redirectUri`), run the build command again and hard-reload the browser. A stale build keeps sending the old values, and sign-in fails with [redirect_uri_mismatch](./troubleshooting#sign-in-fails-with-redirect_uri_mismatch). @@ -343,7 +333,7 @@ On web, the app served at `/app` is the build you created in [Web setup](#web). ### Show the Google sign-in button -The Serverpod template ships with a `SignInScreen` widget at `lib/screens/sign_in_screen.dart`. It listens to `client.auth.authInfoListenable` and swaps between `SignInWidget` while the user is signed out and the `child` you pass it once they sign in. `SignInWidget` auto-detects which identity provider endpoints are registered on the server, so once `GoogleIdpEndpoint` is exposed and the client code has been regenerated, the Google button appears inside it. +New projects include a `SignInScreen` widget at `lib/screens/sign_in_screen.dart`. The version below is trimmed to the essentials. It listens to `client.auth.authInfoListenable` and swaps between `SignInWidget` while the user is signed out and the `child` you pass it once they sign in. The `SignInWidget` auto-detects which identity provider endpoints are registered on the server, so once `GoogleIdpEndpoint` is exposed and the client code has been regenerated, the Google button appears inside it. ```dart import 'package:flutter/material.dart'; @@ -489,7 +479,7 @@ Use `https://.serverpod.space/auth/callback` as the redirect URI in scloud password set googleClientSecret --from-file path/to/google-client-secret.json ``` -Run this from your linked server project directory, or pass `--project ` on the call. See the [Serverpod Cloud passwords guide](https://docs.serverpod.dev/cloud/guides/passwords) for project linking and other options. +Run this from your linked server project directory, or pass `--project ` on the call. See the [Serverpod Cloud passwords guide](/cloud/concepts/passwords-secrets-env-vars) for project linking and other options. ### 4. Update the Android OAuth client with the release SHA-1 @@ -506,7 +496,7 @@ keytool -list -v -keystore your-release-key.jks -alias your-key-alias Once you have the SHA-1, go back to your Android OAuth client in the Google Auth Platform and add it under **SHA-1 certificate fingerprint**. :::warning -Forgetting this step is one of the most common reasons Google Sign-In works in debug builds but silently fails after publishing to the Play Store. +Forgetting this step is one of the most common reasons Google sign-in works in debug builds but silently fails after publishing to the Play Store. ::: ### 5. Publish the OAuth consent screen diff --git a/docs/06-concepts/04-authentication/05-providers/03-google/02-customizations.md b/docs/06-concepts/04-authentication/05-providers/03-google/02-customizations.md index b8ebbb01..267333af 100644 --- a/docs/06-concepts/04-authentication/05-providers/03-google/02-customizations.md +++ b/docs/06-concepts/04-authentication/05-providers/03-google/02-customizations.md @@ -62,9 +62,9 @@ final googleIdpConfig = GoogleIdpConfig( ); ``` -### Custom Account Validation +### Custom account validation -You can customize the validation for Google account details before allowing sign-in. By default, the validation checks that the received account details contains `name`, `fullName`, and `verifiedEmail` set to true. +You can customize the validation for Google account details before allowing sign-in. The default validation rejects sign-in unless `verifiedEmail` is true and both `name` and `fullName` are present. ```dart final googleIdpConfig = GoogleIdpConfigFromPasswords( @@ -72,7 +72,7 @@ final googleIdpConfig = GoogleIdpConfigFromPasswords( googleAccountDetailsValidation: (accountDetails) { // Throw an exception if account doesn't meet custom requirements if (accountDetails.verifiedEmail != true || - !accountDetails.email!.endsWith('@example.com')) { + !accountDetails.email.endsWith('@example.com')) { throw GoogleUserInfoMissingDataException(); } }, @@ -92,7 +92,7 @@ For a full list of available scopes, see the [Google OAuth 2.0 Scopes reference] Adding additional scopes may require approval by Google. On the OAuth consent screen, you can see which of your scopes are considered sensitive. ::: -### Accessing Google APIs on the Server +### Accessing Google APIs on the server On the server side, you can access Google APIs using the access token. The `getExtraGoogleInfoCallback` in `GoogleIdpConfig` receives the access token and can be used to call Google APIs: @@ -109,7 +109,10 @@ final googleIdpConfig = GoogleIdpConfigFromPasswords( // Use accessToken to call Google APIs and store additional info // Example: Access YouTube API final response = await http.get( - Uri.https('www.googleapis.com', '/youtube/v3/channels?part=snippet&mine=true'), + Uri.https('www.googleapis.com', '/youtube/v3/channels', { + 'part': 'snippet', + 'mine': 'true', + }), headers: {'Authorization': 'Bearer $accessToken'}, ); // Process response and store additional info in the database @@ -119,7 +122,7 @@ final googleIdpConfig = GoogleIdpConfigFromPasswords( ### Reacting to auth user creation -The `onBeforeAuthUserCreated` and `onAfterAuthUserCreated` hooks are global callbacks configured on `AuthUsersConfig` in `initializeAuthServices`. They are not specific to Google; they fire for every identity provider. See the [working with users](../../working-with-users#reacting-to-the-user-created-event) page for full details. +The `onBeforeAuthUserCreated` and `onAfterAuthUserCreated` hooks are global callbacks configured on `AuthUsersConfig` in `initializeAuthServices`. They are not specific to Google. They fire for every identity provider. See the [working with users](../../working-with-users#reacting-to-the-user-created-event) page for full details. The `onBeforeAuthUserCreated` callback receives the default scopes and blocked status for the new user and must return the final values. Use it to assign custom scopes at creation time: @@ -154,7 +157,7 @@ pod.initializeAuthServices( ); ``` -### Lightweight Sign-In on the Flutter app +### Lightweight sign-in on the Flutter app Lightweight sign-in is a feature that attempts to authenticate users previously logged in with Google automatically with minimal or no user interaction. When enabled, the Google authentication controller will try to sign the user in using platform-specific lightweight authentication methods. This feature is disabled by default, but can be enabled from the `GoogleSignInWidget` or `GoogleAuthController`. @@ -170,15 +173,17 @@ GoogleSignInWidget( :::note Lightweight sign-in runs automatically when the controller is initialized (typically at app launch). If it fails (no previous session, or the user dismisses the prompt), the regular sign-in button remains available. + +On web, the option has no effect in this version. It only applies to Android and iOS. ::: -### Configuring Client IDs on the App +### Configuring client IDs on the app If no client IDs are provided programmatically, the underlying `google_sign_in` package falls back to reading from platform-specific configuration files (e.g., `GoogleService-Info.plist` for iOS, `google-services.json` for Android). To set them programmatically, you can use the following methods. -#### Passing Client IDs in Code +#### Passing client IDs in code -You can pass the client IDs directly when initializing the Google Sign-In service: +You can pass the client IDs directly when initializing the Google sign-in service: ```dart client.auth.initializeGoogleSignIn( @@ -189,9 +194,9 @@ client.auth.initializeGoogleSignIn( This approach is useful when you need different client IDs per platform and want to manage them in your Dart code. -#### Using Environment Variables +#### Using environment variables -Alternatively, you can pass client IDs during build time using the `--dart-define` option. The Google Sign-In provider supports the following environment variables: +Alternatively, you can pass client IDs during build time using the `--dart-define` option. The Google sign-in provider supports the following environment variables: - `GOOGLE_CLIENT_ID`: The platform-specific OAuth client ID - `GOOGLE_SERVER_CLIENT_ID`: The server (web application) OAuth client ID @@ -217,15 +222,15 @@ 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. ::: -### Configuring the Web redirect URI +### Configuring the web redirect URI You can pass the web redirect URI to `initializeGoogleSignIn` via `--dart-define`. This is useful when building for different environments (development, staging, production) without changing `main.dart`: ```dart if (kIsWeb) { client.auth.initializeGoogleSignIn( - clientId: String.fromEnvironment('GOOGLE_CLIENT_ID'), - redirectUri: String.fromEnvironment('GOOGLE_WEB_REDIRECT_URI'), + clientId: const String.fromEnvironment('GOOGLE_CLIENT_ID'), + redirectUri: const String.fromEnvironment('GOOGLE_WEB_REDIRECT_URI'), ); } else { client.auth.initializeGoogleSignIn(); @@ -238,7 +243,7 @@ flutter run -d chrome \ --dart-define="GOOGLE_WEB_REDIRECT_URI=" ``` -Use the redirect URI that matches the environment you are building for (e.g., `http://localhost:8082/auth/callback` for local development with the integrated route, or `https://my-awesome-project.serverpod.space/auth/callback` for production). +Use the redirect URI that matches the environment you are building for: the integrated-route URL (e.g., `http://localhost:8082/auth/callback`) when Serverpod serves your web app, or your production URL (e.g., `https://my-awesome-project.serverpod.space/auth/callback`). For `flutter run -d chrome`, where the app runs on its own origin, use the [separately-hosted flow](#separately-hosted-flutter-web) instead. ### Separately-hosted Flutter web @@ -264,5 +269,7 @@ Use this flow when your Flutter web app and Serverpod are on different origins. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `clientSecret` | `GoogleClientSecret` | Yes | The Google OAuth client secret loaded from JSON. Can be loaded via `fromJsonString`, `fromJsonFile`, or `fromJson`. | -| `googleAccountDetailsValidation` | `GoogleAccountDetailsValidation?` | No | Custom validation callback for Google account details before allowing sign-in. Throws an exception to reject the account. | +| `googleAccountDetailsValidation` | `GoogleAccountDetailsValidation` | No | Custom validation callback for Google account details before allowing sign-in. Throws an exception to reject the account. | | `getExtraGoogleInfoCallback` | `GetExtraGoogleInfoCallback?` | No | Callback that receives the access token after sign-in, allowing you to call additional Google APIs and store extra user data. | +| `onAfterGoogleAccountCreated` | `AfterGoogleAccountCreatedFunction?` | No | Callback invoked after a new Google account has been created and linked to an auth user. Runs inside the same transaction as account creation. | +| `clockSkewTolerance` | `Duration` | No | Tolerance for clock skew when validating Google ID token timestamps. Defaults to the framework's default tolerance. | diff --git a/docs/06-concepts/04-authentication/05-providers/03-google/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/03-google/03-customizing-the-ui.md index 0b442982..e5f55e88 100644 --- a/docs/06-concepts/04-authentication/05-providers/03-google/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/03-google/03-customizing-the-ui.md @@ -5,18 +5,23 @@ description: Google sign-in UI can be customized with the GoogleSignInWidget and # Customize the Google sign-in UI -When using the Google identity provider, you can customize the UI to your liking. You can use the `GoogleSignInWidget` to display the Google Sign-In flow in your own custom UI, or you can use the `GoogleAuthController` to build a completely custom authentication interface. +When using the Google identity provider, you can customize the UI to your liking. You can use the `GoogleSignInWidget` to display the Google sign-in flow in your own custom UI, or you can use the `GoogleAuthController` to build a completely custom authentication interface. :::info -The `SignInWidget` uses the `GoogleSignInWidget` internally to display the Google Sign-In flow. You can also supply a custom `GoogleSignInWidget` to the `SignInWidget` to override the default behavior. +The `SignInWidget` uses the `GoogleSignInWidget` internally to display the Google sign-in flow. You can also supply a custom `GoogleSignInWidget` to the `SignInWidget` to override the default behavior. ```dart SignInWidget( client: client, googleSignInWidget: GoogleSignInWidget( client: client, - // Customize the widget theme - theme: GSIButtonTheme.filledBlack, + // Shape and label survive inside SignInWidget, unless its buttonStyle + // sets them. Brand colors do not. + shape: SignInButtonShape.rounded, + text: SignInButtonTextVariant.signInWith, + // A custom widget replaces the built-in handling, so pass your own callbacks. + onAuthenticated: () { /* ... */ }, + onError: (error) { /* ... */ }, ), ) ``` @@ -32,14 +37,14 @@ You can customize the widget's appearance and behavior: ```dart GoogleSignInWidget( client: client, - // Button customization - type: GSIButtonType.standard, // or icon - theme: GSIButtonTheme.outlined, // or filledBlue, filledBlack, etc. - size: GSIButtonSize.large, // or medium - text: GSIButtonText.signIn, // or continueWith, signinWith, signUpWith - shape: GSIButtonShape.pill, // or rectangular - logoAlignment: GSIButtonLogoAlignment.left, // or center - minimumWidth: 200, // or null for automatic width + // Button customization. The values shown are the defaults. + style: GoogleButtonStyle.outline, // or filledBlue, filledBlack + size: SignInButtonSize.large, // or medium, small + text: SignInButtonTextVariant.continueWith, // or signInWith, signUpWith, signIn + shape: SignInButtonShape.pill, // or rounded, rectangular + logoAlignment: SignInButtonLogoAlignment.center, // or left + minimumWidth: 240, // at most 400 + textStyle: null, // TextStyle for the label // Scopes to request from Google // These are the default scopes, you can add additional scopes as needed. @@ -48,7 +53,7 @@ GoogleSignInWidget( 'https://www.googleapis.com/auth/userinfo.profile', ], - // Whether to attempt lightweight sign-in (One Tap, FedCM) + // Whether to attempt lightweight sign-in (Android and iOS only) attemptLightweightSignIn: false, onAuthenticated: () { @@ -93,10 +98,10 @@ await controller.signIn(); ``` :::note -On web, the button you can customize depends on which web sign-in mode you use. If you pass `redirectUri` to `initializeGoogleSignIn`, sign-in runs through the OAuth2 redirect flow and your custom widget renders directly. If you do not pass `redirectUri`, the underlying `google_sign_in` package renders Google's built-in button instead and most visual customizations have no effect. Set up `redirectUri` as described in the [Web setup](./setup#web) to control the button yourself. +On web, sign-in always runs through the OAuth2 redirect flow, and your customized widget renders directly. Both `clientId` and `redirectUri` must be passed to `initializeGoogleSignIn`, or the button does not render at all. Set them up as described in [Web setup](./setup#web). ::: -### GoogleAuthController State Management +### GoogleAuthController state management Your widget should render the appropriate UI based on the `state` property of the controller. You can also use the below state properties to build your UI: @@ -121,7 +126,7 @@ controller.addListener(() { }); ``` -#### GoogleAuthController States +#### GoogleAuthController states - `GoogleAuthState.initializing` - Controller is initializing. - `GoogleAuthState.idle` - Ready for user interaction. diff --git a/docs/06-concepts/04-authentication/05-providers/03-google/04-troubleshooting.md b/docs/06-concepts/04-authentication/05-providers/03-google/04-troubleshooting.md index 67986bcd..35284271 100644 --- a/docs/06-concepts/04-authentication/05-providers/03-google/04-troubleshooting.md +++ b/docs/06-concepts/04-authentication/05-providers/03-google/04-troubleshooting.md @@ -14,25 +14,24 @@ Go through this before investigating a specific error. Most problems come from a #### Google Cloud - [ ] Create a **Google Cloud project** in the [Google Cloud Console](https://console.cloud.google.com/). -- [ ] Enable the **People API** in your project. - [ ] In **Google Auth Platform**, complete the initial setup (wizard) and add the required scopes on **Data Access** (`.../auth/userinfo.email` and `.../auth/userinfo.profile`). - [ ] On **Branding** ([Branding](https://console.cloud.google.com/auth/branding)), complete the OAuth consent screen (logo, homepage, privacy policy, terms of service, and developer contact) and add the **root domain** (top private domain) under **Authorized domains**. Google stores only the root, so a single verified entry covers all of its subdomains. On Serverpod Cloud, add `serverpod.space` (already verified by Serverpod, no DNS setup needed). For custom domains, see [Verify your authorized domain](./setup#1-verify-your-authorized-domain). - [ ] Add **test users** on **Audience** while in **Testing** mode ([Audience](https://console.cloud.google.com/auth/audience)), or **Publish app** when everyone should be able to sign in. - [ ] Create a **Web application** OAuth client. For web sign-in, set **Authorized JavaScript origins** to your Flutter web app's origin (e.g., `https://my-awesome-project.serverpod.space`) and **Authorized redirect URIs** to the route URL from [Web setup](./setup#web) (e.g., `https://my-awesome-project.serverpod.space/auth/callback`), or the `auth.html` URL if you use the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) fallback (e.g., `http://localhost:49660/auth.html`). Copy the **Client ID** and **Client secret**. -- [ ] Add `googleClientSecret` to `config/passwords.yaml` with your client ID, client secret, and matching `redirect_uris` (the same callback URL as above). For production, this is the route URL you registered via `FlutterWebAuth2CallbackRoute` (e.g., `https://my-awesome-project.serverpod.space/auth/callback`) from [Web setup](./setup#web), or the production `auth.html` URL on your Flutter web host if you use the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) fallback; see [Publishing to production](./setup#publishing-to-production). +- [ ] Add `googleClientSecret` to `config/passwords.yaml` with your client ID, client secret, and matching `redirect_uris` (the same callback URL as above). For production, this is the route URL you registered via `FlutterWebAuth2CallbackRoute` (e.g., `https://my-awesome-project.serverpod.space/auth/callback`) from [Web setup](./setup#web), or the production `auth.html` URL on your Flutter web host if you use the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) fallback. See [Publishing to production](./setup#publishing-to-production). #### Server - [ ] For new or customized servers, confirm auth services and JWT are configured per [Authentication setup](../../setup#identity-providers-configuration) before adding Google. - [ ] Add `GoogleIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. - [ ] Create a `GoogleIdpEndpoint` file in `lib/src/auth/`. -- [ ] Start the server with `serverpod start`, then create and apply the migration (press **M**, then **A**). +- [ ] Start the server with `serverpod start`, then create and apply the migration (press **M**). #### Client - [ ] Add `client.auth.initializeGoogleSignIn()` after `client.auth.initialize()` in your Flutter app's `main.dart`. On web, pass `clientId` and `redirectUri` (the full callback URL, either the route URL or the `auth.html` URL, depending on your [Web setup](./setup#web)). On Android, pass `serverClientId` (the Web client's ID) unless your app uses the Firebase Gradle plugin. - [ ] Surface Google sign-in in the UI with `SignInWidget` or `GoogleSignInWidget` (see [Present the authentication UI](./setup#present-the-authentication-ui)). -- [ ] Create an **iOS** OAuth client in the **same** Google Cloud project as the Web client, using the same **Bundle ID** as the app; set `GIDClientID` from the iOS client, `GIDServerClientID` to the **Web** client's ID, and add the reversed-client-ID **URL scheme** in `Info.plist` (*iOS only*). +- [ ] Create an **iOS** OAuth client in the **same** Google Cloud project as the Web client, using the same **Bundle ID** as the app. Set `GIDClientID` from the iOS client, `GIDServerClientID` to the **Web** client's ID, and add the reversed-client-ID **URL scheme** in `Info.plist` (*iOS only*). - [ ] Create an **Android** OAuth client in the **same** project, with the same **package name** and **SHA-1** as the build you run (*Android only*). - [ ] Set up the web callback (*Web only*). Pick one: - **Standard:** Register `FlutterWebAuth2CallbackRoute` on `pod.webServer` in `server.dart` before `pod.start()` per [Web setup](./setup#web). @@ -49,17 +48,16 @@ Go through this before investigating a specific error. Most problems come from a - **Authorized JavaScript origins** must contain your Flutter web app's origin (e.g., `http://localhost:49660` locally, `https://my-awesome-project.serverpod.space` in production). - **Authorized redirect URIs** must contain the full callback URL: the route URL from [Web setup](./setup#web) (e.g., `https://my-awesome-project.serverpod.space/auth/callback`), or the full `auth.html` URL if you use the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) fallback (e.g., `http://localhost:49660/auth.html` locally). -The same callback URL must also appear: +The same callback URL must also appear in `client.auth.initializeGoogleSignIn(..., redirectUri: ...)` in your Flutter app. -- In `config/passwords.yaml` under `googleClientSecret.web.redirect_uris`. -- In `client.auth.initializeGoogleSignIn(..., redirectUri: ...)` in your Flutter app. +The `redirect_uris` key in `config/passwords.yaml` must be present for the JSON to parse, but its contents are not used for this check. Common mistakes: - Trailing slashes, port differences, or `http` vs `https`. -- Forgetting the callback path on the redirect URI; the bare origin is not enough. +- Forgetting the callback path on the redirect URI. The bare origin is not enough. - For separately-hosted Flutter web, the Flutter dev server running on a random port. Pass `--web-port=` to `flutter run` so the origin is stable. -- A stale build on the standard [Web setup](./setup#web) flow. The app served at `/app` is a compiled snapshot, so a `redirectUri` change in `main.dart` takes effect only after re-running `flutter build web`. Rebuild and hard-reload the browser; the service worker can cache the old bundle. +- A stale build on the standard [Web setup](./setup#web) flow. The app Serverpod serves is a compiled snapshot, so a `redirectUri` change in `main.dart` takes effect only after re-running `flutter build web`. Rebuild and hard-reload the browser; the service worker can cache the old bundle. ## Production redirect URIs rejected by Google @@ -83,7 +81,7 @@ Common mistakes: **Cause:** The integrated route requires Serverpod and your Flutter web app to be on the **same origin** (same scheme, host, AND port). With `flutter run -d chrome`, Flutter runs on its own dev server port (e.g., `49660`) while Serverpod is on `8082`, so they are different origins. The browser blocks the callback page's `postMessage` across origins. -**Resolution:** Use the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) flow for local dev; it serves `auth.html` from Flutter's own dev server, same-origin with your Flutter app. For production, the integrated route works once Serverpod serves your Flutter build (template default via `FlutterRoute` on `/app`). +**Resolution:** Use the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) flow for local dev. It serves `auth.html` from Flutter's own dev server, same-origin with your Flutter app. For production, the integrated route works once Serverpod serves your Flutter build (via `FlutterRoute`, mounted at `/` on default projects, or `/app` when the website option is enabled). ## Sign-in callback never returns to the Flutter app @@ -93,7 +91,7 @@ Common mistakes: **Resolution:** -1. Confirm your callback page is reachable. Open the `redirectUri` directly in a browser tab; you should see the "Authentication complete" page. +1. Confirm your callback page is reachable. Open the `redirectUri` directly in a browser tab. You should see the "Authentication complete" page. - For the standard [Web setup](./setup#web), confirm `FlutterWebAuth2CallbackRoute` is registered on `pod.webServer` before `pod.start()` and that the path matches the URL you opened. - For the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) fallback, confirm `web/auth.html` exists in your Flutter project and contains the script described in [Web callback page (`auth.html`)](../../setup#web-callback-page-authhtml). If the file is missing, the redirect URL returns a 404. @@ -123,7 +121,7 @@ Every line of the JSON must be indented by at least one level more than `googleC ## Sign-in fails on Android with PlatformException(sign_in_failed) or clientConfigurationError -**Problem:** Google Sign-In throws a `PlatformException(sign_in_failed, ...)` or a `GoogleSignInException` with `clientConfigurationError` on Android but works on other platforms. +**Problem:** Google sign-in throws a `PlatformException(sign_in_failed, ...)` or a `GoogleSignInException` with `clientConfigurationError` on Android but works on other platforms. **Cause:** The SHA-1 fingerprint registered in your Android OAuth client does not match the signing key used to build the app. This commonly happens when switching between debug and release builds, or when the app is signed with a different keystore than the one registered. @@ -147,7 +145,7 @@ Every line of the JSON must be indented by at least one level more than `googleC ## Sign-in works in debug but fails in release -**Problem:** Google Sign-In works in debug mode but fails silently or with `sign_in_failed` in a release build. +**Problem:** Google sign-in works in debug mode but fails silently or with `sign_in_failed` in a release build. **Cause:** Debug and release builds use different signing keys. The SHA-1 fingerprint registered in your Android OAuth client only matches the debug keystore. @@ -167,7 +165,7 @@ client.auth.initializeGoogleSignIn( ); ``` -You can also supply it at build time with `--dart-define=GOOGLE_SERVER_CLIENT_ID=...`; see [Configuring Client IDs on the App](./customizations#configuring-client-ids-on-the-app). +You can also supply it at build time with `--dart-define=GOOGLE_SERVER_CLIENT_ID=...`. See [Configuring client IDs on the app](./customizations#configuring-client-ids-on-the-app). For Firebase-based projects using the Gradle plugin, make sure a Web application OAuth client exists in the same Google Cloud project and re-download `google-services.json` so it includes the web client entry. @@ -185,27 +183,19 @@ flutter run --dart-define=SERVER_URL=http://10.0.2.2:8080/ On the Android emulator, `10.0.2.2` maps to the host machine. On a physical device, use your computer's LAN IP address instead (e.g., `http://192.168.1.20:8080/`), with the phone on the same network. -## People API not enabled - -**Problem:** Sign-in completes on the client but the server returns an error when fetching user profile data. The server logs show a `403` or `PERMISSION_DENIED` error from the People API. - -**Cause:** The People API is not enabled in your Google Cloud project. - -**Resolution:** Navigate to the [People API page](https://console.cloud.google.com/apis/library/people.googleapis.com) and click **Enable**. - ## Server crashes on first Google sign-in with "no such table" **Problem:** The server builds and starts, but crashes when a user tries Google sign-in. The error cites a missing table (like `serverpod_auth_idp_google_account`). **Cause:** The database migration that creates the provider's tables was never created or applied. -**Resolution:** In the running `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +**Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. ## Google sign-in button does not appear -**Problem:** `SignInWidget` renders, but the Google button is missing. +**Problem:** The `SignInWidget` renders, but the Google button is missing. -**Cause:** `SignInWidget` shows the Google button when the client has a registered `GoogleIdpEndpoint` and the Google sign-in service is initialized. The common misses: +**Cause:** The `SignInWidget` shows the Google button when the client has a registered `GoogleIdpEndpoint` and the Google sign-in service is initialized. The common misses: - The app was hot reloaded after adding `initializeGoogleSignIn` to `main.dart`. Hot reload does not re-run `main()`, so the service is never initialized. - `GoogleIdpEndpoint` is missing on the server, or the client was not regenerated after adding it. @@ -215,15 +205,15 @@ On the Android emulator, `10.0.2.2` maps to the host machine. On a physical devi ## Lightweight sign-in (One Tap) not appearing -**Problem:** You enabled `attemptLightweightSignIn: true` but the One Tap prompt never appears on Web, or the silent sign-in doesn't trigger on mobile. +**Problem:** You enabled `attemptLightweightSignIn: true` but the silent sign-in doesn't trigger. -**Cause:** Lightweight sign-in requires the user to have previously signed in with Google on this device or browser. It also depends on platform-specific conditions: on Web, FedCM or One Tap must be supported by the browser; on mobile, the user must have a Google account configured on the device. +**Cause:** On web, `attemptLightweightSignIn` has no effect at all in this version, so nothing appearing there is expected. On Android and iOS, lightweight sign-in requires that the user has signed in with Google on the device before and has a Google account configured. -**Resolution:** This is expected behavior for first-time users. The lightweight sign-in prompt only appears for returning users. If the user dismisses One Tap multiple times, Google may suppress it temporarily. The regular sign-in button remains available as a fallback. +**Resolution:** On mobile, this is expected behavior for first-time users, since the prompt only appears for returning users, and Google may suppress it temporarily after repeated dismissals. The regular sign-in button remains available as a fallback. ## iOS sign-in prompt doesn't show -**Problem:** Tapping the Google Sign-In button on iOS has no effect or throws an error about a missing client ID. +**Problem:** Tapping the Google sign-in button on iOS has no effect or throws an error about a missing client ID. **Cause:** The `GIDClientID` or `GIDServerClientID` keys are missing or incorrect in `Info.plist`, or the URL scheme is not registered. @@ -233,7 +223,7 @@ On the Android emulator, `10.0.2.2` maps to the host machine. On a physical devi 2. Verify the URL scheme (`CFBundleURLSchemes`) contains the reversed client ID from the iOS plist (the `REVERSED_CLIENT_ID` value). 3. Clean the build and run again. -## clientId is required when initializing Google Sign-In on web with a redirect URI +## clientId is required when initializing Google Sign-In on web **Problem:** The Flutter app throws an `ArgumentError` at startup saying `clientId is required when initializing Google Sign-In on web with a redirect URI`. @@ -258,10 +248,10 @@ Or: flutter run --dart-define=GOOGLE_CLIENT_ID=your-web-client-id.apps.googleusercontent.com ... ``` -## Google API calls fail after one hour on Web +## Google API calls fail after one hour -**Problem:** Your app calls Google APIs (e.g., Calendar, Drive) using the access token from sign-in, but requests start returning `401 Unauthorized` after about an hour. This only affects the Web platform. +**Problem:** Your server calls Google APIs (e.g., Calendar, Drive) with the access token captured during sign-in, but requests start returning `401 Unauthorized` after about an hour. -**Cause:** On Web, the `accessToken` returned by the underlying sign-in library expires after 3,600 seconds (one hour) and is not automatically refreshed. +**Cause:** Google access tokens expire after 3,600 seconds (one hour). Serverpod captures the token during sign-in for the `getExtraGoogleInfoCallback`, and does not refresh it afterwards. -**Resolution:** When making Google API calls on Web, check the token age and prompt the user to re-authenticate if the token has expired. On mobile platforms, the token is refreshed automatically and this is not an issue. +**Resolution:** Fetch what you need inside `getExtraGoogleInfoCallback` while the token is fresh. For ongoing access, ask the user to sign in again, or run your own token exchange in a custom endpoint so you control the refresh token. diff --git a/docs/06-concepts/04-authentication/05-providers/04-apple/01-setup.md b/docs/06-concepts/04-authentication/05-providers/04-apple/01-setup.md index 9c0035e1..098474ac 100644 --- a/docs/06-concepts/04-authentication/05-providers/04-apple/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/04-apple/01-setup.md @@ -15,7 +15,7 @@ Before you start, make sure you have: ## Get your credentials -All platforms require an App ID and a Sign in with Apple key. Android and Web additionally require a Service ID. +All platforms require an App ID, a Sign in with Apple key, and a Service ID. Only the Android and web flow uses the Service ID at runtime, but sign-in initialization requires it everywhere. ### Register your App ID @@ -35,9 +35,7 @@ All platforms require an App ID and a Sign in with Apple key. Android and Web ad 6. Click **Continue**, then **Register**. -### Create a Service ID (Android and Web only) - -Skip this section if you are building for iOS or macOS only. +### Create a Service ID 1. In Certificates, Identifiers & Profiles, click **Identifiers → +**. @@ -47,7 +45,7 @@ Skip this section if you are building for iOS or macOS only. 3. Enter a description and a unique **Identifier** (e.g. `com.example.service`). This value becomes your `serviceIdentifier`. Click **Continue**, then **Register**. -4. Click on the Service ID you just created. Check **Sign in with Apple** and click **Configure**. +4. Click on the Service ID you created above. Check **Sign in with Apple** and click **Configure**. 5. In the modal, set: - **Primary App ID**: the App ID from the previous section @@ -63,7 +61,7 @@ All return URLs must use **HTTPS**. Apple rejects HTTP URLs, including `localhos ::: :::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. +Register the value you will use for `appleRedirectUri`, the server callback route, under **Return URLs**. Apple validates that redirect. The separate `appleWebRedirectUri` is a page in your own web app that the server sends the browser to afterwards, so Apple never sees it and it does not belong here. ::: ### Create a Sign in with Apple key @@ -104,7 +102,7 @@ development: -----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg... -----END PRIVATE KEY----- - # Web only (server callback route). + # Web only (the web app URL the server redirects the browser back to). appleWebRedirectUri: 'https://example.com/auth/apple-complete' # Android only. appleAndroidPackageIdentifier: 'com.example.app' @@ -163,7 +161,7 @@ The `webAuthenticationCallbackRoutePath` must match the **Return URL** you regis ### 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: +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 your app can call them: ```dart import 'package:serverpod_auth_idp_server/providers/apple.dart'; @@ -179,7 +177,7 @@ Start the server from your server project directory (e.g., `my_project_server/`) serverpod start ``` -Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create and apply the migration. :::note 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). @@ -217,17 +215,18 @@ Enable the Sign in with Apple capability in your Xcode project: 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`: +The redirect URI and `appleAndroidPackageIdentifier` were already configured in the [Store your credentials](#store-your-credentials) and [Service ID](#create-a-service-id) steps. The only remaining step is to register the `signinwithapple` URI scheme in your `AndroidManifest.xml`: ```xml - + + ``` @@ -244,7 +243,7 @@ Sign in with Apple on Web requires the Apple JS SDK. Add the following script to ``` -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. +The redirect URI and `appleWebRedirectUri` were already configured in the [Store your credentials](#store-your-credentials) and [Service ID](#create-a-service-id) steps. ## Present the authentication UI @@ -257,7 +256,7 @@ client.auth.initialize(); client.auth.initializeAppleSignIn(); ``` -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`: +The sign-in service needs your Service ID and redirect URI on every platform, even though only the web and Android flow uses them. Pass them as build-time environment variables using `--dart-define`: ```bash flutter run \ @@ -266,7 +265,7 @@ flutter run \ --dart-define="APPLE_REDIRECT_URI=https://example.com/auth/callback" ``` -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. +Use the same values you configured in the [Service ID](#create-a-service-id) 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. @@ -326,7 +325,7 @@ Add your production domain and callback URL to the Service ID. The development t ### Set production credentials -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. +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. @@ -351,11 +350,11 @@ 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. +Run these from your linked server project directory, or pass `--project ` on each call. See the [Serverpod Cloud passwords guide](/cloud/concepts/passwords-secrets-env-vars) for project linking and other options. -### Update client builds +### Update app builds -For Web and Android release builds, pass the production Service ID and redirect URI via `--dart-define`: +Release builds need the production Service ID and redirect URI on every platform, the same as during development. Pass them via `--dart-define`: ```bash flutter build web \ diff --git a/docs/06-concepts/04-authentication/05-providers/04-apple/02-customizations.md b/docs/06-concepts/04-authentication/05-providers/04-apple/02-customizations.md index e32c9ccf..09dde0c2 100644 --- a/docs/06-concepts/04-authentication/05-providers/04-apple/02-customizations.md +++ b/docs/06-concepts/04-authentication/05-providers/04-apple/02-customizations.md @@ -36,7 +36,18 @@ final appleIdpConfig = AppleIdpConfig( ### 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. +For Apple-specific logic, use `onAfterAppleAccountCreated` on `AppleIdpConfig`. It receives the created `AppleAccount` row, so you can read the Apple identifier and the name Apple returned on first sign-in. + +```dart +AppleIdpConfigFromPasswords( + onAfterAppleAccountCreated: + (session, authUser, appleAccount, {required transaction}) async { + session.log('Apple account created: ${appleAccount.userIdentifier}'); + }, +) +``` + +For logic that should run whichever provider the user signed in with, use [`onAfterAuthUserCreated`](../../working-with-users#reacting-to-the-user-created-event) on `AuthUsersConfig` instead. It fires the first time any provider creates an auth user. ```dart pod.initializeAuthServices( @@ -66,11 +77,7 @@ This callback runs inside the same database transaction as the auth user creatio ### 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: +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, using the `pod.configureAppleIdpRoutes()` method: ```dart pod.configureAppleIdpRoutes( @@ -80,15 +87,15 @@ pod.configureAppleIdpRoutes( ``` - `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. +- `webAuthenticationCallbackRoutePath` (default: `'/auth/apple/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. +When a user revokes access from their Apple ID settings, Apple sends a notification to `revokedNotificationRoutePath`. Registering the route is enough: Serverpod revokes the Apple authorization and the tokens it issued through Apple sign-in for that user. Clean up only your own derived records. ::: ### 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: +Your app 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( @@ -100,12 +107,12 @@ client.auth.initializeAppleSignIn( 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. +Only the web and Android flow consumes these values, but `initializeAppleSignIn` requires them on every platform. Pass them (or the matching dart-defines) even in an iOS-only app, or initialization throws. ::: #### Using environment variables -The build variables `APPLE_SERVICE_IDENTIFIER` and `APPLE_REDIRECT_URI` are read by `initializeAppleSignIn()` on web and Android: +The build variables `APPLE_SERVICE_IDENTIFIER` and `APPLE_REDIRECT_URI` are read by `initializeAppleSignIn()` whenever you do not pass the values as parameters: - `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`) @@ -132,11 +139,11 @@ You can set `--dart-define` values in your IDE run configuration or CI/CD pipeli | 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. | +| `serviceIdentifier` | `String` | Yes | `appleServiceIdentifier` | The Services ID identifier (e.g. `com.example.service`). Required on every platform, though only the Android and web OAuth flow uses it. | | `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. | +| `redirectUri` | `String` | Yes | `appleRedirectUri` | The server callback route Apple redirects to after sign-in. Sent with every authorization-code exchange, and validated by Apple in the Android and web flow. 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. | +| `androidPackageIdentifier` | `String?` | Android only | `appleAndroidPackageIdentifier` | The Android package name (e.g. `com.example.app`). When set, the callback route redirects Android sign-ins back to the app via an intent URI. | diff --git a/docs/06-concepts/04-authentication/05-providers/04-apple/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/04-apple/03-customizing-the-ui.md index 05d166a9..cd330032 100644 --- a/docs/06-concepts/04-authentication/05-providers/04-apple/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/04-apple/03-customizing-the-ui.md @@ -15,8 +15,13 @@ SignInWidget( client: client, appleSignInWidget: AppleSignInWidget( client: client, - // Customize the widget - style: AppleButtonStyle.black, + // Shape and label survive inside SignInWidget, unless its buttonStyle + // sets them. Brand colors do not. + shape: SignInButtonShape.rounded, + text: SignInButtonTextVariant.signInWith, + // A custom widget replaces the built-in handling, so pass your own callbacks. + onAuthenticated: () { /* ... */ }, + onError: (error) { /* ... */ }, ), ) ``` @@ -29,18 +34,23 @@ The `AppleSignInWidget` handles the complete Apple Sign-In flow for iOS, macOS, You can customize the widget's appearance and behavior: ```dart +// AppleIDAuthorizationScopes comes from the sign_in_with_apple package. +// Add it to your app's dependencies to import it. +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; + AppleSignInWidget( client: client, - // Button customization - type: AppleButtonText.signIn, // or signInWith, continue, signUp - style: AppleButtonStyle.black, // or white - size: AppleButtonSize.large, // or small, medium - shape: AppleButtonShape.rectangular, // or pill - logoAlignment: AppleButtonLogoAlignment.left, // or center - minimumWidth: 200, // or null for automatic width - - // Scopes to request from Apple - // These are the default, and the only ones supported by Apple Sign-In. + // Button customization. The values shown are the defaults. + style: AppleButtonStyle.black, // or white, whiteOutlined + size: SignInButtonSize.large, // or medium, small + text: SignInButtonTextVariant.continueWith, // or signInWith, signUpWith, signIn + shape: SignInButtonShape.pill, // or rounded, rectangular + logoAlignment: SignInButtonLogoAlignment.center, // or left + minimumWidth: 240, // at most 400 + textStyle: null, // TextStyle for the label + + // Scopes to request from Apple. + // These are the default, and the only ones Sign in with Apple supports. scopes: const [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName, @@ -65,6 +75,7 @@ For more control over the UI, you can use the `AppleAuthController` class, which ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; +// Also import sign_in_with_apple here for AppleIDAuthorizationScopes. final controller = AppleAuthController( client: client, onAuthenticated: () { @@ -86,7 +97,7 @@ final controller = AppleAuthController( await controller.signIn(); ``` -### AppleAuthController State Management +### AppleAuthController state management Your widget should render the appropriate UI based on the `state` property of the controller. You can also use the below state properties to build your UI: @@ -111,7 +122,7 @@ controller.addListener(() { }); ``` -#### AppleAuthController States +#### AppleAuthController states - `AppleAuthState.idle` - Ready for user interaction. - `AppleAuthState.loading` - Processing a sign-in request. diff --git a/docs/06-concepts/04-authentication/05-providers/04-apple/04-troubleshooting.md b/docs/06-concepts/04-authentication/05-providers/04-apple/04-troubleshooting.md index 7b30f413..d3b09b14 100644 --- a/docs/06-concepts/04-authentication/05-providers/04-apple/04-troubleshooting.md +++ b/docs/06-concepts/04-authentication/05-providers/04-apple/04-troubleshooting.md @@ -14,8 +14,8 @@ 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** 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*). +* [ ] Create a **Service ID** for OAuth. +* [ ] On the Service ID, check **Sign in with Apple**, click **Configure**, and select your **Primary App ID**. * [ ] 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. @@ -28,7 +28,7 @@ Go through this before investigating a specific error. Most problems come from a * [ ] 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/`. -* [ ] Start the server with `serverpod start`, then create and apply the migration (press **M**, then **A**). +* [ ] Start the server with `serverpod start`, then create and apply the migration (press **M**). #### Client @@ -36,7 +36,7 @@ Go through this before investigating a specific error. Most problems come from a * [ ] 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*). +* [ ] Pass **`APPLE_SERVICE_IDENTIFIER`** and **`APPLE_REDIRECT_URI`** via `--dart-define`. Initialization throws an `ArgumentError` without them, on every platform. * [ ] 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. @@ -61,7 +61,7 @@ Alternatively, set `appleKey` via the `SERVERPOD_PASSWORD_appleKey` environment **Problem:** Sign-in was working for months, then suddenly fails with `invalid_client` and you haven't changed code. -**Cause:** `appleKey` has a pre-generated client secret JWT, not the raw `.p8` key. Apple makes JWTs expire after six months. When it expires, all sign-ins fail. +**Cause:** The `appleKey` value holds a pre-generated client secret JWT, not the raw `.p8` key. Apple makes JWTs expire after six months. When it expires, all sign-ins fail. **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). @@ -69,17 +69,15 @@ Alternatively, set `appleKey` via the `SERVERPOD_PASSWORD_appleKey` environment **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: +**Cause:** Apple's authorization codes are single-use and expire after five 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 @@ -109,13 +107,14 @@ If you use `--dart-define`, confirm `APPLE_SERVICE_IDENTIFIER` is the Services I ```xml - + + ``` @@ -126,7 +125,7 @@ If you use `--dart-define`, confirm `APPLE_SERVICE_IDENTIFIER` is the Services I **Cause:** The database migration that creates the provider's tables was never created or applied. -**Resolution:** In the running `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +**Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. ## Apple rejects the redirect URI with `invalid_request` @@ -184,7 +183,6 @@ If you use `--dart-define`, confirm `APPLE_SERVICE_IDENTIFIER` is the Services I ``` -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" @@ -203,6 +201,6 @@ The `crossorigin="anonymous"` attribute is needed because Flutter's service work **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. -**Cause:** Your server receives Apple's revocation notification but doesn't terminate the user's active sessions. +**Cause:** Apple's revocation notification never reaches your server. Once it does, Serverpod revokes the Apple authorization and the tokens it issued through Apple sign-in automatically. -**Resolution:** When you receive a revocation notification at the route set using `pod.configureAppleIdpRoutes(revokedNotificationRoutePath: ...)`, look up the user by the `sub` value in the payload and invalidate all their sessions. See [Processing changes for Sign in with Apple accounts](https://developer.apple.com/documentation/signinwithapple/processing-changes-for-sign-in-with-apple-accounts) for how the notification works. +**Resolution:** Check that `pod.configureAppleIdpRoutes()` registers a `revokedNotificationRoutePath`, that the route's public HTTPS URL is registered as the server-to-server notification endpoint in the Apple Developer Portal, and that the URL is reachable from the internet. See [Processing changes for Sign in with Apple accounts](https://developer.apple.com/documentation/signinwithapple/processing-changes-for-sign-in-with-apple-accounts) for how the notification works. diff --git a/docs/06-concepts/04-authentication/05-providers/05-facebook/01-setup.md b/docs/06-concepts/04-authentication/05-providers/05-facebook/01-setup.md index 432b1ab1..b05a0901 100644 --- a/docs/06-concepts/04-authentication/05-providers/05-facebook/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/05-facebook/01-setup.md @@ -79,7 +79,7 @@ Save your changes after completing the configuration. - **Client token** :::tip -The **App secret** is sensitive. Keep it confidential and never commit it to version control. The **Client token** is required for some platforms (especially mobile and web). +The **App secret** is sensitive. Keep it confidential and never commit it to version control. The **Client token** is used by the mobile SDKs on Android and iOS. Web and macOS initialize with the App ID alone. ::: ## Server-side configuration @@ -131,7 +131,7 @@ If you need more control over how the credentials are loaded, use `FacebookIdpCo ### Create the endpoint -Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/facebook_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so the Flutter client can call them to complete the authentication flow: +Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/facebook_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so your app can call them to complete the authentication flow: ```dart import 'package:serverpod_auth_idp_server/providers/facebook.dart'; @@ -147,7 +147,7 @@ Start the server from your server project directory (e.g., `my_project_server/`) serverpod start ``` -Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create and apply the migration. :::warning Skipping the migration will cause the server to crash at runtime when the Facebook 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). @@ -459,6 +459,7 @@ For more detailed macOS setup instructions, refer to the [flutter_facebook_auth Initialize the service in your app's `main()` function using the `initializeFacebookSignIn()` extension method on `FlutterAuthSessionManager`, on the line after `client.auth.initialize()`. ```dart +import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'package:serverpod_auth_idp_flutter_facebook/serverpod_auth_idp_flutter_facebook.dart'; import 'package:your_client/your_client.dart'; diff --git a/docs/06-concepts/04-authentication/05-providers/05-facebook/02-customizations.md b/docs/06-concepts/04-authentication/05-providers/05-facebook/02-customizations.md index 32404f4c..feb7dc68 100644 --- a/docs/06-concepts/04-authentication/05-providers/05-facebook/02-customizations.md +++ b/docs/06-concepts/04-authentication/05-providers/05-facebook/02-customizations.md @@ -57,7 +57,9 @@ The `facebookAccountDetailsValidation` callback receives a `FacebookAccountDetai | -------- | ---- | ----------- | | `userIdentifier` | `String` | The Facebook user's unique identifier (UID) | | `email` | `String?` | The user's email address (may be null) | -| `name` | `String?` | The user's display name from Facebook | +| `fullName` | `String?` | The user's full name from Facebook | +| `firstName` | `String?` | The user's first name | +| `lastName` | `String?` | The user's last name | | `image` | `Uri?` | URL to the user's profile image | Example of accessing these properties: @@ -66,7 +68,7 @@ Example of accessing these properties: facebookAccountDetailsValidation: (accountDetails) { print('Facebook UID: ${accountDetails.userIdentifier}'); print('Email: ${accountDetails.email}'); - print('Display name: ${accountDetails.name}'); + print('Display name: ${accountDetails.fullName}'); print('Profile image: ${accountDetails.image}'); // Custom validation logic @@ -102,6 +104,10 @@ Adding additional permissions may require App Review depending on the sensitivit ### Accessing Facebook APIs on the server +:::caution +The `getExtraFacebookInfoCallback` below runs on **every** sign-in, not only the first. Cache what you fetch, and guard external calls with `try`/`catch` so a provider outage does not block sign-in. +::: + On the server side, you can access Facebook APIs using the access token. The `getExtraFacebookInfoCallback` in `FacebookIdpConfig` receives the access token and can be used to call Facebook Graph APIs: ```dart @@ -149,7 +155,7 @@ This callback runs inside the same database transaction as the account creation. ::: :::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` in combination with `getExtraFacebookInfoCallback` to fetch and store the data you need before the auth user is created. +Scopes you assign here with `AuthServices.instance.authUsers.update()` do not apply to the login that is already in progress, because token issuance uses the scopes loaded before this callback runs. They take effect the next time the user signs in. To assign scopes at creation time instead, use `onBeforeAuthUserCreated` together with `getExtraFacebookInfoCallback`, which runs before the auth user is created. ::: ### Configuring Facebook sign-in on the app @@ -183,7 +189,7 @@ flutter run -d \ This approach is useful when you need to: -- Manage separate App IDs for different platforms (Android, iOS, Web, macOS) in a centralized way. +- Set the App ID for web and macOS builds, where it is read from Dart. On Android and iOS the Facebook SDK reads it from the native configuration files instead. - Avoid committing App IDs to version control. - Configure different credentials for different build environments (development, staging, production). diff --git a/docs/06-concepts/04-authentication/05-providers/05-facebook/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/05-facebook/03-customizing-the-ui.md index 0ba314a1..f02d950e 100644 --- a/docs/06-concepts/04-authentication/05-providers/05-facebook/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/05-facebook/03-customizing-the-ui.md @@ -18,17 +18,19 @@ The `FacebookSignInWidget` handles the complete Facebook Sign-In flow for iOS, A You can customize the widget's appearance and behavior: ```dart +import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'package:serverpod_auth_idp_flutter_facebook/serverpod_auth_idp_flutter_facebook.dart'; FacebookSignInWidget( client: client, - // Button customization - type: FacebookButtonText.continueWith, // or signinWith, signupWith, signIn + // Button customization. The values shown are the defaults. style: FacebookButtonStyle.blue, // or white - size: FacebookButtonSize.large, // or medium, small - shape: FacebookButtonShape.pill, // or rectangular - logoAlignment: FacebookButtonLogoAlignment.center, // or left - minimumWidth: 240, // in pixels, max 400 + size: SignInButtonSize.large, // or medium, small + text: SignInButtonTextVariant.continueWith, // or signInWith, signUpWith, signIn + shape: SignInButtonShape.pill, // or rounded, rectangular + logoAlignment: SignInButtonLogoAlignment.center, // or left + minimumWidth: 240, // at most 400 + textStyle: null, // TextStyle for the label // Permissions to request from Facebook // These are the default permissions. @@ -51,6 +53,7 @@ FacebookSignInWidget( For more control over the UI, you can use the `FacebookAuthController` class, which provides all the authentication logic without any UI components. This allows you to build a completely custom authentication interface. ```dart +import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'package:serverpod_auth_idp_flutter_facebook/serverpod_auth_idp_flutter_facebook.dart'; final controller = FacebookAuthController( diff --git a/docs/06-concepts/04-authentication/05-providers/05-facebook/04-troubleshooting.md b/docs/06-concepts/04-authentication/05-providers/05-facebook/04-troubleshooting.md index e6fd685e..c4411db6 100644 --- a/docs/06-concepts/04-authentication/05-providers/05-facebook/04-troubleshooting.md +++ b/docs/06-concepts/04-authentication/05-providers/05-facebook/04-troubleshooting.md @@ -24,7 +24,7 @@ Go through this before investigating a specific error. Most problems come from a - [ ] Added `facebookAppId` and `facebookAppSecret` to `config/passwords.yaml` under the matching environment (`development:` for local, `production:` for prod), or set the matching `SERVERPOD_PASSWORD_facebookAppId` and `SERVERPOD_PASSWORD_facebookAppSecret` environment variables. - [ ] Added `FacebookIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. - [ ] Created a `FacebookIdpEndpoint` file in `lib/src/auth/`. -- [ ] Started the server with `serverpod start`, then created and applied the migration (pressed **M**, then **A**). +- [ ] Started the server with `serverpod start`, then created and applied the migration (pressed **M**). #### Client @@ -126,7 +126,7 @@ Quotes are required because the values are strings. On Serverpod Cloud, set them **Cause:** The database migration that creates the provider's tables was never created or applied. -**Resolution:** In the running `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. See [Start the server](./setup#start-the-server). +**Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. See [Start the server](./setup#start-the-server). ## Sign-in works in dev but fails after deploy diff --git a/docs/06-concepts/04-authentication/05-providers/06-firebase/01-setup.md b/docs/06-concepts/04-authentication/05-providers/06-firebase/01-setup.md index 25bcb801..e0308a93 100644 --- a/docs/06-concepts/04-authentication/05-providers/06-firebase/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/06-firebase/01-setup.md @@ -96,7 +96,7 @@ Production credentials are covered in [Publishing to production](#publishing-to- ::: :::warning -**Indent the JSON consistently under the `|` block scalar.** Any indentation error will silently break the JSON parser, and authentication will fail at runtime. Mixing tabs and spaces is a common cause. +**Indent the JSON consistently under the `|` block scalar.** Any indentation error makes the JSON fail to parse, so the server throws at startup when the credentials load. Mixing tabs and spaces is a common cause. ::: ## Server-side configuration @@ -147,7 +147,7 @@ Start the server from your server project directory (e.g., `my_project_server/`) serverpod start ``` -Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create and apply the migration. :::note Skipping the migration will cause the server to crash at runtime when the Firebase 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). @@ -199,6 +199,7 @@ In your Flutter app's `main.dart` file (e.g., `my_project_flutter/lib/main.dart` import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:serverpod_flutter/serverpod_flutter.dart'; +import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; import 'package:serverpod_auth_idp_flutter_firebase/serverpod_auth_idp_flutter_firebase.dart'; import 'package:your_client/your_client.dart'; import 'firebase_options.dart'; @@ -270,6 +271,22 @@ class _SignInScreenState extends State { backgroundColor: Colors.red, ), ); + + // Rebuild when the controller's state changes, so the gate swaps to the + // app once login() succeeds. The controller does not notify on sign-out. + // Listen to client.auth.authInfoListenable as well if you need that. + controller.addListener(_onControllerChanged); + } + + void _onControllerChanged() { + if (mounted) setState(() {}); + } + + @override + void dispose() { + controller.removeListener(_onControllerChanged); + controller.dispose(); + super.dispose(); } /// Handle changes in authentication state: Log in a new Firebase user with Serverpod, if any. @@ -354,7 +371,7 @@ Use `scloud password set` and pass the JSON file with `--from-file`: scloud password set firebaseServiceAccountKey --from-file ./firebase-service-account.json ``` -Run this 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 the [passwords vs secrets vs variables](https://docs.serverpod.dev/cloud/guides/passwords#passwords-vs-secrets-vs-variables) note for when to use each. +Run this from your linked server project directory, or pass `--project ` on each call. See the [Serverpod Cloud passwords guide](/cloud/concepts/passwords-secrets-env-vars) for project linking and when to use passwords, secrets, or variables. ### 3. Authorize your production domain diff --git a/docs/06-concepts/04-authentication/05-providers/06-firebase/02-customizations.md b/docs/06-concepts/04-authentication/05-providers/06-firebase/02-customizations.md index 6c759128..415bf877 100644 --- a/docs/06-concepts/04-authentication/05-providers/06-firebase/02-customizations.md +++ b/docs/06-concepts/04-authentication/05-providers/06-firebase/02-customizations.md @@ -56,7 +56,7 @@ final firebaseIdpConfig = FirebaseIdpConfig( Do not inline the service account fields (especially `private_key`) directly in source code. Load every sensitive field from a secure source such as `pod.getPassword()` (backed by `passwords.yaml` or `SERVERPOD_PASSWORD_*` environment variables) or a secrets manager. ::: -**Project ID only** (only token verification, no admin operations like deleting Firebase accounts): +**Project ID only** (Serverpod uses only this field and ignores the rest): ```dart final firebaseIdpConfig = FirebaseIdpConfig( @@ -67,21 +67,33 @@ final firebaseIdpConfig = FirebaseIdpConfig( ``` :::note -Only `projectId` is required to verify Firebase ID tokens. The full service account JSON is only needed if you also use the [admin operations](./admin-operations) on the server. +Only `projectId` is used from the service account JSON. The other fields are accepted so you can paste the downloaded file unchanged, but Serverpod does not use them. ID token signatures are verified against Google's public certificates, not against the service account key. ::: ## Custom account validation -You can customize the validation for Firebase account details before allowing sign-in. By default, the validation requires the email to be verified when present (phone-only authentication is allowed without an email). +You can customize the validation for Firebase account details before allowing sign-in. By default every account is accepted, including one whose email is not verified. Firebase Email/Password accounts start unverified and users usually sign in straight after signing up, so rejecting them would block that flow. + +To require a verified email instead, pass the built-in validator: + +```dart +final firebaseIdpConfig = FirebaseIdpConfigFromPasswords( + firebaseAccountDetailsValidation: FirebaseIdpConfig.requireVerifiedEmail, +); +``` + +It throws `FirebaseEmailNotVerifiedException`, which reaches the app so you can prompt the user to verify. Accounts with no email, such as phone sign-in, are still accepted. To customize validation, provide your own `firebaseAccountDetailsValidation` function: ```dart final firebaseIdpConfig = FirebaseIdpConfigFromPasswords( firebaseAccountDetailsValidation: (accountDetails) { - // Require verified email (even for phone auth) + // Require verified email (even for phone auth). Throw the serializable + // FirebaseEmailNotVerifiedException so the app can tell this case apart. + // A plain Exception reaches the app only as a generic server error. if (accountDetails.verifiedEmail != true) { - throw Exception('Email must be verified'); + throw FirebaseEmailNotVerifiedException(); } // Restrict to specific email domain @@ -110,7 +122,7 @@ Which properties are populated depends on the Firebase sign-in method the user c [`onBeforeAuthUserCreated`](https://pub.dev/documentation/serverpod_auth_idp_server/latest/core/AuthUsersConfig/onBeforeAuthUserCreated.html) and [`onAfterAuthUserCreated`](https://pub.dev/documentation/serverpod_auth_idp_server/latest/core/AuthUsersConfig/onAfterAuthUserCreated.html) are global callbacks on `AuthUsersConfig`. They fire for every identity provider, not just Firebase. See [Working with users](../../working-with-users#reacting-to-the-user-created-event) for full details. -The example below uses Firebase phone numbers as the trigger for assigning a `phone-verified` scope at sign-up, and persists the Firebase UID for later admin lookups: +The core callbacks cannot see Firebase account details, so provider-specific logic, such as a scope derived from the phone number, belongs in `onAfterFirebaseAccountCreated`, which receives the `FirebaseAccount`. The example below assigns a baseline scope to every new user: ```dart pod.initializeAuthServices( @@ -143,8 +155,26 @@ pod.initializeAuthServices( ); ``` +For the Firebase-specific hook, pass `onAfterFirebaseAccountCreated` to the provider config. It receives the created `FirebaseAccount`, so it can read the phone number or the Firebase UID: + +```dart +FirebaseIdpConfigFromPasswords( + onAfterFirebaseAccountCreated: + (session, authUser, firebaseAccount, {required transaction}) async { + if (firebaseAccount.phone != null) { + await AuthServices.instance.authUsers.update( + session, + authUserId: authUser.id, + scopes: {...authUser.scopes, Scope('phone-verified')}, + transaction: transaction, + ); + } + }, +) +``` + :::warning -Both callbacks run inside the same database transaction as the account creation. Throwing an exception inside either callback aborts the sign-up. Wrap external side-effects (email sending, analytics) in `try`/`catch` so a third-party outage does not block new sign-ups. +These callbacks run inside the same database transaction as the account creation. Throwing an exception inside a callback aborts the sign-up. Wrap external side-effects (email sending, analytics) in `try`/`catch` so a third-party outage does not block new sign-ups. ::: ## FirebaseIdpConfig parameter reference @@ -152,6 +182,6 @@ Both callbacks run inside the same database transaction as the account creation. | Parameter | Type | Required | Description | | ---------------------------------- | -------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `credentials` | `FirebaseServiceAccountCredentials` | Yes | Firebase service account credentials for verifying ID tokens. Can be loaded via `fromJsonString`, `fromJsonFile`, `fromJson`, or the default constructor with just `projectId`. When using `FirebaseIdpConfigFromPasswords`, this is loaded automatically from the `firebaseServiceAccountKey` key in `passwords.yaml` or the `SERVERPOD_PASSWORD_firebaseServiceAccountKey` environment variable. | -| `firebaseAccountDetailsValidation` | `FirebaseAccountDetailsValidation?` | No | Custom validation callback for Firebase account details before allowing sign-in. By default, validates that email is verified when present (phone-only auth is allowed). | +| `firebaseAccountDetailsValidation` | `FirebaseAccountDetailsValidation` | No | Custom validation callback for Firebase account details before allowing sign-in. By default all account details are accepted, including unverified emails. Pass `FirebaseIdpConfig.requireVerifiedEmail` to reject accounts whose email has not been verified. | | `onAfterFirebaseAccountCreated` | `AfterFirebaseAccountCreatedFunction?` | No | Callback invoked after a new Firebase account has been created and linked to an auth user. Receives the session, the created `AuthUserModel`, the `FirebaseAccount`, and the active `Transaction`. Runs inside the same database transaction as account creation, so the `transaction` can be used to perform additional database operations atomically with sign-up. | | `clockSkewTolerance` | `Duration` | No | Tolerance for clock skew when validating Firebase ID token timestamps. Defaults to the framework's default clock skew tolerance. | diff --git a/docs/06-concepts/04-authentication/05-providers/06-firebase/04-admin-operations.md b/docs/06-concepts/04-authentication/05-providers/06-firebase/04-admin-operations.md index e3a201f4..c85aa0f8 100644 --- a/docs/06-concepts/04-authentication/05-providers/06-firebase/04-admin-operations.md +++ b/docs/06-concepts/04-authentication/05-providers/06-firebase/04-admin-operations.md @@ -42,7 +42,8 @@ final accountByAuthUser = await admin.findAccountByAuthUserId( authUserId: authUserId, ); -final userId = await admin.findUserByFirebaseUserId( +// findUserByFirebaseUserId is static, unlike the instance methods above. +final userId = await FirebaseIdpAdmin.findUserByFirebaseUserId( session, userIdentifier: 'firebase-uid', ); @@ -91,7 +92,7 @@ Deleting a Firebase account only removes the link between Firebase authenticatio ## FirebaseIdpUtils -The `FirebaseIdpUtils` class provides a lower-level `authenticate` method for when you need to verify a Firebase ID token and create or update the associated Serverpod user in custom endpoint logic (outside the normal sign-in flow): +The `FirebaseIdpUtils` class provides a lower-level `authenticate` method for when you need to verify a Firebase ID token and find or create the associated Serverpod user in custom endpoint logic (outside the normal sign-in flow): ```dart final utils = firebaseIdp.utils; diff --git a/docs/06-concepts/04-authentication/05-providers/06-firebase/05-troubleshooting.md b/docs/06-concepts/04-authentication/05-providers/06-firebase/05-troubleshooting.md index 57a664d0..77066490 100644 --- a/docs/06-concepts/04-authentication/05-providers/06-firebase/05-troubleshooting.md +++ b/docs/06-concepts/04-authentication/05-providers/06-firebase/05-troubleshooting.md @@ -26,7 +26,7 @@ Go through this before investigating a specific error. Most problems come from a - [ ] Confirm the `project_id` inside `firebaseServiceAccountKey` matches the Firebase project the client is using. - [ ] Add `FirebaseIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. - [ ] Create a `FirebaseIdpEndpoint` file in `lib/src/auth/` extending `FirebaseIdpBaseEndpoint`. -- [ ] Start the server with `serverpod start`, then create and apply the migration (press **M**, then **A**). +- [ ] Start the server with `serverpod start`, then create and apply the migration (press **M**). #### Client @@ -43,18 +43,21 @@ Go through this before investigating a specific error. Most problems come from a **Cause:** The database migration that creates the provider's tables was never created or applied. -**Resolution:** In the running `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +**Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. ## Token verification fails with "invalid signature" **Problem:** The server rejects Firebase ID tokens with a signature verification error. -**Cause:** The service account key in `passwords.yaml` does not belong to the same Firebase project that the client is using, or the YAML indentation broke the JSON. +**Cause:** The ID token itself failed verification against Google's public certificates. The token was truncated or corrupted in transit, or what was sent is not a Firebase ID token. + +A project mismatch produces `Invalid issuer` or `Audience does not match` instead, and broken JSON in `passwords.yaml` fails at startup (see the parse-error entry below). **Resolution:** -1. Verify the `project_id` in your `firebaseServiceAccountKey` matches the project in `firebase_options.dart`. -2. Check that the JSON in `passwords.yaml` is properly indented under the `|` block scalar. All lines must be indented consistently. +1. Log the token length in the app before sending it, and confirm the server receives the same value. +2. Confirm the app sends the Firebase **ID token** (`getIdToken()`), not an access token or a custom token. +3. For `Invalid issuer` or `Audience does not match`, verify the `project_id` in your `firebaseServiceAccountKey` matches the project in `firebase_options.dart`. ## Token verification fails with "token expired" @@ -108,22 +111,22 @@ If you haven't run `flutterfire configure`, do so to generate the `firebase_opti 1. **Missing service account key:** The `firebaseServiceAccountKey` is not present in `passwords.yaml`, or the JSON is invalid. 2. **Missing endpoint:** You did not create the endpoint class extending `FirebaseIdpBaseEndpoint`. Without it, the client has no endpoint to call. -3. **Missing migration:** The provider's database tables don't exist yet. In the `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +3. **Missing migration:** The provider's database tables don't exist yet. In the `serverpod start` terminal, press **M** to create and apply the migration. 4. **Project mismatch:** The service account key belongs to a different Firebase project than the one configured in your Flutter app. Compare `project_id` in `firebaseServiceAccountKey` against the project in `firebase_options.dart`. 5. **App Check enabled prematurely:** If you enabled Firebase App Check before the client integration is in place, every request will be rejected with an App Check assertion error. Disable App Check until the client is wired up. ## Email validation rejects phone-only users -**Problem:** Users who sign in with phone authentication are rejected with a `FirebaseUserInfoMissingDataException`. +**Problem:** Users who sign in with phone authentication are rejected. The app receives `FirebaseEmailNotVerifiedException`, or a generic `FirebaseIdTokenVerificationException` when the validator throws a plain exception. -**Cause:** A custom `firebaseAccountDetailsValidation` callback requires a verified email, but phone-only users don't have an email. The default validation allows phone-only authentication. If you overrode the default with a stricter check, you need to account for phone-only sign-in. +**Cause:** A custom `firebaseAccountDetailsValidation` callback requires a verified email, but phone-only users don't have an email. The default validation accepts phone-only authentication, so this only happens with a custom validator. -**Resolution:** Update your validation to allow phone-only authentication by checking for the presence of an email before requiring verification: +**Resolution:** Guard on the presence of an email before requiring verification. The built-in `FirebaseIdpConfig.requireVerifiedEmail` does exactly this, or write it yourself: ```dart firebaseAccountDetailsValidation: (accountDetails) { if (accountDetails.email != null && accountDetails.verifiedEmail != true) { - throw FirebaseUserInfoMissingDataException(); + throw FirebaseEmailNotVerifiedException(); } }, ``` diff --git a/docs/06-concepts/04-authentication/05-providers/07-github/01-setup.md b/docs/06-concepts/04-authentication/05-providers/07-github/01-setup.md index 036ea40f..7dd87234 100644 --- a/docs/06-concepts/04-authentication/05-providers/07-github/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/07-github/01-setup.md @@ -36,14 +36,14 @@ The callback URL is where GitHub redirects the user after they authorize your ap 1. In the **Callback URL** field, enter the redirect URI for your app. GitHub Apps accept up to 10 entries, one per line. Add every platform you target: - - **iOS and Android**: `com.example.yourapp://auth` (a custom scheme registered in `AndroidManifest.xml` and `Info.plist`). + - **iOS, macOS, and Android**: `com.example.yourapp://auth`. Register the custom scheme in `AndroidManifest.xml`. iOS and macOS need no special configuration. - **Web**: `http://localhost:8082/auth/callback` locally, `https://my-awesome-project.serverpod.space/auth/callback` in production. ![Callback URL field](/img/authentication/providers/github/2-callback-url.png) -2. Leave **Expire user authorization tokens** enabled (GitHub's default). Token expiration is recommended for sign-in flows so leaked tokens have a short useful lifetime. Serverpod handles refreshing the token; you do not need to write any refresh logic. +2. Leave **Expire user authorization tokens** enabled (GitHub's default). Token expiration is recommended for sign-in flows so leaked tokens have a short useful lifetime. Serverpod uses the access token once during sign-in and does not store or refresh it, so expiry does not affect signed-in users. -3. Leave **Request user authorization (OAuth) during installation** unchecked unless you need the installation of your Flutter app to immediately trigger an OAuth sign-in. +3. Leave **Request user authorization (OAuth) during installation** unchecked unless you need users to authorize immediately when they install the GitHub App on their account or organization. ### Disable webhooks @@ -137,7 +137,7 @@ If you need more control over how the credentials are loaded, use `GitHubIdpConf ### Create the endpoint -Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/github_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so the Flutter client can call them to complete the authentication flow: +Create a new endpoint file in your server project (e.g., `my_project_server/lib/src/auth/github_idp_endpoint.dart`) alongside the existing auth endpoints. Extending the base class registers the sign-in methods with your server so your app can call them to complete the authentication flow: ```dart import 'package:serverpod_auth_idp_server/providers/github.dart'; @@ -153,7 +153,7 @@ Start the server from your server project directory (e.g., `my_project_server/`) serverpod start ``` -Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +Then create and apply the migration for the provider's tables: in the `serverpod start` terminal, press **M** to create and apply the migration. :::warning Skipping the migration will cause the server to crash at runtime when the GitHub 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). @@ -202,11 +202,11 @@ The scheme in `AndroidManifest.xml` must exactly match the scheme in your GitHub On web, GitHub completes sign-in by redirecting the browser to a callback URL you control. This flow requires Serverpod to serve your Flutter web app on the **same origin** as the callback route. To test locally, build your Flutter web app into Serverpod's `web/app/` directory: ```bash -flutter build web --output ../my_project_server/web/app # from your Flutter project +flutter build web --base-href / --output ../my_project_server/web/app # from your Flutter project serverpod start --no-flutter # from your server project ``` -Open `http://localhost:8082/app` to test. Pass `--no-flutter` so `serverpod start` serves your prebuilt web app instead of launching a separate `flutter run -d chrome` instance, which runs on a different port and would not share Serverpod's origin. For hot-reload workflows, use the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) flow instead. +Open `http://localhost:8082/` to test. Projects created with the website option serve the app under `/app` instead. Build those with `--base-href /app/` and open `/app`. Pass `--no-flutter` so `serverpod start` serves your prebuilt web app instead of launching a separate `flutter run -d chrome` instance, which runs on a different port and would not share Serverpod's origin. For hot-reload workflows, use the [separately-hosted Flutter web](./customizations#separately-hosted-flutter-web) flow instead. The examples below use port `8082` (Serverpod's default from `config/development.yaml`). @@ -261,7 +261,7 @@ void main() async { await client.auth.initialize(); await client.auth.initializeGitHubSignIn( clientId: 'your-github-client-id', - redirectUri: Uri.parse('com.example.yourapp://auth'), + redirectUri: 'com.example.yourapp://auth', ); runApp(const MyApp()); @@ -276,7 +276,7 @@ To keep these values out of `main.dart` and vary them per build, read them from ### Show the GitHub sign-in button -The Serverpod template ships with a `SignInScreen` widget at `lib/screens/sign_in_screen.dart`. It listens to `client.auth.authInfoListenable` and swaps between `SignInWidget` while the user is signed out and the `child` you pass it once they sign in. `SignInWidget` auto-detects which identity provider endpoints are registered on the server, so once `GitHubIdpEndpoint` is exposed and the client code has been regenerated, the GitHub button appears inside it. +The Serverpod template ships with a `SignInScreen` widget at `lib/screens/sign_in_screen.dart`. It listens to `client.auth.authInfoListenable` and swaps between `SignInWidget` while the user is signed out and the `child` you pass it once they sign in. The `SignInWidget` auto-detects which identity provider endpoints are registered on the server, so once `GitHubIdpEndpoint` is exposed and the client code has been regenerated, the GitHub button appears inside it. To customize the GitHub button or build a fully custom UI, see [Customizing the UI](./customizing-the-ui). @@ -293,7 +293,7 @@ Go back to your GitHub App's settings and add your production callback URL to ** ### 2. Set production credentials -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. +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. If you use the same GitHub App for development and production, you can reuse the same `githubClientId` and `githubClientSecret`. For separate environments, [register a second GitHub App](https://github.com/settings/apps/new) first and use its values. @@ -319,7 +319,7 @@ scloud password set githubClientId your-github-client-id scloud password set githubClientSecret --from-file path/to/github-client-secret.txt ``` -Run these from your linked server project directory, or pass `--project ` on each call (the flag is required unless the project is linked). See the [Serverpod Cloud passwords guide](https://docs.serverpod.dev/cloud/guides/passwords) for project linking and other options. +Run these from your linked server project directory, or pass `--project ` on each call (the flag is required unless the project is linked). See the [Serverpod Cloud passwords guide](/cloud/concepts/passwords-secrets-env-vars) for project linking and other options. ### 3. Verify the redirect URI in the Flutter build diff --git a/docs/06-concepts/04-authentication/05-providers/07-github/02-customizations.md b/docs/06-concepts/04-authentication/05-providers/07-github/02-customizations.md index a0c0590f..4e069e87 100644 --- a/docs/06-concepts/04-authentication/05-providers/07-github/02-customizations.md +++ b/docs/06-concepts/04-authentication/05-providers/07-github/02-customizations.md @@ -144,12 +144,12 @@ This callback runs inside the same database transaction as the account creation. ::: :::caution -If you need to assign Serverpod scopes based on provider account data, updating the database alone (via `AuthServices.instance.authUsers.update()`) is **not enough** for the current login session. 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 scope assignment at creation time, use [`onBeforeAuthUserCreated`](#reacting-to-auth-user-creation) in combination with `getExtraGitHubInfoCallback` to fetch and store the data you need before the auth user is created. +Scopes you assign here with `AuthServices.instance.authUsers.update()` do not apply to the login that is already in progress, because token issuance uses the scopes loaded before this callback runs. They take effect the next time the user signs in. To force them sooner, revoke the user's tokens so they sign in again. The `onBeforeAuthUserCreated` hook, covered below, assigns scopes at creation time, but it cannot use GitHub data, because `getExtraGitHubInfoCallback` runs after the auth user is created. ::: ### Reacting to auth user creation -The `onBeforeAuthUserCreated` and `onAfterAuthUserCreated` hooks are global callbacks configured on `AuthUsersConfig` in `initializeAuthServices`. They are not specific to GitHub; they fire for every identity provider. See the [working with users](../../working-with-users#reacting-to-the-user-created-event) page for full details. +The `onBeforeAuthUserCreated` and `onAfterAuthUserCreated` hooks are global callbacks configured on `AuthUsersConfig` in `initializeAuthServices`. They are not specific to GitHub. They fire for every identity provider. See the [working with users](../../working-with-users#reacting-to-the-user-created-event) page for full details. The `onBeforeAuthUserCreated` callback receives the default scopes and blocked status for the new user and must return the final values. Use it to assign custom scopes at creation time: @@ -193,7 +193,7 @@ You can pass the `clientId` and `redirectUri` directly when initializing the Git ```dart await client.auth.initializeGitHubSignIn( clientId: 'your-github-client-id', - redirectUri: Uri.parse('com.example.yourapp://auth'), + redirectUri: 'com.example.yourapp://auth', ); ``` diff --git a/docs/06-concepts/04-authentication/05-providers/07-github/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/07-github/03-customizing-the-ui.md index 67e29756..8ac633c7 100644 --- a/docs/06-concepts/04-authentication/05-providers/07-github/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/07-github/03-customizing-the-ui.md @@ -15,8 +15,13 @@ SignInWidget( client: client, githubSignInWidget: GitHubSignInWidget( client: client, - // Customize the widget - style: GitHubButtonStyle.black, + // Shape and label survive inside SignInWidget, unless its buttonStyle + // sets them. Brand colors do not. + shape: SignInButtonShape.rounded, + text: SignInButtonTextVariant.signInWith, + // A custom widget replaces the built-in handling, so pass your own callbacks. + onAuthenticated: () { /* ... */ }, + onError: (error) { /* ... */ }, ), ) ``` @@ -32,14 +37,14 @@ You can customize the widget's appearance and behavior: ```dart GitHubSignInWidget( client: client, - // Button customization - text: GitHubButtonText.continueWith, // or signIn, signUp - type: GitHubButtonType.standard, // or icon + // Button customization. The values shown are the defaults. style: GitHubButtonStyle.black, // or white - size: GitHubButtonSize.large, // or medium - shape: GitHubButtonShape.pill, // or rectangular, rounded - logoAlignment: GitHubButtonLogoAlignment.left, // or center - minimumWidth: 240, // or null for automatic width + size: SignInButtonSize.large, // or medium, small + text: SignInButtonTextVariant.continueWith, // or signInWith, signUpWith, signIn + shape: SignInButtonShape.pill, // or rounded, rectangular + logoAlignment: SignInButtonLogoAlignment.center, // or left + minimumWidth: 240, // at most 400 + textStyle: null, // TextStyle for the label // Scopes to request from GitHub // These are the default. diff --git a/docs/06-concepts/04-authentication/05-providers/07-github/04-troubleshooting.md b/docs/06-concepts/04-authentication/05-providers/07-github/04-troubleshooting.md index 3154eb40..b120bb7d 100644 --- a/docs/06-concepts/04-authentication/05-providers/07-github/04-troubleshooting.md +++ b/docs/06-concepts/04-authentication/05-providers/07-github/04-troubleshooting.md @@ -25,7 +25,7 @@ Go through this before investigating a specific error. Most problems come from a - [ ] Added `githubClientId` and `githubClientSecret` to `config/passwords.yaml` under the matching environment (`development:` for local, `production:` for prod), or set the matching `SERVERPOD_PASSWORD_githubClientId` and `SERVERPOD_PASSWORD_githubClientSecret` environment variables. - [ ] Added `GitHubIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. - [ ] Created a `GitHubIdpEndpoint` file in `lib/src/auth/`. -- [ ] Started the server with `serverpod start`, then created and applied the migration (pressed **M**, then **A**). +- [ ] Started the server with `serverpod start`, then created and applied the migration (pressed **M**). #### Client @@ -59,14 +59,14 @@ Common mistakes: **Resolution:** -- **Web (Serverpod-hosted Flutter)**: Confirm `pod.webServer.addRoute(FlutterWebAuth2CallbackRoute(host: ...), '/auth/callback')` is called in `server.dart` and that `host` matches the domain serving your Flutter web app. Open `https://your-domain.com/auth/callback` directly in a browser tab; you should see the "Authentication complete" page. Also confirm Flutter web and the route share scheme + host + port (`postMessage` is blocked across origins). -- **Web (separately-hosted Flutter)**: Confirm `web/auth.html` exists in your Flutter project and contains the script shown in [Web](./setup#web). Open the redirect URL directly in a browser tab; you should see the "Authentication complete" page. +- **Web (Serverpod-hosted Flutter)**: Confirm `FlutterWebAuth2CallbackRoute` is registered on `pod.webServer` and the provider redirects to that route's URL. +- **Web (separately-hosted Flutter)**: Confirm `web/auth.html` exists in your Flutter project and contains the callback script from [Web callback page (`auth.html`)](../../setup#web-callback-page-authhtml). - **Android**: Verify the `` value in `AndroidManifest.xml` matches the scheme in your callback URL exactly. - **iOS / macOS**: Universal Links require HTTPS callback URLs and associated-domain entitlements. Standard custom-scheme callbacks work without extra configuration. ## Sign-in succeeds but the user has no email -**Problem:** The user signs in successfully on the client, but the server-side `GitHubAccountDetails.email` value is `null`. +**Problem:** The user signs in successfully in the app, but the server-side `GitHubAccountDetails.email` value is `null`. **Cause:** GitHub users can keep their email private, and the OAuth response will return `null` for `email` in that case. Your app may have a custom validator that rejects accounts without an email and blocks the sign-in. @@ -87,7 +87,7 @@ Common mistakes: ```dart await client.auth.initializeGitHubSignIn( clientId: 'your-github-client-id', - redirectUri: Uri.parse('myapp://auth'), + redirectUri: 'myapp://auth', ); ``` @@ -110,7 +110,7 @@ See [Configuring client IDs on the app](./customizations#configuring-client-ids- **Resolution:** 1. Open your GitHub App's settings and confirm the production callback URL is listed under **Callback URL** alongside the development one. Both should remain registered so dev and prod work simultaneously. -2. Confirm your production Flutter build is initialized with the production `redirectUri`. The simplest way is to read it from `--dart-define` and pass the production value in your CI/CD or `flutter_build` step. See [Publishing to production](./setup#publishing-to-production). +2. Confirm your production Flutter build is initialized with the production `redirectUri`. The simplest way is to read it from `--dart-define` and pass the production value in your CI/CD or `flutter build` step. See [Publishing to production](./setup#publishing-to-production). ## Sign-in works for you but not for other users @@ -124,7 +124,7 @@ See [Configuring client IDs on the app](./customizations#configuring-client-ids- **Problem:** A user tries to sign in but sees a GitHub message about the organization restricting access to third-party applications, or the sign-in flow returns with no authorization. -**Cause:** The user's GitHub organization has **OAuth App access restrictions** enabled, and your app has not been approved for that organization. This is independent of your app's own settings; the org controls it. +**Cause:** The user's GitHub organization has **OAuth App access restrictions** enabled, and your app has not been approved for that organization. This is independent of your app's own settings. The organization controls it. **Resolution:** The user (or an organization owner) needs to request approval for your GitHub App in the organization's **Settings > Third-party Access** page on GitHub. There is nothing you can do server-side to bypass this. Surface a clear error message to the user explaining the org policy. @@ -138,7 +138,7 @@ See [Configuring client IDs on the app](./customizations#configuring-client-ids- - Make sure the OAuth callback fires only once. Refreshing the `auth.html` page or navigating back to it after authorization re-sends the now-spent code. - If the user genuinely took too long to complete sign-in, the code expired. Have them start the flow again from your sign-in button. -- This is a transient error if it only happens occasionally. Investigate the client only if it reproduces consistently. +- This is a transient error if it only happens occasionally. Investigate the app only if it reproduces consistently. ## GitHub API calls from getExtraGitHubInfoCallback fail or rate-limit @@ -148,9 +148,9 @@ See [Configuring client IDs on the app](./customizations#configuring-client-ids- **Resolution:** -- Cache the data you fetch instead of calling the API on every sign-in. `getExtraGitHubInfoCallback` runs on **every** authentication attempt; if you fetch the same data every time, you will burn through rate limits quickly. -- For long-lived background work, store the access token (encrypted) and refresh it on demand rather than re-running expensive fetches on every sign-in. -- If a single user triggers many sign-ins (e.g., dev iteration), expect to hit the per-user limit; wait an hour or test with a different account. +- Cache the data you fetch instead of calling the API on every sign-in. The `getExtraGitHubInfoCallback` runs on **every** authentication attempt, so fetching the same data every time burns through rate limits quickly. +- Serverpod does not keep the GitHub access token after sign-in, so fetch what you need inside `getExtraGitHubInfoCallback` and store the results, rather than re-fetching on every sign-in. +- If a single user triggers many sign-ins (e.g., dev iteration), expect to hit the per-user limit. Wait an hour or test with a different account. ## Permission changes on the GitHub App do not take effect @@ -174,7 +174,7 @@ development: githubClientSecret: 'your-github-client-secret' ``` -Quotes are required because the values are strings; YAML interprets unquoted values that look like numbers or booleans differently. +Quoting the values is a safeguard. YAML parses unquoted values that look like numbers or booleans as those types instead of strings. ## Server crashes on first GitHub sign-in with "no such table" @@ -182,7 +182,7 @@ Quotes are required because the values are strings; YAML interprets unquoted val **Cause:** The database migration that creates the provider's tables was never created or applied. -**Resolution:** In the running `serverpod start` terminal, press **M** to create the migration, then **A** to apply it. +**Resolution:** In the running `serverpod start` terminal, press **M** to create and apply the migration. ## Android sign-in opens GitHub but the callback never fires diff --git a/docs/06-concepts/04-authentication/05-providers/08-microsoft/01-setup.md b/docs/06-concepts/04-authentication/05-providers/08-microsoft/01-setup.md index 64d9136e..ea697bf0 100644 --- a/docs/06-concepts/04-authentication/05-providers/08-microsoft/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/08-microsoft/01-setup.md @@ -8,10 +8,10 @@ description: Sign in with Microsoft uses a Microsoft Entra ID app registration. To set up **Sign in with Microsoft**, you must create an app registration on [Microsoft Entra ID (formerly Azure AD)](https://portal.azure.com/) and configure your Serverpod application accordingly. :::caution -You need to install the auth module before you continue, see [Setup](../../setup). +Install the authentication module before you continue. See [Setup](../../setup). ::: -## Create your Microsoft Entra ID App +## Create your Microsoft Entra ID app 1. Go to [Microsoft Azure Portal](https://portal.azure.com/) and log in with your Microsoft account. 2. Navigate to **Microsoft Entra ID** from the portal menu. @@ -57,7 +57,7 @@ After registration, you'll be redirected to the app overview page where you can The client secret value is only shown once. Store it securely immediately after creation. Never commit this value to version control. ::: -### Get the tenant ID (Optional) +### Get the tenant ID (optional) If you're restricting authentication to a specific tenant, you'll need your **Directory (tenant) ID**, which is also shown on the app overview page. For most applications, you can use one of these common values: @@ -131,7 +131,7 @@ development: Keep your Client Secret confidential. Never commit this value to version control. Store it securely using environment variables or secret management. ::: -### Configure the Microsoft Identity Provider +### Configure the Microsoft identity provider In your main `server.dart` file, configure the Microsoft identity provider: @@ -187,7 +187,7 @@ class MicrosoftIdpEndpoint extends MicrosoftIdpBaseEndpoint {} ### Generate and migrate -Finally, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**, then **A**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). +Finally, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). ### Basic configuration options diff --git a/docs/06-concepts/04-authentication/05-providers/08-microsoft/02-configuration.md b/docs/06-concepts/04-authentication/05-providers/08-microsoft/02-configuration.md index 1ac9af37..b259c894 100644 --- a/docs/06-concepts/04-authentication/05-providers/08-microsoft/02-configuration.md +++ b/docs/06-concepts/04-authentication/05-providers/08-microsoft/02-configuration.md @@ -65,7 +65,7 @@ The `microsoftAccountDetailsValidation` callback receives a `MicrosoftAccountDet | `userIdentifier` | `String` | The Microsoft user's unique identifier (Object ID) | | `email` | `String?` | The user's email address (may be null) | | `name` | `String?` | The user's display name from Microsoft | -| `image` | `Uri?` | URL to the user's profile image | +| `imageBytes` | `Uint8List?` | The user's profile photo. Always `null` during validation, because the photo is fetched afterwards, and only when the `fetchProfilePhoto` option on `MicrosoftIdpConfig` is enabled (the default) | Example of accessing these properties: @@ -74,7 +74,7 @@ microsoftAccountDetailsValidation: (accountDetails) { print('Microsoft Object ID: ${accountDetails.userIdentifier}'); print('Email: ${accountDetails.email}'); print('Display name: ${accountDetails.name}'); - print('Profile image: ${accountDetails.image}'); + // imageBytes is always null here. The photo is fetched after validation. // Custom validation logic if (accountDetails.email == null) { @@ -112,6 +112,10 @@ Adding additional scopes may require admin consent depending on your tenant conf ### Accessing Microsoft APIs on the server +:::caution +The `getExtraMicrosoftInfoCallback` below runs on **every** sign-in, not only the first. Cache what you fetch, and guard external calls with `try`/`catch` so a provider outage does not block sign-in. +::: + On the server side, you can access Microsoft APIs using the access token. The `getExtraMicrosoftInfoCallback` in `MicrosoftIdpConfig` receives the access token and can be used to call Microsoft Graph APIs: ```dart @@ -139,7 +143,7 @@ final microsoftIdpConfig = MicrosoftIdpConfigFromPasswords( You can use the `onAfterMicrosoftAccountCreated` callback to run logic after a new Microsoft 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 complementary to the [core `onAfterAuthUserCreated` callback](../../working-with-users#reacting-to-the-user-created-event). Use it for side effects specific to a Microsoft login, like storing analytics, sending a welcome email, or storing additional data. ```dart final microsoftIdpConfig = MicrosoftIdpConfigFromPasswords( @@ -159,7 +163,7 @@ This callback runs inside the same database transaction as the account creation. ::: :::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` in combination with `getExtraMicrosoftInfoCallback` to fetch and store the data you need before the auth user is created. +Scopes you assign here with `AuthServices.instance.authUsers.update()` do not apply to the login that is already in progress, because token issuance uses the scopes loaded before this callback runs. They take effect the next time the user signs in. To assign scopes at creation time instead, use `onBeforeAuthUserCreated` together with `getExtraMicrosoftInfoCallback`, which runs before the auth user is created. ::: ## Configuring client IDs on the app @@ -190,10 +194,11 @@ Alternatively, you can pass client configuration during build time using the `-- ```bash flutter run -d \ --dart-define="MICROSOFT_CLIENT_ID=your_client_id" \ - --dart-define="MICROSOFT_REDIRECT_URI=msauth://auth" \ - --dart-define="MICROSOFT_TENANT=common" + --dart-define="MICROSOFT_REDIRECT_URI=msauth://auth" ``` +The tenant has no environment variable. Pass it as an argument when you initialize Microsoft sign-in. + This approach is useful when you need to: - Manage separate client IDs for different platforms (Android, iOS, Web, macOS) in a centralized way diff --git a/docs/06-concepts/04-authentication/05-providers/08-microsoft/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/08-microsoft/03-customizing-the-ui.md index 6b8c368f..ceaae1e5 100644 --- a/docs/06-concepts/04-authentication/05-providers/08-microsoft/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/08-microsoft/03-customizing-the-ui.md @@ -15,8 +15,13 @@ SignInWidget( client: client, microsoftSignInWidget: MicrosoftSignInWidget( client: client, - // Customize the widget - style: MicrosoftButtonStyle.dark, + // Shape and label survive inside SignInWidget, unless its buttonStyle + // sets them. Brand colors do not. + shape: SignInButtonShape.rounded, + text: SignInButtonTextVariant.signInWith, + // A custom widget replaces the built-in handling, so pass your own callbacks. + onAuthenticated: () { /* ... */ }, + onError: (error) { /* ... */ }, ), ) ``` @@ -32,14 +37,14 @@ You can customize the widget's appearance and behavior: ```dart MicrosoftSignInWidget( client: client, - // Button customization - text: MicrosoftButtonText.continueWith, // or signIn, signUp - type: MicrosoftButtonType.standard, // or icon + // Button customization. The values shown are the defaults. style: MicrosoftButtonStyle.light, // or dark - size: MicrosoftButtonSize.large, // or medium - shape: MicrosoftButtonShape.pill, // or rectangular, rounded - logoAlignment: MicrosoftButtonLogoAlignment.left, // or center - minimumWidth: 240, // or null for automatic width + size: SignInButtonSize.large, // or medium, small + text: SignInButtonTextVariant.continueWith, // or signInWith, signUpWith, signIn + shape: SignInButtonShape.pill, // or rounded, rectangular + logoAlignment: SignInButtonLogoAlignment.center, // or left + minimumWidth: 240, // at most 400 + textStyle: null, // TextStyle for the label // Scopes to request from Microsoft // These are the default scopes. diff --git a/docs/06-concepts/04-authentication/05-providers/09-passkey/01-setup.md b/docs/06-concepts/04-authentication/05-providers/09-passkey/01-setup.md index a8fbbc66..fe9a13f3 100644 --- a/docs/06-concepts/04-authentication/05-providers/09-passkey/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/09-passkey/01-setup.md @@ -67,7 +67,7 @@ import 'package:serverpod_auth_idp_server/providers/passkey.dart'; class PasskeyIdpEndpoint extends PasskeyIdpBaseEndpoint {} ``` -Finally, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**, then **A**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). +Finally, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). ### Basic configuration options diff --git a/docs/06-concepts/04-authentication/05-providers/10-custom-providers/01-overview.md b/docs/06-concepts/04-authentication/05-providers/10-custom-providers/01-overview.md index 7bf0e6ac..c05f6c0e 100644 --- a/docs/06-concepts/04-authentication/05-providers/10-custom-providers/01-overview.md +++ b/docs/06-concepts/04-authentication/05-providers/10-custom-providers/01-overview.md @@ -9,5 +9,5 @@ Serverpod's authentication module lets you implement custom authentication provi :::note This section is under development and will be updated soon. -The package also provides general-purpose utilities to facilitate building IDPs. See [OAuth2 Utility](./oauth2-utility/setup). +The package also provides general-purpose utilities to support building IDPs. See [OAuth2 Utility](./oauth2-utility/setup). ::: diff --git a/docs/06-concepts/04-authentication/05-providers/10-custom-providers/02-oauth2-utility/01-setup.md b/docs/06-concepts/04-authentication/05-providers/10-custom-providers/02-oauth2-utility/01-setup.md index 49fd89ad..3483d0d1 100644 --- a/docs/06-concepts/04-authentication/05-providers/10-custom-providers/02-oauth2-utility/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/10-custom-providers/02-oauth2-utility/01-setup.md @@ -95,9 +95,25 @@ Using the previously created `config` object, create the `OAuth2PkceUtil` on you import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; +import '../generated/protocol.dart'; + class MyProviderIdpEndpoint extends IdpBaseEndpoint { final oauth2Util = OAuth2PkceUtil(config: config); + /// Required by IdpBaseEndpoint: report whether the signed-in user already + /// has an account with this provider. Query the account model your provider + /// defines (see the full walkthrough for the model definition). + @override + Future hasAccount(Session session) async { + final authUserId = session.authenticated?.authUserId; + if (authUserId == null) return false; + return await MyProviderAccount.db.findFirstRow( + session, + where: (t) => t.authUserId.equals(authUserId), + ) != + null; + } + Future authenticate( Session session, { required String code, @@ -143,12 +159,12 @@ class MyProviderIdpEndpoint extends IdpBaseEndpoint { // Fetch user data from provider's API using the access token } - Future _authenticate( + Future<_AccountResult> _authenticate( Session session, Map userInfo, ) async { - // Find existing provider account or create new user based on provider user info - // Returns provider account (e.g., GoogleAccount, GitHubAccount) with authUserId linked to AuthUser + // Find the existing provider account or create a new auth user from the + // provider's user info, and return its authUserId with the scopes to grant. } Future _issueToken( @@ -159,6 +175,9 @@ class MyProviderIdpEndpoint extends IdpBaseEndpoint { // Issue Serverpod authentication token for the authenticated user } } + +/// What _authenticate resolves: the linked auth user and the scopes to grant. +typedef _AccountResult = ({UuidValue authUserId, Set scopes}); ``` ### Exception handling @@ -232,15 +251,22 @@ try { // The PKCE code verifier (required for token exchange) final codeVerifier = result.codeVerifier; - // Send both to your backend - await client.myProviderIdp.authenticate( + // Send both to your backend. codeVerifier is null when the provider does + // not use PKCE, so guard it before calling an endpoint that requires it. + if (codeVerifier == null) { + throw StateError('The provider did not return a PKCE code verifier.'); + } + + // The generated client drops the Endpoint suffix: + // MyProviderIdpEndpoint becomes client.myProviderIdp. + final authSuccess = await client.myProviderIdp.authenticate( code: code, codeVerifier: codeVerifier, redirectUri: config.redirectUri, ); -} on OAuth2PkceUserCancelledException catch (e) { - // User cancelled the authorization flow - print('User cancelled: ${e.message}'); + + // Register the session, otherwise the app is never actually signed in. + await client.auth.updateSignedInUser(authSuccess); } on OAuth2PkceStateMismatchException catch (e) { // Possible CSRF attack detected print('Security error: ${e.message}'); @@ -262,11 +288,10 @@ The client-side utility throws specific exceptions to help you handle different | Exception | Description | Typical Cause | | ----------- | ------------- | --------------- | -| `OAuth2PkceUserCancelledException` | User cancelled authorization | User closed browser/denied access | | `OAuth2PkceStateMismatchException` | State validation failed | Possible CSRF attack or browser issue | | `OAuth2PkceMissingAuthorizationCodeException` | No authorization code received | Provider didn't return expected code | | `OAuth2PkceProviderErrorException` | Provider returned error response | Invalid credentials, rate limiting | -| `OAuth2PkceUnknownException` | Unexpected error occurred | Network issues, unknown problems | +| `OAuth2PkceUnknownException` | Unexpected error occurred | Network issues, unknown problems, and a cancelled sign-in | ### Platform-specific configuration @@ -274,7 +299,7 @@ The OAuth2 utility uses the [flutter_web_auth_2](https://pub.dev/packages/flutte #### iOS and macOS -There is no special configuration needed for iOS and MacOS for "normal" authentication flows. +There is no special configuration needed for iOS and macOS for "normal" authentication flows. However, if you are using **Universal Links** on iOS, they require redirect URIs to use **https**. Follow the instructions in the [flutter_web_auth_2](https://pub.dev/packages/flutter_web_auth_2#ios) documentation. @@ -350,7 +375,7 @@ For a full end-to-end implementation of a custom OAuth2 provider (server configu 2. **Validate State Parameter**: Keep `enableState: true` to prevent CSRF attacks. The state parameter ensures the authorization response matches your request. 3. **Secure Client Secret**: Never expose your client secret in client-side code. Store it securely in `passwords.yaml` or environment variables on the server. 4. **Use HTTPS**: Always use HTTPS URLs for production endpoints. Only use HTTP for local development. -5. **Validate Redirect URIs**: Ensure redirect URIs in your code exactly match those registered with your OAuth provider. +5. **Validate Redirect URIs**: Ensure redirect URIs in your code exactly match those registered with your OAuth2 provider. ### Error handling diff --git a/docs/06-concepts/04-authentication/05-providers/10-custom-providers/02-oauth2-utility/02-creating-an-oauth2-based-identity-provider.md b/docs/06-concepts/04-authentication/05-providers/10-custom-providers/02-oauth2-utility/02-creating-an-oauth2-based-identity-provider.md index b3c0287f..e488837a 100644 --- a/docs/06-concepts/04-authentication/05-providers/10-custom-providers/02-oauth2-utility/02-creating-an-oauth2-based-identity-provider.md +++ b/docs/06-concepts/04-authentication/05-providers/10-custom-providers/02-oauth2-utility/02-creating-an-oauth2-based-identity-provider.md @@ -115,8 +115,27 @@ import 'package:serverpod_auth_idp_server/core.dart'; import '../generated/protocol.dart'; import 'my_provider_idp_config.dart'; -class MyProviderIdp { - static const String method = 'myprovider'; +class MyProviderIdp implements IdentityProvider { + /// Required by IdentityProvider: the stable method identifier for tokens. + @override + String get method => 'myprovider'; + + /// Required by IdentityProvider: carry this provider's rows over when two + /// auth users are merged. + @override + Future mergeAuthUsers( + Session session, { + required UuidValue userToKeepId, + required UuidValue userToRemoveId, + required Transaction transaction, + }) async { + await MyProviderAccount.db.updateWhere( + session, + where: (t) => t.authUserId.equals(userToRemoveId), + columnValues: (t) => [t.authUserId(userToKeepId)], + transaction: transaction, + ); + } final MyProviderIdpConfig config; final TokenIssuer _tokenIssuer; @@ -321,12 +340,26 @@ Create the endpoint: import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; +import '../generated/protocol.dart'; import 'my_provider_idp.dart'; class MyProviderIdpEndpoint extends IdpBaseEndpoint { MyProviderIdp get myProviderIdp => AuthServices.getIdentityProvider(); + /// Required by IdpBaseEndpoint: report whether the signed-in user already + /// has an account with this provider. + @override + Future hasAccount(Session session) async { + final authUserId = session.authenticated?.authUserId; + if (authUserId == null) return false; + return await MyProviderAccount.db.findFirstRow( + session, + where: (t) => t.authUserId.equals(authUserId), + ) != + null; + } + Future login( Session session, { required String code, @@ -365,6 +398,8 @@ Register the provider in `server.dart`: import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; +import 'src/generated/endpoints.dart'; +import 'src/generated/protocol.dart'; import 'my_provider_idp_config.dart'; void run(List args) async { @@ -532,20 +567,24 @@ class MyProviderAuthController extends ChangeNotifier { // Get authorization code from provider final result = await MyProviderService.instance.signIn(); - // Exchange for tokens on backend - final endpoint = client.getEndpointOfType(); - await endpoint.login( + // Exchange for tokens on the server. The generated client names the + // endpoint after the class, with the Endpoint suffix dropped, so + // MyProviderIdpEndpoint becomes client.myProviderIdp. + final authSuccess = await client.myProviderIdp.login( code: result.code, + // Safe to unwrap: this config keeps PKCE enabled, so the + // verifier is always present. codeVerifier: result.codeVerifier!, redirectUri: MyProviderConfig.clientConfig.redirectUri, ); + // Register the session, otherwise the app is never actually signed in. + await client.auth.updateSignedInUser(authSuccess); + _setState(MyProviderAuthState.authenticated); onAuthenticated?.call(); - } on OAuth2PkceUserCancelledException { - // User cancelled - just reset to idle - _setState(MyProviderAuthState.idle); } catch (error) { + // A cancelled sign-in arrives as OAuth2PkceUnknownException. _error = error; _setState(MyProviderAuthState.error); onError?.call(error); diff --git a/docs/06-concepts/04-authentication/07-ui-components.md b/docs/06-concepts/04-authentication/07-ui-components.md index 008af9b5..310c073c 100644 --- a/docs/06-concepts/04-authentication/07-ui-components.md +++ b/docs/06-concepts/04-authentication/07-ui-components.md @@ -100,7 +100,7 @@ SignInWidget( ) ``` -Fields set on `buttonStyle` apply to the provider buttons. Brand style presets, such as `GoogleButtonStyle.filledBlack`, only apply when a provider widget is used on its own, outside `SignInWidget`. +Fields set on `buttonStyle` apply to the provider buttons, and they also override the same-named arguments on a custom provider widget you pass to `SignInWidget`. Fields left unset fall through to the widget's own arguments. Brand style presets, such as `GoogleButtonStyle.filledBlack`, only apply when a provider widget is used on its own, outside `SignInWidget`. For all options of each provider widget, see the "Customizing the UI" page for that provider, which also covers building a custom UI with the provider's controller. For example, see [the email provider](./providers/email/customizing-the-ui). diff --git a/static/img/authentication/providers/google/6-people-api.png b/static/img/authentication/providers/google/6-people-api.png deleted file mode 100644 index 7e974b1e..00000000 Binary files a/static/img/authentication/providers/google/6-people-api.png and /dev/null differ