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 64ad87e3..d70067e9 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 @@ -6,10 +6,10 @@ description: Anonymous authentication lets users access your app without creatin # Set up anonymous sign-in :::warning -The Anonymous identity provider is **experimental** and can not be completely used yet due to the missing support for account linking. The missing parts will be added in the next releases. +The anonymous identity provider is **experimental** and can not be completely used yet due to the missing support for account linking. The missing parts will be added in the next releases. ::: -To properly configure Anonymous authentication, you must allow anonymous access in your Serverpod auth configuration. +To properly configure anonymous authentication, you must allow anonymous access in your Serverpod auth configuration. :::caution You need to install the auth module before you continue, see [Setup](../../setup). @@ -22,10 +22,10 @@ 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 'package:serverpod_auth_idp_server/providers/anonymous.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 { final pod = Serverpod( @@ -39,7 +39,7 @@ void run(List args) async { JwtConfigFromPasswords(), ], identityProviderBuilders: [ - // Configure the Anonymous Identity Provider + // Configure the anonymous identity provider AnonymousIdpConfig(), ], ); @@ -60,13 +60,13 @@ Then, start the server with `serverpod start` to generate the client code, then ### Basic configuration options -Although the Anonymous IDP can be used directly with no other configuration, it is recommended to add some form of app attestation to prevent abuse on production environments. See the [Using a token for app attestation section](./configuration#using-a-token-for-app-attestation) for more details. +Although the anonymous identity provider can be used directly with no other configuration, it is recommended to add some form of app attestation to prevent abuse in production environments. See the [Using a token for app attestation section](./customizations#using-a-token-for-app-attestation) for more details. -For other configuration options such as callbacks (before/after account creation) and rate limiting, see the [configuration section](./configuration). +For other configuration options such as callbacks (before/after account creation) and rate limiting, see the [customizations page](./customizations). ## Client-side configuration -If you have configured the `SignInWidget` as described in the [setup section](../../setup#present-the-authentication-ui), the Anonymous identity provider will be automatically detected and displayed in the sign-in widget as a "Continue without account" option. +If you have configured the `SignInWidget` as described in the [setup section](../../setup#present-the-authentication-ui), the anonymous identity provider will be automatically detected and displayed in the sign-in widget as a "Continue without account" option. You can also use the `AnonymousSignInWidget` to include anonymous sign-in in your own custom UI: @@ -90,4 +90,4 @@ AnonymousSignInWidget( ) ``` -The widget displays a "Continue without account" button that creates an anonymous session when pressed. For details on customizing the button (size, shape), using a custom widget with `SignInWidget`, or building a fully custom UI with `AnonymousAuthController`, see the [customizing the UI section](./customizing-the-ui). +The widget displays a "Continue without account" button that creates an anonymous session when pressed. For details on customizing the button (size, shape), using a custom widget with `SignInWidget`, or building a fully custom UI with `AnonymousAuthController`, see the [customizations page](./customizations#customize-the-sign-in-button). 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 deleted file mode 100644 index 7a55911f..00000000 --- a/docs/06-concepts/04-authentication/05-providers/01-anonymous/02-configuration.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -sidebar_label: Configuration -description: Anonymous authentication can take an app attestation token to protect sign-in from abuse. Configure this and other options for the provider. ---- - -# Configure anonymous sign-in - -This page covers configuration options for the anonymous identity provider beyond the basic setup. - -## Using a token for app attestation - -The anonymous `login` endpoint accepts an optional **token** that is forwarded to your `onBeforeAnonymousAccountCreated` callback. This lets you tie anonymous sign-in to an app attestation or app-check provider (e.g. [Firebase App Check](https://firebase.google.com/docs/app-check)) so only requests from your real app can create anonymous accounts. - -:::warning -Using the anonymous provider without a token for app attestation is not recommended due to the risk of abuse. Make sure to configure an attestation before releasing your app to the public. -::: - -### Configuring the Flutter app - -Obtain a token from your app-check provider and pass it to the login call by setting `createAnonymousToken` on `AnonymousSignInWidget` or `AnonymousAuthController`. That callback is invoked when the user taps "Continue without account". The returned token is sent to the server as the `token` argument of the anonymous login endpoint. - -```dart -import 'package:firebase_app_check/firebase_app_check.dart'; -import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; - -AnonymousSignInWidget( - client: client, - createAnonymousToken: () async { - // Get a Firebase App Check token (or similar) to prove the request comes - // from your app to prevent abuse. - final appCheckToken = await FirebaseAppCheck.instance.getToken(); - return appCheckToken; - }, - onAuthenticated: () { /* ... */ }, - onError: (error) { /* ... */ }, -) -``` - -### 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. - -```dart -AnonymousIdpConfig( - onBeforeAnonymousAccountCreated: ( - Session session, { - String? token, - required Transaction? transaction, - }) async { - if (token == null || token.isEmpty) { - throw AnonymousAccountBlockedException( - reason: AnonymousAccountBlockedExceptionReason.denied, - ); - } - // Verify the token with your app-check provider (e.g. Firebase App Check). - // Example: call Firebase's verifyAppCheckToken REST API or your provider's - // verification endpoint. If invalid, throw AnonymousAccountBlockedException. - final isValid = await _verifyAppCheckToken(session, token); - if (!isValid) { - throw AnonymousAccountBlockedException( - reason: AnonymousAccountBlockedExceptionReason.denied, - ); - } - }, -) -``` - -For Firebase App Check, you can verify the token from a custom backend using the [Firebase App Check REST API](https://firebase.google.com/docs/app-check/custom-resource-backend) (`verifyAppCheckToken`). Other app-check or attestation providers can be integrated the same way: client sends a token, server validates it in the callback and denies creation if invalid. - -## Reacting to anonymous account creation - -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( - onAfterAnonymousAccountCreated: ( - Session session, { - required UuidValue authUserId, - required Transaction? transaction, - }) async { - // e.g. track creation for analytics or send to your logging service - }, -) -``` - -## Rate limiting - -The anonymous provider includes built-in rate limiting per IP address to prevent abuse. The default is 100 anonymous account creations per hour per IP. You can customize the rate limit in the `AnonymousIdpConfig` using the `perIpAddressRateLimit` parameter: - -```dart -AnonymousIdpConfig( - perIpAddressRateLimit: const RateLimit( - maxAttempts: 50, - timeframe: Duration(hours: 1), - ), -) -``` - -When the limit is exceeded, the provider throws an `AnonymousAccountBlockedException` with reason `tooManyAttempts`. diff --git a/docs/06-concepts/04-authentication/05-providers/01-anonymous/02-customizations.md b/docs/06-concepts/04-authentication/05-providers/01-anonymous/02-customizations.md new file mode 100644 index 00000000..5dd30362 --- /dev/null +++ b/docs/06-concepts/04-authentication/05-providers/01-anonymous/02-customizations.md @@ -0,0 +1,250 @@ +--- +sidebar_label: Customizations +description: Configuration options and UI customizations for the anonymous identity provider, including app attestation tokens, rate limiting, and the AnonymousSignInWidget and AnonymousAuthController. +--- + +# Customize anonymous sign-in + +This page covers configuration options and UI customizations for the anonymous identity provider beyond the basic setup. On the server, you can tie sign-in to an app attestation token, react to new account creation, and adjust the rate limit. In the app, you can use the `AnonymousSignInWidget` to display the anonymous sign-in button in your own layout, or the `AnonymousAuthController` to build a completely custom authentication interface. + +## Server configuration + +All server-side options for the anonymous provider are set on the `AnonymousIdpConfig`. + +### Using a token for app attestation + +The anonymous `login` endpoint accepts an optional **token** that is forwarded to your `onBeforeAnonymousAccountCreated` callback. This lets you tie anonymous sign-in to an app attestation or app-check provider (e.g. [Firebase App Check](https://firebase.google.com/docs/app-check)) so only requests from your real app can create anonymous accounts. + +:::warning +Using the anonymous provider without a token for app attestation is not recommended due to the risk of abuse. Make sure to configure an attestation before releasing your app to the public. +::: + +#### 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. + +```dart +AnonymousIdpConfig( + onBeforeAnonymousAccountCreated: ( + Session session, { + String? token, + required Transaction? transaction, + }) async { + if (token == null || token.isEmpty) { + throw AnonymousAccountBlockedException( + reason: AnonymousAccountBlockedExceptionReason.denied, + ); + } + // Verify the token with your app-check provider (e.g. Firebase App Check). + // Example: call Firebase's verifyAppCheckToken REST API or your provider's + // verification endpoint. If invalid, throw AnonymousAccountBlockedException. + final isValid = await _verifyAppCheckToken(session, token); + if (!isValid) { + throw AnonymousAccountBlockedException( + reason: AnonymousAccountBlockedExceptionReason.denied, + ); + } + }, +) +``` + +For Firebase App Check, you can verify the token from a custom backend using the [Firebase App Check REST API](https://firebase.google.com/docs/app-check/custom-resource-backend) (`verifyAppCheckToken`). Other app-check or attestation providers can be integrated the same way. The app sends a token, and the server validates it in the callback and denies creation if it is invalid. + +For the app side of this flow, see [Configuring the Flutter app](#configuring-the-flutter-app). + +### Reacting to anonymous account creation + +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( + onAfterAnonymousAccountCreated: ( + Session session, { + required UuidValue authUserId, + required Transaction? transaction, + }) async { + // e.g. track creation for analytics or send to your logging service + }, +) +``` + +### Rate limiting + +The anonymous provider includes built-in rate limiting per IP address to prevent abuse. The default is 100 anonymous account creations per hour per IP. You can customize the rate limit in the `AnonymousIdpConfig` using the `perIpAddressRateLimit` parameter: + +```dart +// RateLimit is exported by the email provider library. +import 'package:serverpod_auth_idp_server/providers/email.dart'; + +AnonymousIdpConfig( + perIpAddressRateLimit: const RateLimit( + maxAttempts: 50, + timeframe: Duration(hours: 1), + ), +) +``` + +When the limit is exceeded, the provider throws an `AnonymousAccountBlockedException` with reason `tooManyAttempts`. + +## App configuration + +On the app side, the main configuration is supplying the attestation token described in [Using a token for app attestation](#using-a-token-for-app-attestation). + +### Configuring the Flutter app + +Obtain a token from your app-check provider and pass it to the login call by setting `createAnonymousToken` on `AnonymousSignInWidget` or `AnonymousAuthController`. That callback is invoked when the user taps "Continue without account". The returned token is sent to the server as the `token` argument of the anonymous login endpoint. + +```dart +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; + +AnonymousSignInWidget( + client: client, + createAnonymousToken: () async { + // Get a Firebase App Check token (or similar) to prove the request comes + // from your app to prevent abuse. + final appCheckToken = await FirebaseAppCheck.instance.getToken(); + return appCheckToken; + }, + onAuthenticated: () { /* ... */ }, + onError: (error) { /* ... */ }, +) +``` + +## Customize the sign-in button + +When the button renders inside a `SignInWidget`, fields set on `buttonStyle` override its same-named arguments, as described in [Styling the buttons](../../ui-components#styling-the-buttons). + +:::info +The `SignInWidget` uses the `AnonymousSignInWidget` internally when the anonymous provider is enabled. You can supply a custom `AnonymousSignInWidget` to the `SignInWidget` to override the default (e.g. to pass `createAnonymousToken` or change size and shape). + +```dart +SignInWidget( + client: client, + anonymousSignInWidget: AnonymousSignInWidget( + client: client, + createAnonymousToken: () async => await getAppCheckToken(), + size: SignInButtonSize.medium, + shape: SignInButtonShape.rectangular, + // A custom widget replaces the built-in handling, so pass your own callbacks. + onAuthenticated: () { /* ... */ }, + onError: (error) { /* ... */ }, + ), +) +``` +::: + +### Using the `AnonymousSignInWidget` + +The `AnonymousSignInWidget` displays a single "Continue without account" button that starts the anonymous sign-in flow when pressed. You can customize the widget's behavior and appearance using its constructor parameters: + +```dart +import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; + +AnonymousSignInWidget( + client: client, + createAnonymousToken: () async { + // Optional: provide a token for app attestation (e.g. Firebase App Check) + return await getAppCheckToken(); + }, + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, + // 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. 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: SignInButtonSize.medium, + shape: SignInButtonShape.rectangular, +) +``` + +#### Customizing the button appearance + +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 +AnonymousSignInWidget( + client: client, + textStyle: const TextStyle(fontWeight: FontWeight.w600), +) +``` + +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. + +## Build a custom UI with AnonymousAuthController + +For full control over the UI, use the `AnonymousAuthController` class. It provides the anonymous sign-in logic without any built-in widget, so you can trigger login from your own button or flow and build a completely custom layout. + +```dart +import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; + +final controller = AnonymousAuthController( + client: client, + createAnonymousToken: () async => await getAppCheckToken(), + onAuthenticated: () { + // Do something when the user is authenticated. + }, + onError: (error) { + // Handle errors + }, +); +``` + +### AnonymousAuthController state management + +The controller notifies listeners when its state changes. Use these properties to drive your UI: + +```dart +// Check if a request is in progress +final isLoading = controller.isLoading; + +// Check current state (idle, loading, error, authenticated) +final state = controller.state; + +// Listen to state changes +controller.addListener(() { + setState(() { + // Rebuild when controller state changes + }); +}); +``` + +### AnonymousAuthController methods + +The controller exposes a single action for anonymous sign-in: + +```dart +// Start anonymous sign-in. +// Obtains token if `createAnonymousToken` is set, then calls the login endpoint. +await controller.login(); +``` + +Call `controller.login()` from your custom button's `onPressed`, or from any other trigger (e.g. after a delay or when the user performs an action). The controller handles loading state, success, and errors and invokes `onAuthenticated` or `onError` as appropriate. + +:::tip +Remember to dispose the controller when it is no longer needed (e.g. in your widget's `dispose`), unless the widget manages its own controller and disposes it for you. +::: + +## Related + +- [Setup](./setup): enable the anonymous provider on the server and show the sign-in button in your app. +- [UI components](../../ui-components): the shared sign-in screen and styling for all provider buttons. +- [Working with users](../../working-with-users): access the authenticated user and profile data on the server. 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 deleted file mode 100644 index a6ebec4c..00000000 --- a/docs/06-concepts/04-authentication/05-providers/01-anonymous/03-customizing-the-ui.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -sidebar_label: Customizing the UI -description: Anonymous sign-in UI can be customized with the AnonymousSignInWidget and AnonymousAuthController to match the rest of your app's design. ---- - -# Customize the anonymous sign-in UI - -When using the anonymous identity provider, you can customize the UI to your liking. You can use the `AnonymousSignInWidget` to display the anonymous sign-in button in your own layout, or you can use the `AnonymousAuthController` to build a completely custom authentication interface. - -:::info -The `SignInWidget` uses the `AnonymousSignInWidget` internally when the anonymous provider is enabled. You can supply a custom `AnonymousSignInWidget` to the `SignInWidget` to override the default (e.g. to pass `createAnonymousToken` or change size and shape). - -```dart -SignInWidget( - client: client, - anonymousSignInWidget: AnonymousSignInWidget( - client: client, - createAnonymousToken: () async => await getAppCheckToken(), - size: SignInButtonSize.medium, - shape: SignInButtonShape.rectangular, - // A custom widget replaces the built-in handling, so pass your own callbacks. - onAuthenticated: () { /* ... */ }, - onError: (error) { /* ... */ }, - ), -) -``` -::: - -## Using the `AnonymousSignInWidget` - -The `AnonymousSignInWidget` displays a single "Continue without account" button that starts the anonymous sign-in flow when pressed. You can customize the widget's behavior and appearance using its constructor parameters: - -```dart -import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; - -AnonymousSignInWidget( - client: client, - createAnonymousToken: () async { - // Optional: provide a token for app attestation (e.g. Firebase App Check) - return await getAppCheckToken(); - }, - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, - // 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. 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: SignInButtonSize.medium, - shape: SignInButtonShape.rectangular, -) -``` - -### Customizing the button appearance - -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 -AnonymousSignInWidget( - client: client, - textStyle: const TextStyle(fontWeight: FontWeight.w600), -) -``` - -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` - -For full control over the UI, use the `AnonymousAuthController` class. It provides the anonymous sign-in logic without any built-in widget, so you can trigger login from your own button or flow and build a completely custom layout. - -```dart -import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; - -final controller = AnonymousAuthController( - client: client, - createAnonymousToken: () async => await getAppCheckToken(), - onAuthenticated: () { - // Do something when the user is authenticated. - }, - onError: (error) { - // Handle errors - }, -); -``` - -### AnonymousAuthController state management - -The controller notifies listeners when its state changes. Use these properties to drive your UI: - -```dart -// Check if a request is in progress -final isLoading = controller.isLoading; - -// Check current state (idle, loading, error, authenticated) -final state = controller.state; - -// Listen to state changes -controller.addListener(() { - setState(() { - // Rebuild when controller state changes - }); -}); -``` - -### AnonymousAuthController methods - -The controller exposes a single action for anonymous sign-in: - -```dart -// Start anonymous sign-in. -// Obtains token if `createAnonymousToken` is set, then calls the login endpoint. -await controller.login(); -``` - -Call `controller.login()` from your custom button's `onPressed`, or from any other trigger (e.g. after a delay or when the user performs an action). The controller handles loading state, success, and errors and invokes `onAuthenticated` or `onError` as appropriate. - -:::tip -Remember to dispose the controller when it is no longer needed (e.g. in your widget's `dispose`), unless the widget manages its own controller and disposes it for you. -::: 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 82d1df89..3b73ab4c 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 @@ -242,17 +242,17 @@ 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/` locally on default projects). 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: ```bash -flutter build web --base-href /app/ --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 # from your server project ``` -Replace `my_project_server` with your server package directory. Open `http://localhost:8082/app` to test. +Replace `my_project_server` with your server package directory. 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`. The examples below use port `8082` (Serverpod's default from `config/development.yaml`). @@ -328,7 +328,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. :::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). +On web, the app Serverpod serves 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). ::: ### Show the Google sign-in button @@ -413,7 +413,7 @@ The `SignInWidget` renders the standard Google sign-in button: ![Google sign-in button](/img/authentication/providers/google/3-button.png) -To change the button's theme or build a fully custom UI, see [Customizing the UI](./customizing-the-ui). +To change the button's theme or build a fully custom UI, see [Customizations](./customizations#customize-the-sign-in-button). :::tip If you run into issues, see the [troubleshooting guide](./troubleshooting). 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 267333af..b5911ff8 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 @@ -1,13 +1,17 @@ --- sidebar_label: Customizations -description: Sign in with Google can be configured through GoogleIdpConfig, including how to load client secrets and use the available callbacks. +description: Configuration options for Google sign-in, including GoogleIdpConfig callbacks on the server, client IDs and redirect URIs in the app, sign-in button customization, and custom UIs with GoogleAuthController. --- # Customize Google sign-in -This page covers additional configuration options for the Google identity provider beyond the basic setup. +This page covers configuration and UI options for the Google identity provider beyond the basic setup. On the server, you can control how the client secret is loaded and hook into the sign-in flow with callbacks. In the app, you can configure client IDs and redirect URIs, customize the sign-in button with the `GoogleSignInWidget`, or build a completely custom authentication interface with the `GoogleAuthController`. -## Configuration options +## Server configuration + +These options control how the Google identity provider behaves on the server. + +### Configuration options Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `GoogleIdpConfig` in-code documentation. @@ -18,9 +22,9 @@ The Google identity provider can be configured using one of two classes: The `GoogleIdpConfigFromPasswords` class is a convenience wrapper around `GoogleIdpConfig` that handles credential loading for you. -Both classes accept the same optional callbacks shown in the sections below. The examples on this page use `GoogleIdpConfigFromPasswords` unless the section specifically demonstrates manual client secret loading. +Both classes accept the same optional callbacks, such as `googleAccountDetailsValidation` and `getExtraGoogleInfoCallback`, shown below. The examples on this page use `GoogleIdpConfigFromPasswords` unless the section specifically demonstrates manual client secret loading. -### Load the client secret using GoogleIdpConfig +#### Load the client secret using GoogleIdpConfig When using `GoogleIdpConfig`, you must provide the client secret explicitly. @@ -62,7 +66,7 @@ final googleIdpConfig = GoogleIdpConfig( ); ``` -### Custom account validation +#### Custom account validation 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. @@ -79,20 +83,7 @@ final googleIdpConfig = GoogleIdpConfigFromPasswords( ); ``` -### Accessing Google APIs - -The default setup allows access to basic user information, such as email, profile image, and name. You may require additional access scopes, such as accessing a user's calendar, contacts, or files. To do this, you will need to: - -- Add the required scopes to the [Data Access](./setup#configure-google-auth-platform) page in the Google Auth Platform. -- Request access to the scopes when signing in. Do this by setting the `scopes` parameter of the `GoogleSignInWidget` or `GoogleAuthController`. - -For a full list of available scopes, see the [Google OAuth 2.0 Scopes reference](https://developers.google.com/identity/protocols/oauth2/scopes). - -:::info -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: @@ -120,42 +111,40 @@ final googleIdpConfig = GoogleIdpConfigFromPasswords( ); ``` -### Reacting to auth user creation +To request additional scopes at sign-in, see [Accessing Google APIs](#accessing-google-apis) under App configuration. -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. +#### Reacting to auth user creation -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: +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 [user creation callbacks](../../working-with-users#user-creation-callbacks) for full details on both hooks. -```dart -pod.initializeAuthServices( - tokenManagerBuilders: [ - JwtConfigFromPasswords(), - ], - identityProviderBuilders: [ - GoogleIdpConfigFromPasswords(), - ], - authUsersConfig: AuthUsersConfig( - onBeforeAuthUserCreated: ( - session, - scopes, - blocked, { - required transaction, - }) { - return ( - scopes: {...scopes, Scope('user')}, - blocked: blocked, - ); - }, - onAfterAuthUserCreated: ( - session, - authUser, { - required transaction, - }) async { - // e.g. send a welcome email, log for analytics - }, - ), -); -``` +### GoogleIdpConfig parameter reference + +| 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. | +| `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. | + +## App configuration + +These options are set in your Flutter app rather than on the server. + +### Accessing Google APIs + +The default setup allows access to basic user information, such as email, profile image, and name. You may require additional access scopes, such as accessing a user's calendar, contacts, or files. To do this, you will need to: + +- Add the required scopes to the [Data Access](./setup#configure-google-auth-platform) page in the Google Auth Platform. +- Request access to the scopes when signing in. Do this by setting the `scopes` parameter of the `GoogleSignInWidget` or `GoogleAuthController`. + +For a full list of available scopes, see the [Google OAuth 2.0 Scopes reference](https://developers.google.com/identity/protocols/oauth2/scopes). + +:::info +Adding additional scopes may require approval by Google. On the OAuth consent screen, you can see which of your scopes are considered sensitive. +::: + +To use the granted scopes from the server with the access token, see [Accessing Google APIs on the server](#accessing-google-apis-on-the-server). ### Lightweight sign-in on the Flutter app @@ -179,7 +168,7 @@ On web, the option has no effect in this version. It only applies to Android and ### 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. +If no client IDs are provided programmatically, the underlying `google_sign_in` package falls back to `GoogleService-Info.plist` on iOS. On Android, the `google-services.json` fallback only works when the app uses the Firebase `com.google.gms.google-services` Gradle plugin. A plain project must pass the IDs in code or with `--dart-define`, or sign-in fails (see [troubleshooting](./troubleshooting#sign-in-fails-on-android-with-serverclientid-must-be-provided)). To set them programmatically, you can use the following methods. #### Passing client IDs in code @@ -264,12 +253,140 @@ Use this flow when your Flutter web app and Serverpod are on different origins. 4. Pass the same URL to `initializeGoogleSignIn` via the `redirectUri` argument instead of the route URL. -## GoogleIdpConfig parameter reference +## Customize the sign-in button -| 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. | -| `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. | +See [Styling the buttons](../../ui-components#styling-the-buttons) for how the `GoogleSignInWidget` parameters interact with the `buttonStyle` set on `SignInWidget`. + +:::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. + +```dart +SignInWidget( + client: client, + googleSignInWidget: GoogleSignInWidget( + client: client, + // 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) { /* ... */ }, + ), +) +``` + +::: + +### Using the `GoogleSignInWidget` + +The `GoogleSignInWidget` handles the complete Google sign-in flow for iOS, Android, and web. + +You can customize the widget's appearance and behavior: + +```dart +GoogleSignInWidget( + client: client, + // 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. + scopes: const [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + ], + + // Whether to attempt lightweight sign-in (Android and iOS only) + attemptLightweightSignIn: false, + + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, +) +``` + +## Build a custom UI with GoogleAuthController + +For more control over the UI, you can use the `GoogleAuthController` 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'; + +final controller = GoogleAuthController( + client: client, + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, + attemptLightweightSignIn: false, + scopes: const [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + ], +); + +// Initiate sign-in +await controller.signIn(); +``` + +:::note +On web, sign-in always runs through the OAuth2 redirect flow. Call `initializeGoogleSignIn` with both `clientId` and `redirectUri` before calling `signIn()`. When either value cannot be resolved, `initializeGoogleSignIn` throws an `ArgumentError`. Skipping the call entirely leaves the controller in the error state after `signIn()`. Set them up as described in [Web setup](./setup#web). +::: + +### 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: + +```dart +// Check current state +final state = controller.state; // GoogleAuthState enum + +// Check if loading +final isLoading = controller.isLoading; + +// Check if authenticated +final isAuthenticated = controller.isAuthenticated; + +// Get error message +final errorMessage = controller.errorMessage; + +// Listen to state changes +controller.addListener(() { + setState(() { + // Rebuild UI when controller state changes + }); +}); +``` + +#### GoogleAuthController states + +- `GoogleAuthState.initializing` - Controller is initializing. +- `GoogleAuthState.idle` - Ready for user interaction. +- `GoogleAuthState.loading` - Processing a sign-in request. +- `GoogleAuthState.error` - An error occurred. +- `GoogleAuthState.authenticated` - Authentication was successful. + +## Related + +- [Setup](./setup): configure the Google Auth Platform and register the identity provider. +- [Troubleshooting](./troubleshooting): fix common Google sign-in errors. +- [UI components](../../ui-components): style the sign-in buttons and localize the built-in UI. +- [Working with users](../../working-with-users): react to user creation and manage user data. 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 deleted file mode 100644 index e5f55e88..00000000 --- a/docs/06-concepts/04-authentication/05-providers/03-google/03-customizing-the-ui.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -sidebar_label: Customizing the UI -description: Google sign-in UI can be customized with the GoogleSignInWidget and GoogleAuthController to build a custom authentication flow in your app. ---- - -# 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. - -:::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. - -```dart -SignInWidget( - client: client, - googleSignInWidget: GoogleSignInWidget( - client: client, - // 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) { /* ... */ }, - ), -) -``` - -::: - -## Using the `GoogleSignInWidget` - -The `GoogleSignInWidget` handles the complete Google Sign-In flow for iOS, Android, and Web. - -You can customize the widget's appearance and behavior: - -```dart -GoogleSignInWidget( - client: client, - // 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. - scopes: const [ - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile', - ], - - // Whether to attempt lightweight sign-in (Android and iOS only) - attemptLightweightSignIn: false, - - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, -) -``` - -## Building a custom UI with the `GoogleAuthController` - -For more control over the UI, you can use the `GoogleAuthController` 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'; - -final controller = GoogleAuthController( - client: client, - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, - attemptLightweightSignIn: false, - scopes: const [ - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile', - ], -); - -// Initiate sign-in -await controller.signIn(); -``` - -:::note -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 - -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: - -```dart -// Check current state -final state = controller.state; // GoogleAuthState enum - -// Check if loading -final isLoading = controller.isLoading; - -// Check if authenticated -final isAuthenticated = controller.isAuthenticated; - -// Get error message -final errorMessage = controller.errorMessage; - -// Listen to state changes -controller.addListener(() { - setState(() { - // Rebuild UI when controller state changes - }); -}); -``` - -#### GoogleAuthController states - -- `GoogleAuthState.initializing` - Controller is initializing. -- `GoogleAuthState.idle` - Ready for user interaction. -- `GoogleAuthState.loading` - Processing a sign-in request. -- `GoogleAuthState.error` - An error occurred. -- `GoogleAuthState.authenticated` - Authentication was successful. 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/03-troubleshooting.md similarity index 98% rename from docs/06-concepts/04-authentication/05-providers/03-google/04-troubleshooting.md rename to docs/06-concepts/04-authentication/05-providers/03-google/03-troubleshooting.md index 35284271..16e76cba 100644 --- a/docs/06-concepts/04-authentication/05-providers/03-google/04-troubleshooting.md +++ b/docs/06-concepts/04-authentication/05-providers/03-google/03-troubleshooting.md @@ -255,3 +255,9 @@ flutter run --dart-define=GOOGLE_CLIENT_ID=your-web-client-id.apps.googleusercon **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:** 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. + +## Related + +- [Setup](./setup): configure the Google Auth Platform and register the identity provider. +- [Customizations](./customizations): configuration options and sign-in UI customization. +- [UI components](../../ui-components): the sign-in widgets and how to compose them. 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 098474ac..fed52f10 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 @@ -112,7 +112,7 @@ development: Paste the raw `.p8` file contents as-is. Do not pre-generate a JWT. Serverpod handles that internally. If sign-in fails, see the [troubleshooting guide](./troubleshooting). ::: -When you are ready to ship, see [Going to production](#going-to-production) for the production credential setup. +When you are ready to ship, see [Publishing to production](#publishing-to-production) for the production credential setup. ## Server-side configuration @@ -305,13 +305,13 @@ The widget automatically handles: - Token management. - Underlying `sign_in_with_apple` package error handling. -For details on how to customize the Sign in with Apple UI in your Flutter app, see the [customizing the UI section](./customizing-the-ui). +For details on how to customize the Sign in with Apple UI in your Flutter app, see the [customizations page](./customizations#customize-the-sign-in-button). :::warning -Apple sends the user's email and name only on the **first sign-in**. If your server does not persist them during that first authentication, they cannot be retrieved later. +Apple sends the user's email and name only on the **first sign-in**. Serverpod stores them automatically during that first authentication. If the first sign-in never reaches your server, they cannot be retrieved later. See [User email is null after sign-in](./troubleshooting#user-email-is-null-after-sign-in). ::: -## Going to production +## Publishing to production ### Update the Apple Developer Portal 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 09dde0c2..55693ac3 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 @@ -1,13 +1,13 @@ --- sidebar_label: Customizations -description: Sign in with Apple can be configured through AppleIdpConfig, including how to load credentials and use the available options. +description: Configuration options for Sign in with Apple, including AppleIdpConfig credentials and web routes, app build variables, and UI customization with AppleSignInWidget and AppleAuthController. --- # Customize Apple sign-in -This page covers additional configuration options for the Apple identity provider beyond the basic setup. +This page covers additional configuration options for the Apple identity provider beyond the basic setup. On the server, you can control how credentials are loaded, react to account creation, and configure the web routes. In your app, you can configure Sign in with Apple, customize the sign-in button with the `AppleSignInWidget`, or build a completely custom interface with the `AppleAuthController`. -## Configuration options +## Server configuration Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `AppleIdpConfig` in-code documentation. @@ -93,6 +93,23 @@ pod.configureAppleIdpRoutes( 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. ::: +### AppleIdpConfig parameters + +| Parameter | Type | Required | `passwords.yaml` key | Description | +| --- | --- | --- | --- | --- | +| `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 | `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 sign-ins back to the app via an intent URI. | + +## App configuration + +These options control the values your app passes to `initializeAppleSignIn()`. + ### Configuring Sign in with Apple on the app 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: @@ -135,15 +152,135 @@ This approach is useful when you need to: You can set `--dart-define` values in your IDE run configuration or CI/CD pipeline instead of passing them on every `flutter run` command. ::: -## AppleIdpConfig parameters +## Customize the sign-in button -| Parameter | Type | Required | `passwords.yaml` key | Description | -| --- | --- | --- | --- | --- | -| `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 | `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 sign-ins back to the app via an intent URI. | +For the `buttonStyle` precedence rules that apply to all built-in buttons, see [Styling the buttons](../../ui-components#styling-the-buttons). + +:::info +The `SignInWidget` uses the `AppleSignInWidget` internally to display the Apple sign-in flow. You can also supply a custom `AppleSignInWidget` to the `SignInWidget` to override the default behavior. + +```dart +SignInWidget( + client: client, + appleSignInWidget: AppleSignInWidget( + client: client, + // 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) { /* ... */ }, + ), +) +``` +::: + +### Using the `AppleSignInWidget` + +The `AppleSignInWidget` handles the complete Apple sign-in flow for iOS, macOS, Android, and web. + +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. 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, + ], + + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, +) +``` + +## Build a custom UI with AppleAuthController + +For more control over the UI, you can use the `AppleAuthController` 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'; + +// Also import sign_in_with_apple here for AppleIDAuthorizationScopes. +final controller = AppleAuthController( + client: client, + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, + scopes: const [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], +); + +// Initiate sign-in +await controller.signIn(); +``` + +### 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: + +```dart +// Check current state +final state = controller.state; // AppleAuthState enum + +// Check if loading +final isLoading = controller.isLoading; + +// Check if authenticated +final isAuthenticated = controller.isAuthenticated; + +// Get error message +final errorMessage = controller.errorMessage; + +// Listen to state changes +controller.addListener(() { + setState(() { + // Rebuild UI when controller state changes + }); +}); +``` + +#### AppleAuthController states + +- `AppleAuthState.idle` - Ready for user interaction. +- `AppleAuthState.loading` - Processing a sign-in request. +- `AppleAuthState.error` - An error occurred. +- `AppleAuthState.authenticated` - Authentication was successful. + +## Related + +- [Setup](./setup): configure Sign in with Apple on the server and in your app. +- [Troubleshooting](./troubleshooting): fix common Apple sign-in errors. +- [UI components](../../ui-components): style the sign-in buttons and localize the built-in UI. +- [Working with users](../../working-with-users): manage auth users and react to account events. 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 deleted file mode 100644 index cd330032..00000000 --- a/docs/06-concepts/04-authentication/05-providers/04-apple/03-customizing-the-ui.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -sidebar_label: Customizing the UI -description: Apple sign-in UI can be customized with the AppleSignInWidget and AppleAuthController to build a custom authentication flow in your app. ---- - -# Customize the Apple sign-in UI - -When using the Apple identity provider, you can customize the UI to your liking. You can use the `AppleSignInWidget` to display the Apple Sign-In flow in your own custom UI, or you can use the `AppleAuthController` to build a completely custom authentication interface. - -:::info -The `SignInWidget` uses the `AppleSignInWidget` internally to display the Apple Sign-In flow. You can also supply a custom `AppleSignInWidget` to the `SignInWidget` to override the default behavior. - -```dart -SignInWidget( - client: client, - appleSignInWidget: AppleSignInWidget( - client: client, - // 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) { /* ... */ }, - ), -) -``` -::: - -## Using the `AppleSignInWidget` - -The `AppleSignInWidget` handles the complete Apple Sign-In flow for iOS, macOS, Android, and Web. - -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. 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, - ], - - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, -) -``` - -## Building a custom UI with the `AppleAuthController` - -For more control over the UI, you can use the `AppleAuthController` 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'; - -// Also import sign_in_with_apple here for AppleIDAuthorizationScopes. -final controller = AppleAuthController( - client: client, - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, - scopes: const [ - AppleIDAuthorizationScopes.email, - AppleIDAuthorizationScopes.fullName, - ], -); - -// Initiate sign-in -await controller.signIn(); -``` - -### 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: - -```dart -// Check current state -final state = controller.state; // AppleAuthState enum - -// Check if loading -final isLoading = controller.isLoading; - -// Check if authenticated -final isAuthenticated = controller.isAuthenticated; - -// Get error message -final errorMessage = controller.errorMessage; - -// Listen to state changes -controller.addListener(() { - setState(() { - // Rebuild UI when controller state changes - }); -}); -``` - -#### AppleAuthController states - -- `AppleAuthState.idle` - Ready for user interaction. -- `AppleAuthState.loading` - Processing a sign-in request. -- `AppleAuthState.error` - An error occurred. -- `AppleAuthState.authenticated` - Authentication was successful. 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/03-troubleshooting.md similarity index 90% rename from docs/06-concepts/04-authentication/05-providers/04-apple/04-troubleshooting.md rename to docs/06-concepts/04-authentication/05-providers/04-apple/03-troubleshooting.md index d3b09b14..d3ceb2b7 100644 --- a/docs/06-concepts/04-authentication/05-providers/04-apple/04-troubleshooting.md +++ b/docs/06-concepts/04-authentication/05-providers/04-apple/03-troubleshooting.md @@ -145,11 +145,11 @@ If you use `--dart-define`, confirm `APPLE_SERVICE_IDENTIFIER` is the Services I ## User email is `null` after sign-in -**Problem:** The user's email is missing or `null` after sign in, or it's present on first sign-in but missing after that. +**Problem:** The user's email is missing or `null` after sign-in, or it's present on first sign-in but missing after that. -**Cause:** Apple sends the email address and name only once, during initial authorization. After that, only the `sub` claim is provided. If you didn't save the email the first time, you can't get it again unless the user disconnects and reconnects your app. +**Cause:** Apple sends the email address and name only once, during the initial authorization. Later sign-ins carry only the stable `sub` identifier. Serverpod stores both values on the `AppleAccount` row when it first creates the account. The usual reason they are missing is that the first authorization never completed on your server, for example because the endpoint was added afterwards. -**Resolution:** Make sure your server stores the user's email on their first sign-in. Use `sub` as the main user identifier, not email (which can change if the user updates Hide My Email). See [Authenticating users with Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple/authenticating-users-with-sign-in-with-apple). +**Resolution:** The module does not backfill these fields on later sign-ins. Ask the user for their email in your app. If the user removes your app in their Apple ID settings (**Sign in with Apple > Stop Using Apple ID**) and signs in again, Apple resends the values, but the module keeps the old account row. Sign-in itself is unaffected either way, since accounts are keyed by Apple's stable identifier, not the email. See [Authenticating users with Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple/authenticating-users-with-sign-in-with-apple). ## iOS sign-in prompt doesn't show @@ -204,3 +204,9 @@ If you use `--dart-define`, confirm `APPLE_SERVICE_IDENTIFIER` is the Services I **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:** 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. + +## Related + +- [Setup](./setup): configure Sign in with Apple on the server and in your app. +- [Customizations](./customizations): configuration options and sign-in UI customization. +- [UI components](../../ui-components): the sign-in widgets and how to compose them. 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 b05a0901..85b7d700 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 @@ -351,17 +351,17 @@ You can skip the remaining steps (1, 3, 5-9) as they are not required for Flutte On iOS, Facebook may issue **limited access tokens** when App Tracking Transparency (ATT) permission is not granted. These limited tokens cannot be validated by the server or used to retrieve user profile data, which will cause authentication to fail. -To ensure full Facebook authentication functionality on iOS, you should request ATT permissions before initiating Facebook Sign In. You can use the [`app_tracking_transparency`](https://pub.dev/packages/app_tracking_transparency) package to handle this: +To ensure full Facebook authentication functionality on iOS, you should request ATT permissions before initiating Facebook sign-in. You can use the [`app_tracking_transparency`](https://pub.dev/packages/app_tracking_transparency) package to handle this: ```dart import 'package:app_tracking_transparency/app_tracking_transparency.dart'; -// Request tracking authorization before showing Facebook Sign In +// Request tracking authorization before showing Facebook sign-in final status = await AppTrackingTransparency.requestTrackingAuthorization(); ``` :::warning -Without ATT permission granted, Facebook authentication fails on iOS. Consider requesting this permission early in your app's flow or before showing the Facebook Sign In button. +Without ATT permission granted, Facebook authentication fails on iOS. Consider requesting this permission early in your app's flow or before showing the Facebook sign-in button. ::: For more detailed iOS setup instructions, refer to the [flutter_facebook_auth iOS documentation](https://facebook.meedu.app/docs/7.x.x/ios). @@ -488,7 +488,7 @@ For iOS and Android, the App ID is not required as the SDK reads credentials fro If you use the template's `SignInWidget` (see [Present the authentication UI](../../setup#present-the-authentication-ui)), the Facebook button is detected and shown automatically once the `serverpod_auth_idp_flutter_facebook` package is installed and the service is initialized. It handles the full sign-in flow, token management, and error handling on iOS, Android, web, and macOS. -To customize the button or build a fully custom UI, see [Customizing the UI](./customizing-the-ui). +To customize the button or build a fully custom UI, see [Customizations](./customizations#customize-the-sign-in-button). ## Publishing to production @@ -505,7 +505,7 @@ Going Live requires a valid **Privacy Policy URL** (**App settings** > **Basic** ### 2. Add your production domains and platforms - **Web and macOS**: In **Use cases** > **Customize** > **Settings**, confirm **Login with the JavaScript SDK** is **Yes** and add your production domain to **Allowed Domains for the JavaScript SDK** (e.g., `https://yourdomain.com`) alongside your development domain. Both can stay registered so dev and prod work at the same time. -- **Android**: Add your **release key hash** (not just the debug one) to the Android platform in the Facebook app. Generate it from your release keystore: +- **Android**: Add your **release key hash** (not only the debug one) to the Android platform in the Facebook app. Generate it from your release keystore: ```bash keytool -exportcert -alias YOUR_RELEASE_KEY_ALIAS -keystore YOUR_RELEASE_KEY_PATH | openssl sha1 -binary | openssl base64 @@ -515,7 +515,7 @@ Going Live requires a valid **Privacy Policy URL** (**App settings** > **Basic** ### 3. 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. You can reuse the same Facebook app for development and production, or [create a separate app](https://developers.facebook.com/apps/creation/) per environment and use its credentials. 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 feb7dc68..8e704a51 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 @@ -1,15 +1,28 @@ --- sidebar_label: Customizations -description: Sign in with Facebook can be configured through FacebookIdpConfig, including how to load credentials and use the available callbacks. +description: Configuration options for Facebook sign-in, including FacebookIdpConfig callbacks, app-side App ID setup, and UI customization with FacebookSignInWidget and FacebookAuthController. --- # Customize Facebook sign-in -This page covers additional configuration options for the Facebook identity provider beyond the basic setup. +This page covers additional configuration options for the Facebook identity provider beyond the basic setup. It also covers how to customize the sign-in UI. You can use the `FacebookSignInWidget` to display the Facebook sign-in flow in your own custom UI, or the `FacebookAuthController` to build a completely custom authentication interface. -## Configuration options +## Server configuration -Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `FacebookIdpConfig` in-code documentation. +Common configuration options for the Facebook provider. For more details on all options, check the `FacebookIdpConfig` in-code documentation. + +### Load the credentials yourself + +The setup guide uses `FacebookIdpConfigFromPasswords`, which reads `facebookAppId` and `facebookAppSecret` from your password store. To control the loading yourself, use `FacebookIdpConfig` and pass the values directly: + +```dart +FacebookIdpConfig( + appId: myAppId, + appSecret: myAppSecret, +) +``` + +Both classes accept the same optional callbacks, such as `facebookAccountDetailsValidation` and `getExtraFacebookInfoCallback`, shown below. The examples on this page use `FacebookIdpConfigFromPasswords`, the class from the setup guide. ### Custom account validation @@ -82,26 +95,6 @@ facebookAccountDetailsValidation: (accountDetails) { The properties available depend on the permissions requested and what the user consented to share. ::: -### Accessing Facebook APIs - -The default setup allows access to basic user information, such as `name` and `email`. You may require additional permissions to access other Facebook APIs, such as accessing a user's friends, posts, or pages. - -The default permissions requested are: - -- `email`: Access to user's email address. -- `public_profile`: Access to user's basic profile information. - -To request additional permissions, you will need to: - -- Ensure the required permissions are configured in your Facebook App settings (navigate to **Use cases** > **Customize** > **Permissions and features** in the [Facebook App Dashboard](https://developers.facebook.com/)). -- Request access to the permissions when signing in. Do this by setting the `permissions` parameter of the `FacebookSignInWidget` or `FacebookAuthController`. - -A full list of available permissions can be found in the [Facebook permissions reference](https://developers.facebook.com/docs/permissions). - -:::info -Adding additional permissions may require App Review depending on the sensitivity of the requested permissions and your app's use case. -::: - ### Accessing Facebook APIs on the server :::caution @@ -155,16 +148,28 @@ This callback runs inside the same database transaction as the account creation. ::: :::caution -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. +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`](../../working-with-users#user-creation-callbacks) together with `getExtraFacebookInfoCallback`, which runs before the auth user is created. ::: +### FacebookIdpConfig parameter reference + +| Parameter | Type | Required | Description | +| --- | --- | --- | --- | +| `appId` | `String` | Yes | The app ID from your Facebook Developer app. | +| `appSecret` | `String` | Yes | The app secret from your Facebook Developer app. | +| `facebookAccountDetailsValidation` | `FacebookAccountDetailsValidation` | No | Custom validation callback for Facebook account details before allowing sign-in. Throws an exception to reject the account. Defaults to validating only that `userIdentifier` is non-empty. | +| `getExtraFacebookInfoCallback` | `GetExtraFacebookInfoCallback?` | No | Callback that receives the access token after sign-in, allowing you to call the Facebook Graph API and store extra user data. Runs on every sign-in. | +| `onAfterFacebookAccountCreated` | `AfterFacebookAccountCreatedFunction?` | No | Callback invoked after a new Facebook account is created and linked to an auth user. Fires only for new accounts. | + +## App configuration + +These options are configured in your Flutter app rather than on the server. + ### Configuring Facebook sign-in on the app When using the external `serverpod_auth_idp_flutter_facebook` package, you can configure the App ID in your Flutter application. -#### Passing configuration in code - -You can pass the App ID directly when initializing the Facebook Sign-In service: +You can pass the App ID directly when initializing the Facebook sign-in service: ```dart await client.auth.initializeFacebookSignIn( @@ -172,15 +177,7 @@ await client.auth.initializeFacebookSignIn( ); ``` -If the `appId` value is not supplied when initializing the service, the provider will automatically fetch it from the `FACEBOOK_APP_ID` environment variable. This approach is useful for different configurations per platform or build environment. - -#### Using environment variables - -Alternatively, you can pass the App ID during build time using the `--dart-define` option. The Facebook Sign-In provider supports the following environment variable: - -- `FACEBOOK_APP_ID`: Your Facebook App ID. - -**Example usage:** +If the `appId` value is not supplied when initializing the service, the provider will automatically fetch it from the `FACEBOOK_APP_ID` environment variable. You can set this variable at build time using the `--dart-define` option: ```bash flutter run -d \ @@ -196,3 +193,137 @@ This approach is useful when you need to: :::tip You can also set these environment variables in your IDE's run configuration or CI/CD pipeline to avoid passing them manually each time. ::: + +### Accessing Facebook APIs + +The default setup allows access to basic user information, such as `name` and `email`. You may require additional permissions to access other Facebook APIs, such as accessing a user's friends, posts, or pages. + +The default permissions requested are: + +- `email`: Access to user's email address. +- `public_profile`: Access to user's basic profile information. + +To request additional permissions, you will need to: + +- Ensure the required permissions are configured in your Facebook App settings (navigate to **Use cases** > **Customize** > **Permissions and features** in the [Facebook App Dashboard](https://developers.facebook.com/)). +- Request access to the permissions when signing in. Do this by setting the `permissions` parameter of the `FacebookSignInWidget` or `FacebookAuthController`. + +A full list of available permissions can be found in the [Facebook permissions reference](https://developers.facebook.com/docs/permissions). + +:::info +Adding additional permissions may require App Review depending on the sensitivity of the requested permissions and your app's use case. +::: + +## Customize the sign-in button + +If you render the button inside `SignInWidget`, see [Styling the buttons](../../ui-components#styling-the-buttons) for how its `buttonStyle` overrides the parameters set here. + +:::info +The `SignInWidget` automatically detects and displays the Facebook sign-in flow when the `serverpod_auth_idp_flutter_facebook` package is installed and initialized. The Facebook provider registers itself dynamically with the sign-in widget. +::: + +### Using the `FacebookSignInWidget` + +The `FacebookSignInWidget` handles the complete Facebook sign-in flow for iOS, Android, web, and macOS. + +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. The values shown are the defaults. + style: FacebookButtonStyle.blue, // or white + 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. + permissions: const ['email', 'public_profile'], + + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, +) +``` + +## Build a custom UI with FacebookAuthController + +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( + client: client, + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, + permissions: const ['email', 'public_profile'], +); + +// Initiate sign-in +await controller.signIn(); +``` + +### FacebookAuthController 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: + +```dart +// Check current state +final state = controller.state; // FacebookAuthState enum + +// Check if loading +final isLoading = controller.isLoading; + +// Check if authenticated +final isAuthenticated = controller.isAuthenticated; + +// Get error message +final errorMessage = controller.errorMessage; + +// Get error object +final error = controller.error; + +// Listen to state changes +controller.addListener(() { + setState(() { + // Rebuild UI when controller state changes + }); +}); +``` + +#### FacebookAuthController states + +- `FacebookAuthState.initializing` - Controller is initializing. +- `FacebookAuthState.idle` - Ready for user interaction. +- `FacebookAuthState.loading` - Processing a sign-in request. +- `FacebookAuthState.error` - An error occurred. +- `FacebookAuthState.authenticated` - Authentication was successful. + +## Related + +- [Setup](./setup): set up the Facebook identity provider on the server and in your app. +- [Troubleshooting](./troubleshooting): fix common Facebook sign-in errors. +- [UI components](../../ui-components): style the sign-in widget and its provider buttons. +- [Working with users](../../working-with-users): manage auth users and react to account events. 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 deleted file mode 100644 index f02d950e..00000000 --- a/docs/06-concepts/04-authentication/05-providers/05-facebook/03-customizing-the-ui.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -sidebar_label: Customizing the UI -description: Facebook sign-in UI can be customized with the FacebookSignInWidget and FacebookAuthController to build a custom authentication flow in your app. ---- - -# Customize the Facebook sign-in UI - -When using the Facebook identity provider, you can customize the UI to your liking. You can use the `FacebookSignInWidget` to display the Facebook Sign-In flow in your own custom UI, or you can use the `FacebookAuthController` to build a completely custom authentication interface. - -:::info -The `SignInWidget` automatically detects and displays the Facebook sign-in flow when the `serverpod_auth_idp_flutter_facebook` package is installed and initialized. The Facebook provider registers itself dynamically with the sign-in widget. -::: - -## Using the `FacebookSignInWidget` - -The `FacebookSignInWidget` handles the complete Facebook Sign-In flow for iOS, Android, Web, and macOS. - -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. The values shown are the defaults. - style: FacebookButtonStyle.blue, // or white - 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. - permissions: const ['email', 'public_profile'], - - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, -) -``` - -## Building a custom UI with the `FacebookAuthController` - -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( - client: client, - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, - permissions: const ['email', 'public_profile'], -); - -// Initiate sign-in -await controller.signIn(); -``` - -### FacebookAuthController 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: - -```dart -// Check current state -final state = controller.state; // FacebookAuthState enum - -// Check if loading -final isLoading = controller.isLoading; - -// Check if authenticated -final isAuthenticated = controller.isAuthenticated; - -// Get error message -final errorMessage = controller.errorMessage; - -// Get error object -final error = controller.error; - -// Listen to state changes -controller.addListener(() { - setState(() { - // Rebuild UI when controller state changes - }); -}); -``` - -### FacebookAuthController states - -- `FacebookAuthState.initializing` - Controller is initializing. -- `FacebookAuthState.idle` - Ready for user interaction. -- `FacebookAuthState.loading` - Processing a sign-in request. -- `FacebookAuthState.error` - An error occurred. -- `FacebookAuthState.authenticated` - Authentication was successful. 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/03-troubleshooting.md similarity index 93% rename from docs/06-concepts/04-authentication/05-providers/05-facebook/04-troubleshooting.md rename to docs/06-concepts/04-authentication/05-providers/05-facebook/03-troubleshooting.md index c4411db6..80493927 100644 --- a/docs/06-concepts/04-authentication/05-providers/05-facebook/04-troubleshooting.md +++ b/docs/06-concepts/04-authentication/05-providers/05-facebook/03-troubleshooting.md @@ -118,7 +118,7 @@ development: facebookAppSecret: 'your-facebook-app-secret' ``` -Quotes are required because the values are strings. On Serverpod Cloud, set them with `scloud password set` instead. See [Publishing to production](./setup#publishing-to-production). +Quoting the values is a safeguard. YAML parses unquoted values that look like numbers as numbers instead of strings. Facebook App IDs are numeric, so an unquoted `facebookAppId` crashes startup with an "Invalid password entries" error instead of this one. On Serverpod Cloud, set them with `scloud password set` instead. See [Publishing to production](./setup#publishing-to-production). ## Server crashes on first Facebook sign-in with "no such table" @@ -139,3 +139,9 @@ Quotes are required because the values are strings. On Serverpod Cloud, set them 1. Add your production domain to **Allowed Domains for the JavaScript SDK** (web and macOS) and register the release key hash (Android). 2. Switch the app to **Live** mode. 3. Confirm the production server has `facebookAppId` and `facebookAppSecret` set, and that the production web build passes `--dart-define=FACEBOOK_APP_ID=...`. See [Publishing to production](./setup#publishing-to-production). + +## Related + +- [Setup](./setup): set up the Facebook identity provider on the server and in your app. +- [Customizations](./customizations): configuration options and sign-in UI customization. +- [UI components](../../ui-components): the sign-in widgets and how to compose them. 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 7dd87234..4d7a2651 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 @@ -92,7 +92,7 @@ GitHub users can keep their email private. Even with the **Email addresses** per ### Store your credentials -Your server's `config/passwords.yaml` already has `development:`, `staging:`, and `production:` sections from the project template. Add `githubClientId` and `githubClientSecret` to the `development:` section using the values you just copied: +Your server's `config/passwords.yaml` already has `development:`, `staging:`, and `production:` sections from the project template. Add `githubClientId` and `githubClientSecret` to the `development:` section using the values you copied above: ```yaml development: @@ -278,7 +278,7 @@ To keep these values out of `main.dart` and vary them per build, read them from 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). +To customize the GitHub button or build a fully custom UI, see [Customizations](./customizations#customize-the-sign-in-button). ## Publishing to production 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 4e069e87..875f789a 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 @@ -1,13 +1,17 @@ --- sidebar_label: Customizations -description: Sign in with GitHub can be configured through GitHubIdpConfig, including how to load credentials and use the available callbacks. +description: Configuration options for GitHub sign-in, including credential loading, server callbacks, app-side client IDs, and custom UIs built with GitHubSignInWidget and GitHubAuthController. --- # Customize GitHub sign-in -This page covers additional configuration options for the GitHub identity provider beyond the basic setup. +This page covers additional configuration options for the GitHub identity provider beyond the basic setup. On the server, you can control how credentials are loaded and hook into the sign-in flow with callbacks. In your app, you can configure client IDs and redirect URIs, and customize the sign-in UI. Use the `GitHubSignInWidget` to display the GitHub sign-in flow in your own custom UI, or the `GitHubAuthController` to build a completely custom authentication interface. -## Configuration options +## Server configuration + +The options in this section are set on the server when you register the GitHub identity provider. + +### Configuration options Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `GitHubIdpConfig` in-code documentation. @@ -20,7 +24,7 @@ The `GitHubIdpConfigFromPasswords` class is a convenience wrapper around `GitHub Both classes accept the same optional callbacks shown in the sections below. The examples on this page use `GitHubIdpConfigFromPasswords` unless the section specifically demonstrates manual credential loading. -### Load credentials using GitHubIdpConfig +#### Load credentials using GitHubIdpConfig When using `GitHubIdpConfig`, you must provide the client ID and secret explicitly. Read them from any source you want: @@ -40,7 +44,7 @@ final githubIdpConfig = GitHubIdpConfig( ); ``` -### Custom account validation +#### Custom account validation You can customize the validation for GitHub account details before allowing sign-in. By default, the validation only checks that the received account details contain a non-empty `userIdentifier`. @@ -59,7 +63,7 @@ final githubIdpConfig = GitHubIdpConfigFromPasswords( GitHub users can keep their email private, so `email` may be `null` even for valid accounts. Similarly, `name` is optional on GitHub profiles. To avoid blocking real users with private profiles from signing in, adjust your validation function with care. ::: -#### GitHubAccountDetails +##### GitHubAccountDetails The `githubAccountDetailsValidation` callback receives a `GitHubAccountDetails` record with the following properties: @@ -86,7 +90,7 @@ githubAccountDetailsValidation: (accountDetails) { }, ``` -### Accessing GitHub APIs on the server +#### Accessing GitHub APIs on the server On the server side, you can call GitHub's REST API using the access token returned by sign-in. The `getExtraGitHubInfoCallback` on `GitHubIdpConfig` receives the access token on every authentication attempt and can be used to fetch and store additional user data: @@ -117,10 +121,10 @@ final githubIdpConfig = GitHubIdpConfigFromPasswords( ::: :::info -This callback runs on **every** sign-in, not just the first. Keep operations lightweight or guard expensive work behind a check for whether the data already exists. +This callback runs on **every** sign-in, not only the first. Keep operations lightweight or guard expensive work behind a check for whether the data already exists. Guard external calls with `try`/`catch`, because an uncaught exception in the callback makes the sign-in fail. ::: -### Reacting to GitHub account creation +#### Reacting to GitHub account creation Use the `onAfterGitHubAccountCreated` callback to run logic after a new GitHub account has been created and linked to an auth user. This callback only fires for new accounts, not returning users. @@ -147,48 +151,29 @@ This callback runs inside the same database transaction as the account creation. 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 +#### 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 [user creation callbacks](../../working-with-users#user-creation-callbacks) for full details on both hooks. -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: +### GitHubIdpConfig parameter reference -```dart -pod.initializeAuthServices( - tokenManagerBuilders: [ - JwtConfigFromPasswords(), - ], - identityProviderBuilders: [ - GitHubIdpConfigFromPasswords(), - ], - authUsersConfig: AuthUsersConfig( - onBeforeAuthUserCreated: ( - session, - scopes, - blocked, { - required transaction, - }) { - return ( - scopes: {...scopes, Scope('user')}, - blocked: blocked, - ); - }, - onAfterAuthUserCreated: ( - session, - authUser, { - required transaction, - }) async { - // e.g. send a welcome email, log for analytics - }, - ), -); -``` +| Parameter | Type | Required | Description | +| --- | --- | --- | --- | +| `clientId` | `String` | Yes | The Client ID from your GitHub App or OAuth App. | +| `clientSecret` | `String` | Yes | The Client Secret generated for your GitHub App or OAuth App. | +| `githubAccountDetailsValidation` | `GitHubAccountDetailsValidation` | No | Custom validation callback for GitHub account details before allowing sign-in. Throws an exception to reject the account. Defaults to validating only that `userIdentifier` is non-empty. | +| `getExtraGitHubInfoCallback` | `GetExtraGitHubInfoCallback?` | No | Callback that receives the access token after sign-in, allowing you to call additional GitHub APIs and store extra user data. Runs on every sign-in. | +| `onAfterGitHubAccountCreated` | `AfterGitHubAccountCreatedFunction?` | No | Callback invoked after a new GitHub account is created and linked to an auth user. Fires only for new accounts. | + +## App configuration + +The options in this section are set in your Flutter app when you initialize the GitHub sign-in service. ### Configuring client IDs on the app #### Passing client IDs in code -You can pass the `clientId` and `redirectUri` directly when initializing the GitHub Sign-In service: +You can pass the `clientId` and `redirectUri` directly when initializing the GitHub sign-in service: ```dart await client.auth.initializeGitHubSignIn( @@ -201,7 +186,7 @@ This approach is useful when you need different `redirectUri` values per platfor #### Using environment variables -Alternatively, pass them at build time using `--dart-define`. The GitHub Sign-In provider supports the following environment variables: +Alternatively, pass them at build time using `--dart-define`. The GitHub sign-in provider supports the following environment variables: - `GITHUB_CLIENT_ID`: Your GitHub OAuth client ID. - `GITHUB_REDIRECT_URI`: The callback URI. Use the value matching the platform you build for: a reverse-DNS scheme for mobile, `https://your-domain.com/auth/callback` for Serverpod-hosted Flutter web, or the full `auth.html` URL for separately-hosted Flutter web. @@ -248,12 +233,129 @@ Use this flow when your Flutter web app and Serverpod are on different origins. 4. Pass the same URL to `initializeGitHubSignIn` via the `redirectUri` argument instead of the route URL. -## GitHubIdpConfig parameter reference +## Customize the sign-in button -| Parameter | Type | Required | Description | -| --- | --- | --- | --- | -| `clientId` | `String` | Yes | The Client ID from your GitHub App or OAuth App. | -| `clientSecret` | `String` | Yes | The Client Secret generated for your GitHub App or OAuth App. | -| `githubAccountDetailsValidation` | `GitHubAccountDetailsValidation` | No | Custom validation callback for GitHub account details before allowing sign-in. Throws an exception to reject the account. Defaults to validating only that `userIdentifier` is non-empty. | -| `getExtraGitHubInfoCallback` | `GetExtraGitHubInfoCallback?` | No | Callback that receives the access token after sign-in, allowing you to call additional GitHub APIs and store extra user data. Runs on every sign-in. | -| `onAfterGitHubAccountCreated` | `AfterGitHubAccountCreatedFunction?` | No | Callback invoked after a new GitHub account is created and linked to an auth user. Fires only for new accounts. | +See [Styling the buttons](../../ui-components#styling-the-buttons) for how a `buttonStyle` set on `SignInWidget` takes precedence over the appearance arguments shown below. + +:::info +The `SignInWidget` uses the `GitHubSignInWidget` internally to display the GitHub sign-in flow. You can also supply a custom `GitHubSignInWidget` to the `SignInWidget` to override the default behavior. + +```dart +SignInWidget( + client: client, + githubSignInWidget: GitHubSignInWidget( + client: client, + // 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) { /* ... */ }, + ), +) +``` + +::: + +### Using the `GitHubSignInWidget` + +The `GitHubSignInWidget` handles the complete GitHub sign-in flow for your Flutter app. + +You can customize the widget's appearance and behavior: + +```dart +GitHubSignInWidget( + client: client, + // Button customization. The values shown are the defaults. + style: GitHubButtonStyle.black, // or white + 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. + scopes: const ['user', 'user:email', 'read:user'], + + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, +) +``` + +:::note +The `scopes` argument applies to **OAuth Apps**. For a **GitHub App**, the App's [Permissions](./setup#set-permissions) configured on the GitHub side control access and the `scopes` argument is ignored. +::: + +## Build a custom UI with GitHubAuthController + +For more control over the UI, you can use the `GitHubAuthController` 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'; + +final controller = GitHubAuthController( + client: client, + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, + scopes: const ['user', 'user:email', 'read:user'], +); + +// Initiate sign-in +await controller.signIn(); +``` + +### GitHubAuthController 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: + +```dart +// Check current state +final state = controller.state; // GitHubAuthState enum + +// Check if loading +final isLoading = controller.isLoading; + +// Check if authenticated +final isAuthenticated = controller.isAuthenticated; + +// Get error message +final errorMessage = controller.errorMessage; + +// Listen to state changes +controller.addListener(() { + setState(() { + // Rebuild UI when controller state changes + }); +}); +``` + +#### GitHubAuthController states + +- `GitHubAuthState.idle` - Ready for user interaction. +- `GitHubAuthState.loading` - Processing a sign-in request. +- `GitHubAuthState.error` - An error occurred. +- `GitHubAuthState.authenticated` - Authentication was successful. + +## Related + +- [Setup](./setup): configure GitHub sign-in on the server and in your app. +- [Troubleshooting](./troubleshooting): fix common GitHub sign-in errors. +- [UI components](../../ui-components): style the sign-in buttons and localize the built-in UI. +- [Working with users](../../working-with-users): manage auth users and react to account events. 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 deleted file mode 100644 index 8ac633c7..00000000 --- a/docs/06-concepts/04-authentication/05-providers/07-github/03-customizing-the-ui.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -sidebar_label: Customizing the UI -description: GitHub sign-in UI can be customized with the GitHubSignInWidget and GitHubAuthController to build a custom authentication flow in your app. ---- - -# Customize the GitHub sign-in UI - -When using the GitHub identity provider, you can customize the UI to your liking. You can use the `GitHubSignInWidget` to display the GitHub Sign-In flow in your own custom UI, or you can use the `GitHubAuthController` to build a completely custom authentication interface. - -:::info -The `SignInWidget` uses the `GitHubSignInWidget` internally to display the GitHub Sign-In flow. You can also supply a custom `GitHubSignInWidget` to the `SignInWidget` to override the default behavior. - -```dart -SignInWidget( - client: client, - githubSignInWidget: GitHubSignInWidget( - client: client, - // 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) { /* ... */ }, - ), -) -``` - -::: - -## Using the `GitHubSignInWidget` - -The `GitHubSignInWidget` handles the complete GitHub Sign-In flow for your Flutter app. - -You can customize the widget's appearance and behavior: - -```dart -GitHubSignInWidget( - client: client, - // Button customization. The values shown are the defaults. - style: GitHubButtonStyle.black, // or white - 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. - scopes: const ['user', 'user:email', 'read:user'], - - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, -) -``` - -:::note -The `scopes` argument applies to **OAuth Apps**. For a **GitHub App**, the App's [Permissions](./setup#set-permissions) configured on the GitHub side control access and the `scopes` argument is ignored. -::: - -## Building a custom UI with the `GitHubAuthController` - -For more control over the UI, you can use the `GitHubAuthController` 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'; - -final controller = GitHubAuthController( - client: client, - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, - scopes: const ['user', 'user:email', 'read:user'], -); - -// Initiate sign-in -await controller.signIn(); -``` - -### GitHubAuthController 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: - -```dart -// Check current state -final state = controller.state; // GitHubAuthState enum - -// Check if loading -final isLoading = controller.isLoading; - -// Check if authenticated -final isAuthenticated = controller.isAuthenticated; - -// Get error message -final errorMessage = controller.errorMessage; - -// Listen to state changes -controller.addListener(() { - setState(() { - // Rebuild UI when controller state changes - }); -}); -``` - -#### GitHubAuthController states - -- `GitHubAuthState.idle` - Ready for user interaction. -- `GitHubAuthState.loading` - Processing a sign-in request. -- `GitHubAuthState.error` - An error occurred. -- `GitHubAuthState.authenticated` - Authentication was successful. 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/03-troubleshooting.md similarity index 96% rename from docs/06-concepts/04-authentication/05-providers/07-github/04-troubleshooting.md rename to docs/06-concepts/04-authentication/05-providers/07-github/03-troubleshooting.md index b120bb7d..d57c053e 100644 --- a/docs/06-concepts/04-authentication/05-providers/07-github/04-troubleshooting.md +++ b/docs/06-concepts/04-authentication/05-providers/07-github/03-troubleshooting.md @@ -41,7 +41,7 @@ Go through this before investigating a specific error. Most problems come from a **Cause:** The `redirectUri` your Flutter app sent to GitHub does not exactly match any of the **Callback URL** entries on your GitHub App. -**Resolution:** Open your GitHub App's settings and verify the **Callback URL** entries match your client's `redirectUri` exactly. The match is strict: scheme, host, port, path, casing, and trailing slashes all count. +**Resolution:** Open your GitHub App's settings and verify the **Callback URL** entries match your app's `redirectUri` exactly. The match is strict: scheme, host, port, path, casing, and trailing slashes all count. Common mistakes: @@ -160,7 +160,7 @@ See [Configuring client IDs on the app](./customizations#configuring-client-ids- **Resolution:** Have affected users sign out and sign in again. GitHub will prompt them to approve the updated permissions. For users who never signed in before the change, the new permissions apply immediately. -## Server fails to parse githubClientSecret from passwords.yaml +## Server crashes on startup with a missing password **Problem:** The server crashes on startup with an error about a missing `githubClientId` or `githubClientSecret` key. @@ -193,3 +193,9 @@ Quoting the values is a safeguard. YAML parses unquoted values that look like nu **Resolution:** Open `android/app/src/main/AndroidManifest.xml` and confirm the `CallbackActivity` block exists with `android:exported="true"` and the `` value matches the scheme in your callback URL exactly. The block is shown in [Android setup](./setup#android). After editing the manifest, run `flutter clean` and rebuild. + +## Related + +- [Setup](./setup): configure GitHub sign-in on the server and in your app. +- [Customizations](./customizations): configuration options and sign-in UI customization. +- [UI components](../../ui-components): the sign-in widgets and how to compose them. 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 ea697bf0..1a635827 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 @@ -195,7 +195,7 @@ Finally, start the server with `serverpod start` to generate the client code, th - `clientSecret`: Required. The Client Secret generated for your Microsoft Entra ID app. - `tenant`: Optional. Defaults to `'common'`. Can be `'common'`, `'organizations'`, `'consumers'`, or a specific tenant ID. -For more details on configuration options, see the [configuration section](./configuration). +For more details on configuration options, see the [customizations page](./customizations). ## Client-side configuration @@ -235,43 +235,13 @@ In order to capture the callback URL, add the following activity to your `Androi ### Web -On the web, you need a specific endpoint to capture the OAuth2 callback. To set this up, create an HTML file (e.g., `auth.html`) inside your project's `./web` folder and add the following content: - -```html - -Authentication complete -

Authentication is complete. If this does not happen automatically, please close the window.

- -``` - -:::note -You only need a single callback file (e.g. `auth.html`) in your `./web` folder. -This file is shared across all IDPs that use the OAuth2 utility, as long as your redirect URIs point to it. -::: +Web sign-in needs the shared callback page that hands the OAuth2 result back to your app. Set it up once as described in [Web callback page (`auth.html`)](../../setup#web-callback-page-authhtml), and point your redirect URI at it, for example `https://yourdomain.com/auth.html`. The same page serves every provider that uses the OAuth2 flow. ## Present the authentication UI ### Initializing the `MicrosoftSignInService` -Before presenting any sign-in UI, initialize the Microsoft Sign-In service. This step is necessary to configure the service with your Microsoft app credentials. +Before presenting any sign-in UI, initialize the Microsoft sign-in service. This step is necessary to configure the service with your Microsoft app credentials. ```dart await client.auth.initializeMicrosoftSignIn( @@ -281,7 +251,7 @@ await client.auth.initializeMicrosoftSignIn( ``` :::info -For more information on configuration options and environment variables, see the [configuration section](./configuration). +For more information on configuration options and environment variables, see the [customizations page](./customizations). ::: ### Using the `MicrosoftSignInWidget` @@ -312,9 +282,9 @@ MicrosoftSignInWidget( The widget automatically handles: -- Microsoft Sign-In flow for iOS, Android, Web, and macOS. +- Microsoft sign-in flow for iOS, Android, web, and macOS. - OAuth2 authentication flow. - Token management. - Underlying OAuth2 package error handling. -For details on how to customize the Microsoft Sign-In UI in your Flutter app, see the [customizing the UI section](./customizing-the-ui). +For details on how to customize the Microsoft sign-in UI in your Flutter app, see the [customizations page](./customizations#customize-the-sign-in-button). 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-customizations.md similarity index 60% rename from docs/06-concepts/04-authentication/05-providers/08-microsoft/02-configuration.md rename to docs/06-concepts/04-authentication/05-providers/08-microsoft/02-customizations.md index b259c894..8a9e39e4 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-customizations.md @@ -1,17 +1,21 @@ --- -sidebar_label: Configuration -description: Microsoft identity provider options include which account types can sign in through the tenant setting. Configure them beyond the basic setup. +sidebar_label: Customizations +description: Microsoft identity provider options beyond the basic setup, from tenant and account validation on the server to client IDs and a custom sign-in UI in the app. --- -# Configure Microsoft sign-in +# Customize Microsoft sign-in -This page covers configuration options for the Microsoft identity provider beyond the basic setup. +This page covers configuration options for the Microsoft identity provider beyond the basic setup, on both the server and the app. It also shows how to customize the sign-in UI. You can use the `MicrosoftSignInWidget` to display the Microsoft sign-in flow in your own custom UI, or the `MicrosoftAuthController` to build a completely custom authentication interface. -## Configuration options +## Server configuration + +These options are set on the `MicrosoftIdpConfig` in your server code. + +### Configuration options Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `MicrosoftIdpConfig` in-code documentation. -### Tenant configuration +#### Tenant configuration The `tenant` parameter determines which accounts can sign in to your application: @@ -32,7 +36,7 @@ final microsoftIdpConfig = MicrosoftIdpConfig( Use `'common'` for the widest user base. Use a specific tenant ID when building internal applications for a single organization. ::: -### Custom account validation +#### Custom account validation You can customize the validation for Microsoft account details before allowing sign-in. By default, the validation checks that the received account details contain a non-empty userIdentifier. @@ -56,7 +60,7 @@ final microsoftIdpConfig = MicrosoftIdpConfigFromPasswords( Users may choose not to share their email or other information during the Microsoft login flow. Adjust your validation function carefully to avoid blocking legitimate users. ::: -### MicrosoftAccountDetails +#### MicrosoftAccountDetails The `microsoftAccountDetailsValidation` callback receives a `MicrosoftAccountDetails` record with the following properties: @@ -87,30 +91,7 @@ microsoftAccountDetailsValidation: (accountDetails) { The properties available depend on the scopes requested and what the user consented to share. ::: -### Accessing Microsoft APIs - -The default setup allows access to basic user information, such as `name`, `email`. You may require additional access scopes to access other Microsoft APIs, such as accessing a user's calendar, mail, or OneDrive files. - -The default scopes requested are: - -- `openid`: Required for OpenID Connect authentication. -- `profile`: Access to user's basic profile information. -- `email`: Access to user's email address. -- `offline_access`: Allows refresh tokens for long-lived sessions. -- `https://graph.microsoft.com/User.Read`: Access to user's Microsoft Graph profile. - -To request additional scopes, you will need to: - -- Ensure the required API permissions are configured in your Microsoft Entra ID app registration (navigate to **API permissions** in the [Azure Portal](https://portal.azure.com/)). -- Request access to the scopes when signing in. Do this by setting the `scopes` parameter of the `MicrosoftSignInWidget` or `MicrosoftAuthController`. - -A full list of available scopes and Microsoft Graph API permissions can be found in the [Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference). - -:::info -Adding additional scopes may require admin consent depending on your tenant configuration and the sensitivity of the requested permissions. -::: - -### Accessing Microsoft APIs on the server +#### 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. @@ -139,7 +120,7 @@ final microsoftIdpConfig = MicrosoftIdpConfigFromPasswords( ); ``` -## Reacting to account creation +### Reacting to account creation 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. @@ -166,11 +147,40 @@ This callback runs inside the same database transaction as the account creation. 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 +## App configuration + +These options configure Microsoft sign-in in your Flutter app. + +### Requesting additional Microsoft scopes + +The default setup allows access to basic user information, such as `name`, `email`. You may require additional access scopes to access other Microsoft APIs, such as accessing a user's calendar, mail, or OneDrive files. + +The default scopes requested are: + +- `openid`: Required for OpenID Connect authentication. +- `profile`: Access to user's basic profile information. +- `email`: Access to user's email address. +- `offline_access`: Allows refresh tokens for long-lived sessions. +- `https://graph.microsoft.com/User.Read`: Access to user's Microsoft Graph profile. + +To request additional scopes, you will need to: + +- Ensure the required API permissions are configured in your Microsoft Entra ID app registration (navigate to **API permissions** in the [Azure Portal](https://portal.azure.com/)). +- Request access to the scopes when signing in. Do this by setting the `scopes` parameter of the `MicrosoftSignInWidget` or `MicrosoftAuthController`. + +A full list of available scopes and Microsoft Graph API permissions can be found in the [Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference). + +:::info +Adding additional scopes may require admin consent depending on your tenant configuration and the sensitivity of the requested permissions. +::: + +To use the granted scopes from the server with the access token, see [Accessing Microsoft APIs on the server](#accessing-microsoft-apis-on-the-server). + +### Configuring client IDs on the app -### Passing client IDs in code +#### Passing client IDs in code -You can pass the `clientId`, `redirectUri`, and `tenant` directly when initializing the Microsoft Sign-In service: +You can pass the `clientId`, `redirectUri`, and `tenant` directly when initializing the Microsoft sign-in service: ```dart await client.auth.initializeMicrosoftSignIn( @@ -182,9 +192,9 @@ await client.auth.initializeMicrosoftSignIn( 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 configuration during build time using the `--dart-define` option. The Microsoft Sign-In provider supports the following environment variables: +Alternatively, you can pass client configuration during build time using the `--dart-define` option. The Microsoft sign-in provider supports the following environment variables: - `MICROSOFT_CLIENT_ID`: Your Microsoft Application (client) ID - `MICROSOFT_REDIRECT_URI`: The callback URI @@ -208,3 +218,138 @@ This approach is useful when you need to: :::tip You can also set these environment variables in your IDE's run configuration or CI/CD pipeline to avoid passing them manually each time. ::: + +## Customize the sign-in button + +Inside `SignInWidget`, fields set on its `buttonStyle` take precedence over the settings below, as described in [Styling the buttons](../../ui-components#styling-the-buttons). + +:::info +The `SignInWidget` uses the `MicrosoftSignInWidget` internally to display the Microsoft sign-in flow. You can also supply a custom `MicrosoftSignInWidget` to the `SignInWidget` to override the default behavior. + +```dart +SignInWidget( + client: client, + microsoftSignInWidget: MicrosoftSignInWidget( + client: client, + // 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) { /* ... */ }, + ), +) +``` + +::: + +### Using the `MicrosoftSignInWidget` + +The `MicrosoftSignInWidget` handles the complete Microsoft sign-in flow for your Flutter app. + +You can customize the widget's appearance and behavior: + +```dart +MicrosoftSignInWidget( + client: client, + // Button customization. The values shown are the defaults. + style: MicrosoftButtonStyle.light, // or dark + 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. + scopes: const [ + 'openid', + 'profile', + 'email', + 'offline_access', + 'https://graph.microsoft.com/User.Read', + ], + + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, +) +``` + +## Build a custom UI with MicrosoftAuthController + +For more control over the UI, you can use the `MicrosoftAuthController` 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'; + +final controller = MicrosoftAuthController( + client: client, + onAuthenticated: () { + // Do something when the user is authenticated. + // + // NOTE: You should not navigate to the home screen here, otherwise + // the user will have to sign in again every time they open the app. + }, + onError: (error) { + // Handle errors + }, + scopes: const [ + 'openid', + 'profile', + 'email', + 'offline_access', + 'https://graph.microsoft.com/User.Read', + ], +); + +// Initiate sign-in +await controller.signIn(); +``` + +### MicrosoftAuthController 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: + +```dart +// Check current state +final state = controller.state; // MicrosoftAuthState enum + +// Check if loading +final isLoading = controller.isLoading; + +// Check if authenticated +final isAuthenticated = controller.isAuthenticated; + +// Get error message +final errorMessage = controller.errorMessage; + +// Listen to state changes +controller.addListener(() { + setState(() { + // Rebuild UI when controller state changes + }); +}); +``` + +#### MicrosoftAuthController states + +- `MicrosoftAuthState.idle` - Ready for user interaction. +- `MicrosoftAuthState.loading` - Processing a sign-in request. +- `MicrosoftAuthState.error` - An error occurred. +- `MicrosoftAuthState.authenticated` - Authentication was successful. + +## Related + +- [Setup](./setup): set up Microsoft sign-in on the server and in your app. +- [Troubleshooting](./troubleshooting): fix common Microsoft sign-in errors. +- [UI components](../../ui-components): use and style the built-in sign-in UI. +- [Working with users](../../working-with-users): manage auth users and react to user events. 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 deleted file mode 100644 index ceaae1e5..00000000 --- a/docs/06-concepts/04-authentication/05-providers/08-microsoft/03-customizing-the-ui.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -sidebar_label: Customizing the UI -description: Microsoft sign-in UI can be customized with the MicrosoftSignInWidget and MicrosoftAuthController to build a custom flow. ---- - -# Customize the Microsoft sign-in UI - -When using the Microsoft identity provider, you can customize the UI to your liking. You can use the `MicrosoftSignInWidget` to display the Microsoft Sign-In flow in your own custom UI, or you can use the `MicrosoftAuthController` to build a completely custom authentication interface. - -:::info -The `SignInWidget` uses the `MicrosoftSignInWidget` internally to display the Microsoft Sign-In flow. You can also supply a custom `MicrosoftSignInWidget` to the `SignInWidget` to override the default behavior. - -```dart -SignInWidget( - client: client, - microsoftSignInWidget: MicrosoftSignInWidget( - client: client, - // 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) { /* ... */ }, - ), -) -``` - -::: - -## Using the `MicrosoftSignInWidget` - -The `MicrosoftSignInWidget` handles the complete Microsoft Sign-In flow for your Flutter app. - -You can customize the widget's appearance and behavior: - -```dart -MicrosoftSignInWidget( - client: client, - // Button customization. The values shown are the defaults. - style: MicrosoftButtonStyle.light, // or dark - 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. - scopes: const [ - 'openid', - 'profile', - 'email', - 'offline_access', - 'https://graph.microsoft.com/User.Read', - ], - - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, -) -``` - -## Building a custom UI with the `MicrosoftAuthController` - -For more control over the UI, you can use the `MicrosoftAuthController` 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'; - -final controller = MicrosoftAuthController( - client: client, - onAuthenticated: () { - // Do something when the user is authenticated. - // - // NOTE: You should not navigate to the home screen here, otherwise - // the user will have to sign in again every time they open the app. - }, - onError: (error) { - // Handle errors - }, - scopes: const [ - 'openid', - 'profile', - 'email', - 'offline_access', - 'https://graph.microsoft.com/User.Read', - ], -); - -// Initiate sign-in -await controller.signIn(); -``` - -### MicrosoftAuthController 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: - -```dart -// Check current state -final state = controller.state; // MicrosoftAuthState enum - -// Check if loading -final isLoading = controller.isLoading; - -// Check if authenticated -final isAuthenticated = controller.isAuthenticated; - -// Get error message -final errorMessage = controller.errorMessage; - -// Listen to state changes -controller.addListener(() { - setState(() { - // Rebuild UI when controller state changes - }); -}); -``` - -#### MicrosoftAuthController states - -- `MicrosoftAuthState.idle` - Ready for user interaction. -- `MicrosoftAuthState.loading` - Processing a sign-in request. -- `MicrosoftAuthState.error` - An error occurred. -- `MicrosoftAuthState.authenticated` - Authentication was successful. diff --git a/docs/06-concepts/04-authentication/05-providers/08-microsoft/03-troubleshooting.md b/docs/06-concepts/04-authentication/05-providers/08-microsoft/03-troubleshooting.md new file mode 100644 index 00000000..0b0ecdb6 --- /dev/null +++ b/docs/06-concepts/04-authentication/05-providers/08-microsoft/03-troubleshooting.md @@ -0,0 +1,251 @@ +--- +sidebar_label: Troubleshooting +description: Sign in with Microsoft failures, from setup mistakes to token exchange errors, and how to diagnose and resolve each one in your Serverpod app. +--- + +# Troubleshoot Microsoft sign-in + +This page helps you identify common Sign in with Microsoft failures, explains why they occur, and shows how to resolve them. For underlying issues with the OAuth callback library, see the [flutter_web_auth_2 documentation](https://pub.dev/packages/flutter_web_auth_2). + +## Setup checklist + +Go through this before investigating a specific error. Most problems come from a missed step. + +#### Microsoft Entra ID portal + +- [ ] Created an app registration in [Microsoft Entra ID](https://portal.azure.com/). +- [ ] Chose **Supported account types** that cover everyone who should sign in, and that match your `tenant` setting. +- [ ] Registered every redirect URI you actually use under **Authentication**, each under the right platform: **Web** for `https://your-domain.com/auth.html`, **iOS / macOS** with your bundle ID, and **Android** with your package name and signature hash. +- [ ] Created a client secret and copied its **Value** (not the Secret ID). Microsoft only shows it once. + +#### Server + +- [ ] Added `microsoftClientId` and `microsoftClientSecret` to `config/passwords.yaml` under the matching environment (`development:` for local, `production:` for prod), or set the matching `SERVERPOD_PASSWORD_microsoftClientId` and `SERVERPOD_PASSWORD_microsoftClientSecret` environment variables. The `microsoftTenant` key is optional and defaults to `common`. +- [ ] Added `MicrosoftIdpConfigFromPasswords()` to `identityProviderBuilders` in `server.dart`. +- [ ] Created an endpoint that extends `MicrosoftIdpBaseEndpoint`. +- [ ] Started the server with `serverpod start`, then created and applied the migration (pressed **M**). + +#### Flutter app + +- [ ] Added `client.auth.initializeMicrosoftSignIn(clientId: ..., redirectUri: ...)` after `client.auth.initialize()` in your Flutter app's `main.dart`. +- [ ] Both `clientId` and `redirectUri` match values registered on the app registration. +- [ ] The `tenant` passed on initialization matches the server's `tenant` setting. Both default to `common`. +- [ ] On **Android**, added the `flutter_web_auth_2` `CallbackActivity` to `AndroidManifest.xml` with the **exact** scheme and host used in your callback URL. +- [ ] On **Web**, created `web/auth.html` in your Flutter project with the callback script from [Web callback page (`auth.html`)](../../setup#web-callback-page-authhtml). +- [ ] On **Web**, ran Flutter on a fixed `--web-port` matching the port in the registered redirect URI. + +## Sign-in fails with a redirect URI error + +**Problem:** The browser opens the Microsoft sign-in page, but instead of completing, Microsoft shows an error page saying the redirect URI is not valid for the application. The app never receives a callback. + +**Cause:** The `redirectUri` your Flutter app sent to Microsoft does not exactly match any redirect URI registered on your app registration, or the URI is registered under the wrong platform. + +**Resolution:** Open your app registration's **Authentication** page and verify the registered redirect URIs match your app's `redirectUri` exactly. The match is strict: scheme, host, port, path, casing, and trailing slashes all count. + +Common mistakes: + +- Trailing slashes (`https://your-domain.com/auth.html/`) or port differences. +- Wrong scheme (`http` vs `https`, or a mismatched custom scheme like `myapp:` vs `MyApp:`). +- The URI registered under the wrong platform, for example a web URL added under a native platform. +- Flutter dev server running on a random port. Pass `--web-port=` to `flutter run` so the origin is stable across restarts. + +## Callback never returns to the Flutter app + +**Problem:** The user signs in on the Microsoft page successfully, but the Flutter app never receives the result. The browser sits on a blank page or the sign-in window hangs. + +**Cause:** The browser was redirected to a URL that does not serve the callback page (web), or the callback custom scheme is not registered with the platform (mobile). + +**Resolution:** + +- **Web**: 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). The page posts the result back to its own origin, so your Flutter web app must be served from the same scheme, host, and port as the callback URL. +- **Android**: Verify the `` values in `AndroidManifest.xml` match the scheme and host 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 fails with an access token verification error + +**Problem:** The sign-in flow completes on the Microsoft page, but the app then reports "An error occurred while verifying the Microsoft access token. Please check your Microsoft account and try again. If the problem persists, please contact support." + +**Cause:** The server threw a `MicrosoftAccessTokenVerificationException`. This exception is deliberately generic so it does not leak details to potential attackers. It covers every server-side failure between receiving the authorization code and validating the account: + +- The token exchange with Microsoft was rejected. Typical reasons are a wrong or expired client secret (Microsoft Entra ID secrets always have an expiry date), a `tenant` mismatch between the app and the server, or a reused or expired authorization code. +- The user info request to Microsoft Graph failed, usually because custom scopes dropped `https://graph.microsoft.com/User.Read`. See [Sign-in breaks after changing the scopes parameter](#sign-in-breaks-after-changing-the-scopes-parameter). +- A custom `microsoftAccountDetailsValidation` callback threw. See [Custom account validation](./customizations#custom-account-validation). +- A `getExtraMicrosoftInfoCallback` threw. See [Calls from getExtraMicrosoftInfoCallback fail or block sign-in](#calls-from-getextramicrosoftinfocallback-fail-or-block-sign-in). + +**Resolution:** The server logs the underlying cause at debug level for token exchange, user info, and callback failures. A throwing validation callback surfaces only as a generic invalid-user-info error. Check the server logs with debug logging visible, then work through the matching cause above. If the log shows a token exchange error, verify the client secret is current and that the `tenant` values on the server and in `initializeMicrosoftSignIn` are the same. + +## clientId or redirectUri missing at initialization + +**Problem:** The app throws an `ArgumentError` on startup saying the Microsoft client ID or redirect URI is required. + +**Cause:** Microsoft has no native platform-specific config files. The `clientId` and `redirectUri` must be passed explicitly to `initializeMicrosoftSignIn`, or read from `--dart-define`. + +**Resolution:** Either pass the values directly: + +```dart +await client.auth.initializeMicrosoftSignIn( + clientId: 'your-microsoft-client-id', + redirectUri: 'myapp://auth', +); +``` + +Or read them from `--dart-define`: + +```bash +flutter run \ + --dart-define=MICROSOFT_CLIENT_ID=your-microsoft-client-id \ + --dart-define=MICROSOFT_REDIRECT_URI=myapp://auth +``` + +The tenant has no environment variable. Pass it as an argument when you initialize Microsoft sign-in. See [Configuring client IDs on the app](./customizations#configuring-client-ids-on-the-app). + +## The sign-in button does nothing and onError never fires + +**Problem:** Tapping the Microsoft sign-in button appears to do nothing, or the flow opens and closes, and the `onError` callback is never called. + +**Cause:** The `onError` callback only receives errors that are safe to show to the user. Configuration mistakes and flow interruptions are not passed to it: + +- A `StateError` because `initializeMicrosoftSignIn` was never called before the button was used. +- The user cancelled the sign-in window. +- The OAuth flow failed before reaching the server, for example when Microsoft returned an error on the callback. + +**Resolution:** Check the debug console. The controller prints every failure as `[MicrosoftAuthController] Authentication error: ...` before deciding whether to surface it. If the log shows a `StateError`, move `initializeMicrosoftSignIn` so it runs during app startup, before any sign-in UI is shown. + +## Changed initialization values do not take effect + +**Problem:** You changed the `clientId`, `redirectUri`, or `tenant` passed to `initializeMicrosoftSignIn`, but the app keeps using the old values. + +**Cause:** Initialization is idempotent. Only the first call in the app's lifetime stores the configuration, and later calls return without changing it. A hot reload keeps the old configuration alive. + +**Resolution:** After changing any initialization value, do a hot restart or fully restart the app. Either one re-runs initialization with the new values. + +## Sign-in breaks after changing the scopes parameter + +**Problem:** Sign-in worked, then you set the `scopes` parameter on `MicrosoftSignInWidget` or `MicrosoftAuthController` to request extra permissions, and now every sign-in fails with the access token verification error. + +**Cause:** The `scopes` parameter replaces the default scopes instead of adding to them. The server fetches the user's details from Microsoft Graph during sign-in, which requires the `https://graph.microsoft.com/User.Read` scope. Dropping it breaks the account details fetch, and dropping the OpenID Connect scopes breaks the sign-in itself. + +**Resolution:** Include the defaults alongside your extra scopes: + +```dart +MicrosoftSignInWidget( + client: client, + scopes: [ + ...MicrosoftAuthController.defaultScopes, + 'https://graph.microsoft.com/Calendars.Read', + ], +) +``` + +See [Requesting additional Microsoft scopes](./customizations#requesting-additional-microsoft-scopes) for the default scope list. + +## Users see a "Need admin approval" screen + +**Problem:** A user signs in and Microsoft shows a screen saying the app needs admin approval instead of completing the flow. + +**Cause:** One of the requested scopes requires admin consent in the user's organization. Work and school tenants can require an administrator to approve permissions before any user can grant them. + +**Resolution:** Ask an administrator of that tenant to grant consent for the app's permissions, under **API permissions** in the app registration. Alternatively, remove the scope that triggers the requirement if you do not strictly need it. The default scopes normally do not require admin consent. + +## Some account types cannot sign in + +**Problem:** Sign-in works for some users, but others get a Microsoft error page saying their account cannot be used with this application. + +**Cause:** Two settings restrict which accounts can sign in, and both must allow the user: + +- The **Supported account types** choice on the app registration. +- The `tenant` value in your configuration. Use `common` for personal and work/school accounts, `organizations` for work/school accounts only, `consumers` for personal accounts only, or a tenant ID for a single organization. + +**Resolution:** Align both settings with your audience. Set the `tenant` in the server configuration and pass the same value to `initializeMicrosoftSignIn` in the Flutter app. Update **Supported account types** on the app registration to match. See [Tenant configuration](./customizations#tenant-configuration). + +## Sign-in works on mobile but fails on web, or the reverse + +**Problem:** Microsoft sign-in works on Android and iOS, but fails on web with the access token verification error, or the other way around. + +**Cause:** Microsoft requires the client secret during token exchange for web apps and rejects it for native apps. Serverpod handles this through the `isWebPlatform` flag on the login endpoint, which the provided widgets pass automatically. A platform-specific failure usually means the redirect URI is registered under the wrong platform on the app registration, so Microsoft applies the wrong rules to the exchange. + +**Resolution:** On the app registration's **Authentication** page, confirm web redirect URIs sit under the **Web** platform and native ones under the **iOS / macOS** or **Android** platforms. If you built a custom flow that calls the login endpoint directly, pass `isWebPlatform: true` on web and `false` everywhere else. + +## Sign-in succeeds but the user has no email, or the wrong one + +**Problem:** The user signs in successfully, but the server-side `MicrosoftAccountDetails.email` value is `null`, or the stored email is not an address the user recognizes. + +**Cause:** Microsoft Graph does not always return a mail address. The provider reads the `mail` field first and falls back to `userPrincipalName`, then stores the result in lowercase. The `mail` field can be empty for accounts without a mailbox, and `userPrincipalName` is a sign-in identifier that is not always a real address, especially for guest accounts. A custom validator that requires an email will block these users, and the app then shows the access token verification error. + +**Resolution:** + +- If you do not strictly need an email, relax your validator. The default validator only checks that `userIdentifier` is non-empty. See [Custom account validation](./customizations#custom-account-validation). +- Treat the stored email as informational rather than as a verified mailbox, or collect an email in your own onboarding flow when you need a reliable one. + +## Users have no profile photo + +**Problem:** Users sign in successfully, but their profiles have no photo. + +**Cause:** The provider fetches the photo from Microsoft Graph when `fetchProfilePhoto` on `MicrosoftIdpConfig` is enabled, which is the default. A failed photo fetch never fails the sign-in. It is logged and skipped. Microsoft also does not return a photo for every account. For returning users, the photo is only set when the profile does not already have one. + +**Resolution:** Confirm `fetchProfilePhoto` is not set to `false` if you expect photos. Accept that some accounts have none. If sign-in speed matters more than photos, disable the option. See [Profile photos](../../profile-photos) for how photos are stored. + +## Calls from getExtraMicrosoftInfoCallback fail or block sign-in + +**Problem:** After adding a `getExtraMicrosoftInfoCallback`, sign-ins start failing with the access token verification error, or external API calls inside the callback fail intermittently. + +**Cause:** The callback runs on **every** authentication attempt, before the system determines whether the user is new or returning. Any exception it throws aborts the whole sign-in. + +**Resolution:** + +- Wrap external calls in `try`/`catch` so a Microsoft Graph outage or rate limit does not block sign-in. +- Cache what you fetch in your own tables, keyed by `MicrosoftAccountDetails.userIdentifier`, instead of re-fetching on every sign-in. +- Do not create `MicrosoftAccount`, `UserProfile`, or `AuthUser` records inside the callback. That breaks new account detection and profile creation. See [Accessing Microsoft APIs on the server](./customizations#accessing-microsoft-apis-on-the-server). + +## Sign-in works in dev but fails after deploy + +**Problem:** Microsoft sign-in works locally but fails in production with a redirect URI error or the access token verification error. + +**Cause:** The production redirect URI is not registered on the app registration, or the production Flutter build is using the dev `redirectUri`, or the production server has no Microsoft credentials configured. + +**Resolution:** + +1. Confirm the production redirect URI is registered on the app registration 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 [Using environment variables](./customizations#using-environment-variables). +3. Confirm the production environment provides `microsoftClientId` and `microsoftClientSecret`, in `config/passwords.yaml` under `production:` or through the `SERVERPOD_PASSWORD_` environment variables. + +## Server fails to start with a missing password error + +**Problem:** The server crashes on startup with a `PasswordNotFoundException` naming `microsoftClientId` or `microsoftClientSecret`. + +**Cause:** You use `MicrosoftIdpConfigFromPasswords()`, and the named key is missing from `config/passwords.yaml` and from the environment. The exception message names the exact key and the environment variable it also looked for. + +**Resolution:** Confirm both keys exist under the section matching your run mode (`development:` when running locally with `serverpod start`, `production:` when deployed): + +```yaml +development: + microsoftClientId: 'your-microsoft-client-id' + microsoftClientSecret: 'your-microsoft-client-secret' + microsoftTenant: 'common' # optional, defaults to common +``` + +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 Microsoft sign-in with "no such table" + +**Problem:** The server builds and starts, but crashes when a user tries to sign in with Microsoft. The error cites a missing table such as `serverpod_auth_idp_microsoft_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 and apply the migration. + +## Android sign-in opens Microsoft but the callback never fires + +**Problem:** On Android, tapping the Microsoft sign-in button opens the Microsoft authorization page in a browser, but after authorizing, the browser stays open and the Flutter app never resumes. + +**Cause:** The `CallbackActivity` in `AndroidManifest.xml` is missing, has a wrong scheme or host, or `android:exported` is not set to `true`. + +**Resolution:** Open `android/app/src/main/AndroidManifest.xml` and confirm the `CallbackActivity` block exists with `android:exported="true"` and the `` values matching your callback URL exactly. The block is shown in [Android setup](./setup#android). + +After editing the manifest, run `flutter clean` and rebuild. + +## Related + +- [Setup](./setup): set up Microsoft sign-in on the server and in your app. +- [Customizations](./customizations): configuration options and sign-in UI customization. +- [UI components](../../ui-components): the sign-in widgets and how to compose them. 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 fe9a13f3..e8997f93 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 @@ -32,6 +32,9 @@ import 'package:serverpod/serverpod.dart'; import 'package:serverpod_auth_idp_server/core.dart'; import 'package:serverpod_auth_idp_server/providers/passkey.dart'; +import 'src/generated/endpoints.dart'; +import 'src/generated/protocol.dart'; + void run(List args) async { final pod = Serverpod( args, diff --git a/docs/06-concepts/04-authentication/05-providers/09-passkey/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/09-passkey/03-customizing-the-ui.md deleted file mode 100644 index 08d94e0a..00000000 --- a/docs/06-concepts/04-authentication/05-providers/09-passkey/03-customizing-the-ui.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -sidebar_label: Customizing the UI -description: Passkey sign-in UI can be built with the generated client endpoints and WebAuthn APIs while official Flutter widgets are pending. ---- - -# Customize the passkey sign-in UI - -:::warning -Flutter UI components for passkeys are not yet available. This section will be updated once official Flutter widgets are released. For now, you'll need to build custom UI using the generated client endpoints and WebAuthn APIs. -::: 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 c05f6c0e..f68db7cd 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 @@ -1,13 +1,28 @@ --- -description: Custom providers extend Serverpod's authentication module with your own identity providers, alongside all the built-in ones the module ships with. +description: Custom identity providers plug your own sign-in method into Serverpod's authentication module, next to the built-in providers, with the same tokens, sessions, and user handling. --- # Custom providers -Serverpod's authentication module lets you implement custom authentication providers. You can use all the existing providers supplied by the module along with the specific providers your project requires. +A custom identity provider plugs your own sign-in method into the authentication module. It issues the same tokens, creates the same auth users, and appears to your app like any built-in provider. Build one when the provider you need is not among the built-in ones, or when you authenticate against your own user directory. -:::note -This section is under development and will be updated soon. +A custom provider consists of a few parts: -The package also provides general-purpose utilities to support building IDPs. See [OAuth2 Utility](./oauth2-utility/setup). -::: +- **A provider class** that implements the `IdentityProvider` contract on the server. +- **A config class** extending `IdentityProviderBuilder`, which you pass to `initializeAuthServices` like any built-in config. +- **An endpoint** extending `IdpBaseEndpoint`, which your app calls to sign in. +- **An account model** linking the provider's user identity to the Serverpod auth user. +- **A controller** in your app that runs the sign-in flow and registers the returned session. + +How to build them depends on the kind of provider: + +- **OAuth2-based providers** (most third-party services): the module ships utilities that handle the PKCE flow, token exchange, and error handling on both sides. Start with the [OAuth2 utility setup](./oauth2-utility/setup), then follow [creating an OAuth2-based identity provider](./oauth2-utility/creating-an-oauth2-based-identity-provider) for the full walkthrough. +- **Everything else** (your own credential store, an internal single sign-on system, an API-key exchange): the same parts apply, without the OAuth2 utilities. The walkthrough still shows the shape of each part. + +To replace the authentication module entirely rather than add a provider to it, see [custom overrides](../../custom-overrides) instead. + +## Related + +- [OAuth2 utility setup](./oauth2-utility/setup): the client and server utilities for OAuth2 flows. +- [Creating an OAuth2-based identity provider](./oauth2-utility/creating-an-oauth2-based-identity-provider): the complete walkthrough. +- [Custom overrides](../../custom-overrides): replace the built-in authentication entirely. 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 3483d0d1..42407621 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 @@ -13,7 +13,7 @@ The OAuth2 utility consists of client-side and server-side components that work - **Server-side (`OAuth2PkceUtil`)**: Exchanges authorization codes for access tokens on your backend. :::info -The [GitHub IDP](../../github/setup) is built using these utilities, serving as a reference implementation for developers creating custom providers. +The [GitHub provider](../../github/setup) is built using these utilities, serving as a reference implementation for developers creating custom providers. ::: ## Understanding OAuth2 with PKCE @@ -329,39 +329,7 @@ Add the callback activity to your `AndroidManifest.xml`: #### Web -Create an HTML callback page in your `./web` folder (e.g., `auth.html`): - -```html - -Authentication complete -

Authentication is complete. If this does not happen automatically, please close the window.

- -``` - -:::note -You only need a single callback file (e.g. `auth.html`) in your `./web` folder. -This file is shared across all IDPs that use the OAuth2 utility, as long as your redirect URIs point to it. -::: - -Make sure your redirect URI points to the callback file, e.g. `https://yourdomain.com/auth.html` +Web sign-in needs the shared callback page that hands the OAuth2 result back to your app. Set it up once as described in [Web callback page (`auth.html`)](../../../setup#web-callback-page-authhtml). The same page serves every provider built on the OAuth2 utility, as long as your redirect URIs point to it, for example `https://yourdomain.com/auth.html`. ## Complete example of a custom provider 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 e488837a..902e5ff0 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 @@ -1,10 +1,10 @@ --- -description: Building a custom OAuth2 identity provider with Serverpod's OAuth2 utility, shown through a complete, working example you can adapt for your own. +description: A complete, working custom OAuth2 identity provider built with Serverpod's OAuth2 utility, ready to adapt for your own provider. --- -# Creating an OAuth2-based Identity Provider +# Create an OAuth2-based identity provider -This page provides a complete, working implementation of a custom OAuth2 provider. The [GitHub IDP](../../github/setup) is built the same way, using the same OAuth2 utility shown here, so this example illustrates the general pattern you can follow when creating your own IDP. +This page provides a complete, working implementation of a custom OAuth2 provider. The [GitHub provider](../../github/setup) is built the same way, using the same OAuth2 utility shown here, so this example illustrates the general pattern for your own provider. ## Overview diff --git a/docs/06-concepts/04-authentication/07-ui-components.md b/docs/06-concepts/04-authentication/07-ui-components.md index 310c073c..40bc3d44 100644 --- a/docs/06-concepts/04-authentication/07-ui-components.md +++ b/docs/06-concepts/04-authentication/07-ui-components.md @@ -102,7 +102,7 @@ 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). +For all options of each provider widget, see the provider's customizations page, which also covers building a custom UI with the provider's controller. For example, see [the Google provider](./providers/google/customizations#customize-the-sign-in-button) or [the email provider](./providers/email/customizing-the-ui). ## Localization