From f3377151a6d72b037ee67f60ec351015da06957d Mon Sep 17 00:00:00 2001 From: Alexander Sandor Date: Thu, 22 Oct 2026 08:56:01 +0200 Subject: [PATCH 1/4] docs: Document passwordless email sign-in with a one-time code Covers the OTP flow added in serverpod/serverpod#5869: the EmailOtpIdpBaseEndpoint, the sendOtpLoginVerificationCode callback and its fallback, the new EmailIdpConfig options and rate limits, the EmailOtpSignInWidget and EmailOtpAuthController, the new texts, and the admin cleanup methods. Updates the get started walkthrough to the flow new projects now use. Co-Authored-By: Claude Opus 5.5 --- .../04-authentication/01-get-started.md | 15 +- .../06-concepts/04-authentication/02-setup.md | 4 +- .../05-providers/02-email/01-setup.md | 66 +++++-- .../05-providers/02-email/02-configuration.md | 44 ++++- .../02-email/03-customizing-the-ui.md | 162 +++++++++++++++++- .../02-email/04-admin-operations.md | 23 ++- .../04-authentication/07-ui-components.md | 33 +++- .../03-migrate-from-legacy-auth.md | 2 +- 8 files changed, 316 insertions(+), 33 deletions(-) diff --git a/docs/06-concepts/04-authentication/01-get-started.md b/docs/06-concepts/04-authentication/01-get-started.md index 2eebff77..97b79d95 100644 --- a/docs/06-concepts/04-authentication/01-get-started.md +++ b/docs/06-concepts/04-authentication/01-get-started.md @@ -58,22 +58,27 @@ This sets up the database and starts the server and your app. The app opens on the sign-in screen. To create an account: -1. Choose to create a new account, enter your email, and continue. -2. Look in the server console for the verification code. While testing, it is logged instead of emailed: +1. Enter your email and select **Send code**. +2. Look in the server console for the code. While testing, it is logged instead of emailed: ```text - Registration code for you@example.com: <12345678> + Login code for you@example.com: <123456> ``` -3. Enter the code, then set a password to finish. +3. Enter the code, then select **Create account** to finish. -Once you are signed in, the app shows your content with a sign-out button. +Once you are signed in, the app shows your content with a sign-out button. The next time you sign in with the same email, entering the code takes you straight to your account. + +:::note +Projects created before Serverpod 4.1 use password sign-in, so you set a password when you create the account. To switch, see [Switch an existing project to passwordless sign-in](./providers/email/setup#switch-an-existing-project-to-passwordless-sign-in). +::: ## Next steps - Send verification emails from your own email service instead of the default. See [Use your own email provider](./providers/email/setup#use-your-own-email-provider). - Customize the sign-in screen. See [UI components](./ui-components). - Build your own sign-in UI with the email controller. See [Customizing the UI](./providers/email/customizing-the-ui). +- Let users sign in with a password as well as a code. See [Customizing the UI](./providers/email/customizing-the-ui#using-the-emailotpsigninwidget). - Change password rules, code length, and rate limits. See [Email configuration](./providers/email/configuration). - Require sign-in on your endpoints. See [The basics](./basics#requiring-authentication-on-endpoints). - Add more sign-in options, like Google or Apple. See [Identity providers configuration](./setup#identity-providers-configuration). diff --git a/docs/06-concepts/04-authentication/02-setup.md b/docs/06-concepts/04-authentication/02-setup.md index 547b9a87..27883e4f 100644 --- a/docs/06-concepts/04-authentication/02-setup.md +++ b/docs/06-concepts/04-authentication/02-setup.md @@ -82,7 +82,7 @@ For more details on how to configure token managers or create custom ones, see t Identity providers handle authentication with different methods (Email, Google, Apple, etc.). Each provider has its own configuration: -- **[Email](./providers/email/setup)**: sign-up and sign-in with email and password. +- **[Email](./providers/email/setup)**: sign-in with a one-time code sent by email, or with email and password. - **[Anonymous](./providers/anonymous/setup)** (experimental): accounts without any credentials, for trying the app before registering. - **[Google](./providers/google/setup)**, **[Apple](./providers/apple/setup)**, **[Facebook](./providers/facebook/setup)**, **[GitHub](./providers/github/setup)**, and **[Microsoft](./providers/microsoft/setup)**: sign-in with the respective account. - **[Firebase](./providers/firebase/setup)**: reuse Firebase Authentication, including its phone and social sign-ins. @@ -115,7 +115,7 @@ By default, endpoints for all providers are disabled. To enable a provider: ```dart import 'package:serverpod_auth_idp_server/providers/email.dart'; - class EmailIdpEndpoint extends EmailIdpBaseEndpoint {} + class EmailIdpEndpoint extends EmailOtpIdpBaseEndpoint {} ``` 3. Start the server with `serverpod start`. It generates the client code and endpoint methods for the provider, then runs the server with hot reload. diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md b/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md index 8fb81301..bc49c518 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md @@ -1,11 +1,16 @@ --- sidebar_label: Setup -description: Sign in with Email lets users authenticate with an email and password, with verification codes delivered by Serverpod Cloud or your own email provider. +description: Sign in with Email lets users authenticate with a one-time code or a password, with codes delivered by Serverpod Cloud or your own email provider. --- # Set up email sign-in -Sign in with Email verifies the user's address with a code, both when they register and when they reset their password. +Sign in with Email supports two flows: + +- **Passwordless sign-in.** The user enters their email and receives a one-time code. Entering the code signs them in, or creates an account if there is none for the email yet. New projects use this flow. +- **Password sign-in.** The user registers with an email and a password, and signs in with them. A code sent by email verifies the address on registration and on password reset. + +Both flows share the same accounts, and passwordless sign-in can optionally let users sign in with a password too. :::caution You need to install the auth module before you continue, see [Setup](../../setup). @@ -43,16 +48,33 @@ If you are adding the auth module to an existing project, extend the abstract en ```dart import 'package:serverpod_auth_idp_server/providers/email.dart'; -class EmailIdpEndpoint extends EmailIdpBaseEndpoint {} +class EmailIdpEndpoint extends EmailOtpIdpBaseEndpoint {} ``` +For password sign-in only, extend `EmailIdpBaseEndpoint` instead. + Then start the server with `serverpod start` to generate the client code, and create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). +#### Switch an existing project to passwordless sign-in + +Projects created before Serverpod 4.1 extend `EmailIdpBaseEndpoint` and use password sign-in. To switch to passwordless sign-in: + +1. In your email endpoint file, change the base class to `EmailOtpIdpBaseEndpoint`: + + ```dart + class EmailIdpEndpoint extends EmailOtpIdpBaseEndpoint {} + ``` + +2. Start the server with `serverpod start` to regenerate the client code, then create and apply the migration (in the `serverpod start` terminal, press **M**). +3. If you use [your own email provider](#use-your-own-email-provider), add a `sendOtpLoginVerificationCode` callback. + +Existing accounts keep working, and users who registered with a password can sign in with a code sent to the same email. To keep password sign-in available in the app as well, set `allowPasswordSignIn: true` on the widget, as shown in [client-side configuration](#client-side-configuration). + ### Use your own email provider Serverpod Cloud delivery is there to get sign-in working quickly, and it sends a standard message carrying your `appDisplayName`. You might prefer using a custom email provider to have full control over the body, layout, and language of the emails. For servers hosted outside of Serverpod Cloud, it is the only option. -Changing the email provider is done by replacing `ServerpodCloudEmailIdpConfig` with `EmailIdpConfigFromPasswords`, which requires you to pass your own callbacks for the two codes. One convenient option is the [mailer](https://pub.dev/packages/mailer) package, which can send emails through any SMTP service. Most email providers, such as Resend, SendGrid, or Mandrill, support SMTP. +Changing the email provider is done by replacing `ServerpodCloudEmailIdpConfig` with `EmailIdpConfigFromPasswords`, which requires you to pass your own callbacks for sending the codes. One convenient option is the [mailer](https://pub.dev/packages/mailer) package, which can send emails through any SMTP service. Most email providers, such as Resend, SendGrid, or Mandrill, support SMTP. ```dart import 'package:serverpod_auth_idp_server/core.dart'; @@ -71,6 +93,8 @@ void run(List args) async { // Configure the Email Identity Provider // This is the basic configuration for the Email IDP to work. EmailIdpConfigFromPasswords( + // Callback to send the one-time sign-in code to the user. + sendOtpLoginVerificationCode: _sendOtpLoginCode, // Callback to send the registration verification code to the user. sendRegistrationVerificationCode: _sendRegistrationCode, // Callback to send the password reset verification code to the user. @@ -82,6 +106,18 @@ void run(List args) async { await pod.start(); } +void _sendOtpLoginCode( + Session session, { + required String email, + required UuidValue otpLoginRequestId, + required String verificationCode, + required Transaction? transaction, +}) { + // NOTE: Here you call your mail service to send the sign-in code to the + // user. For testing, we will log the code. + session.log('[EmailIDP] Login code ($email): $verificationCode'); +} + void _sendRegistrationCode( Session session, { required String email, @@ -109,24 +145,32 @@ void _sendPasswordResetCode( #### Basic configuration options +- `sendOtpLoginVerificationCode`: A callback that will be called to send the one-time code for passwordless sign-in. It is called whether or not the email already has an account, so write the email to work for both signing in and creating an account. - `sendRegistrationVerificationCode`: A callback that will be called to send the registration verification code to the user. Here you should call the email sending service to send the verification code to the user. - `sendPasswordResetVerificationCode`: A callback that will be called to send the password reset verification code to the user. Here you should call the email sending service to send the verification code to the user. +:::warning +If you don't set `sendOtpLoginVerificationCode`, sign-in codes are sent through `sendRegistrationVerificationCode` instead, and a warning is logged the first time this happens. That callback then receives the ID of the sign-in request as its `accountRequestId`, so any links built from it, such as a link to complete the registration, won't work. Set `sendOtpLoginVerificationCode` if your registration emails contain more than the code. If neither callback is set, requesting a code fails with a `StateError`. +::: + Both configurations hash passwords and verification codes with a secret pepper, which `EmailIdpConfigFromPasswords` reads from the `emailSecretHashPepper` key in `config/passwords.yaml` or the `SERVERPOD_PASSWORD_emailSecretHashPepper` environment variable. Newly generated projects already have a value for every run mode. To pass the pepper directly instead, use `EmailIdpConfig` with its `secretHashPepper` parameter. For more details on configuration options, such as customizing password requirements, verification code generation, rate limiting, and more, see the [configuration section](./configuration). ## Client-side configuration -If you have configured the `SignInWidget` as described in the [setup section](../../setup#present-the-authentication-ui), the Email identity provider will be automatically detected and displayed in the sign-in widget. +If you have configured the `SignInWidget` as described in the [setup section](../../setup#present-the-authentication-ui), the Email identity provider will be automatically detected and displayed in the sign-in widget. It shows passwordless sign-in when your endpoint extends `EmailOtpIdpBaseEndpoint`, and password sign-in when it extends `EmailIdpBaseEndpoint`. -You can also use the `EmailSignInWidget` to include the email authentication flow in your own custom UI. +You can also use the `EmailOtpSignInWidget` to include passwordless sign-in in your own custom UI. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; -EmailSignInWidget( +EmailOtpSignInWidget( client: client, + // Also let users sign in with a password and reset a forgotten one. + // Defaults to false. + allowPasswordSignIn: true, onAuthenticated: () { // Do something when the user is authenticated. // @@ -144,9 +188,11 @@ EmailSignInWidget( The widget automatically handles: -- Login with email and password. -- Registration with terms acceptance and email verification. -- Password reset flow with email verification. +- Sending and verifying the one-time code. +- Sign-in to an existing account, or account creation with terms acceptance for a new email. +- Password sign-in and password reset, if `allowPasswordSignIn` is `true`. - Navigation between screens. +For password sign-in, use the `EmailSignInWidget` with the same `client`, `onAuthenticated`, and `onError` parameters. It handles login with email and password, registration with email verification, and password reset. + For details on how to display the email authentication UI in your Flutter app, see the [customizing the UI section](./customizing-the-ui). diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md b/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md index f589cd85..dff2e7a1 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md @@ -9,7 +9,7 @@ This page covers configuration options for the email identity provider beyond th ## Configuration options -New projects use `ServerpodCloudEmailIdpConfig`. It reads the [pepper](#peppering) from the `emailSecretHashPepper` password, but it does not take the other options on this page. To set them, switch to `EmailIdpConfigFromPasswords` and pass your own `sendRegistrationVerificationCode` and `sendPasswordResetVerificationCode` callbacks. See [Use your own email provider](./setup#use-your-own-email-provider). +New projects use `ServerpodCloudEmailIdpConfig`. It reads the [pepper](#peppering) from the `emailSecretHashPepper` password, but it does not take the other options on this page. To set them, switch to `EmailIdpConfigFromPasswords` and pass your own `sendOtpLoginVerificationCode`, `sendRegistrationVerificationCode`, and `sendPasswordResetVerificationCode` callbacks. See [Use your own email provider](./setup#use-your-own-email-provider). Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `EmailIdpConfig` in-code documentation. @@ -53,12 +53,38 @@ final emailIdpConfig = EmailIdpConfigFromPasswords( ``` :::tip -This is useful to ensure password policies on the server-side. It is a best practice to pair it with a configuration on the client-side to provide a better UX when creating a new password. The `EmailSignInWidget` and `EmailAuthController` have a `passwordRequirements` parameter that can be used to configure the password requirements. +This is useful to ensure password policies on the server-side. It is a best practice to pair it with a configuration on the client-side to provide a better UX when creating a new password. The email widgets and controllers have a `passwordRequirements` parameter that can be used to configure the password requirements. ::: +### Allowing a password on account creation + +By default, accounts created through passwordless sign-in have no password. To let users set one while creating their account, enable `allowPasswordOnOtpRegistration`: + +```dart +final emailIdpConfig = EmailIdpConfigFromPasswords( + allowPasswordOnOtpRegistration: true, +); +``` + +The account creation screen then shows an optional password field. To let users sign in with that password later, also set `allowPasswordSignIn: true` on the `EmailOtpSignInWidget`. See the [customizing the UI section](./customizing-the-ui#using-the-emailotpsigninwidget). + ### Custom verification code generation -You can customize how verification codes are generated: +One-time codes for passwordless sign-in are 6 digits by default, while registration and password reset codes are 8 digits. You can customize how each kind of code is generated: + +```dart +final emailIdpConfig = EmailIdpConfigFromPasswords( + // Use 8-digit codes for passwordless sign-in. + otpLoginVerificationCodeGenerator: () => + defaultVerificationCodeGenerator(length: 8), +); +``` + +:::warning +Remember to configure the `otpVerificationCodeConfig` parameter on the `EmailOtpSignInWidget` to match the length and allowed characters of the one-time code you generate. The app does not get the code format from the server, so if the two differ, users will never be able to enter the code correctly. See the [customizing the UI section](./customizing-the-ui#using-the-emailotpsigninwidget) for more details. +::: + +Registration codes are customized the same way: ```dart final emailIdpConfig = EmailIdpConfigFromPasswords( @@ -85,6 +111,9 @@ pod.initializeAuthServices( ], identityProviderBuilders: [ EmailIdpConfigFromPasswords( + otpLoginVerificationCodeGenerator: pod.runMode == 'development' + ? () => '111111' // Digits only, and the same length as production. + : defaultOtpVerificationCodeGenerator, registrationVerificationCodeGenerator: pod.runMode == 'development' ? () => '11111111' // Digits only, and the same length as production. : defaultVerificationCodeGenerator, @@ -95,7 +124,7 @@ pod.initializeAuthServices( ### Reacting to events -Beyond the `sendRegistrationVerificationCode` and `sendPasswordResetVerificationCode` callbacks to send verification codes to the user, there are a few callbacks that can be used to react to events, such as `onAfterAccountCreated` and `onPasswordResetCompleted`. These can be useful for sending emails to the user to communicate the successful operation. +Beyond the callbacks to send verification codes to the user, there are a few callbacks that can be used to react to events, such as `onAfterAccountCreated` and `onPasswordResetCompleted`. These can be useful for sending emails to the user to communicate the successful operation. ### Rate limiting @@ -103,12 +132,19 @@ The email provider includes built-in rate limiting to prevent abuse, with some s - **Failed login attempts**: Limited to 5 attempts per 5 minutes - **Password reset attempts**: Limited to 3 attempts per hour +- **One-time code requests**: Limited to 5 requests per email per 15 minutes, reset by a successful sign-in - **Verification code attempts**: Limited to 3 attempts per code lifetime of 15 minutes You can customize all rate limits in the `EmailIdpConfig`: ```dart final emailIdpConfig = EmailIdpConfigFromPasswords( + otpLoginVerificationCodeLifetime: Duration(minutes: 15), + otpLoginVerificationCodeAllowedAttempts: 3, + maxOtpLoginAttempts: RateLimit( + maxAttempts: 5, + timeframe: Duration(minutes: 15), + ), registrationVerificationCodeLifetime: Duration(minutes: 15), registrationVerificationCodeAllowedAttempts: 3, passwordResetVerificationCodeLifetime: Duration(minutes: 15), diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md index a5cb6426..f2ce2beb 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md @@ -1,22 +1,25 @@ --- sidebar_label: Customizing the UI -description: Email sign-in UI can be customized with the EmailSignInWidget and EmailAuthController to build a custom authentication flow. +description: Customize email sign-in with the EmailOtpSignInWidget and EmailSignInWidget, or build your own sign-in UI on top of their controllers. --- # Customize the email sign-in UI -When using the email identity provider, you can customize the UI to your liking. You can use the `EmailSignInWidget` to display the email authentication flow in your own custom UI, or you can use the `EmailAuthController` to build a completely custom authentication interface. +When using the email identity provider, you can customize the UI to your liking. Use a widget to display the email authentication flow in your own custom UI, or a controller to build a completely custom authentication interface. Each flow has its own widget and controller: + +- **Passwordless sign-in**: the `EmailOtpSignInWidget` and the `EmailOtpAuthController`, for endpoints that extend `EmailOtpIdpBaseEndpoint`, as in new projects. +- **Password sign-in**: the `EmailSignInWidget` and the `EmailAuthController`, for endpoints that extend `EmailIdpBaseEndpoint`. :::info -The `SignInWidget` uses the `EmailSignInWidget` internally to display the email authentication flow. You can also supply a custom `EmailSignInWidget` to the `SignInWidget` to override the default behavior. +The `SignInWidget` uses one of these widgets internally, depending on your endpoint. You can supply your own widget to the `SignInWidget` to override the default behavior, as either `emailOtpSignInWidget` or `emailSignInWidget`. ```dart SignInWidget( client: client, - emailSignInWidget: EmailSignInWidget( + emailOtpSignInWidget: EmailOtpSignInWidget( client: client, - // Open on the login screen instead of the default registration screen - startScreen: EmailFlowScreen.login, + // Also let users sign in with their password + allowPasswordSignIn: true, // A custom widget replaces the built-in handling, so pass your own callbacks. onAuthenticated: () { /* ... */ }, onError: (error) { /* ... */ }, @@ -25,6 +28,151 @@ SignInWidget( ``` ::: +## Using the `EmailOtpSignInWidget` + +The `EmailOtpSignInWidget` handles the complete passwordless flow: requesting and verifying the one-time code, signing in to an existing account, and creating an account for a new email. It can also offer password sign-in and password reset. + +You can customize the widget's behavior using its constructor parameters: + +```dart +EmailOtpSignInWidget( + client: client, + // Offer password sign-in and password reset on the start screen. + // Defaults to false. + allowPasswordSignIn: true, + // Customize the one-time code input. Must match the server's code format. + // The default matches the default server code generation: 6 digits. + otpVerificationCodeConfig: VerificationCodeConfig( + length: 6, + keyboardType: TextInputType.number, + allowedCharactersPattern: RegExp(r'[0-9]'), + ), + // Customize the password reset code input, used with allowPasswordSignIn. + // The default matches the server's 8-digit password reset codes. + verificationCodeConfig: VerificationCodeConfig( + resendCountdownDuration: Duration(minutes: 1), + ), + // Custom email validation function. Throw InvalidEmailException so the + // widget shows the message. Other exceptions block sign-in silently. + emailValidation: (email) { + if (!email.contains('@example.com')) { + throw const InvalidEmailException('Only @example.com emails allowed'); + } + }, + // Requirements for the password set on password reset, or on account + // creation if the server allows it. + passwordRequirements: [ + PasswordRequirement.minLength(12), + ], + onTermsAndConditionsPressed: () { + // Open terms and conditions + }, + onPrivacyPolicyPressed: () { + // Open privacy policy + }, + 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 + }, +) +``` + +If you change the code format on the server, update `otpVerificationCodeConfig` to match. See [custom verification code generation](./configuration#custom-verification-code-generation). + +By default, both email widgets accept email aliases, such as `name+alias@example.com`. To reject them, pass `emailValidation: (email) => EmailFlowController.validateEmail(email, allowEmailAliases: false)`. + +For a new email, the account creation screen shows the terms checkbox if you provide `onTermsAndConditionsPressed` or `onPrivacyPolicyPressed`. It also offers an optional password if the server enables [`allowPasswordOnOtpRegistration`](./configuration#allowing-a-password-on-account-creation). + +Optionally, you can provide an externally managed `EmailOtpAuthController` instance to the widget. A controller and a `client` are mutually exclusive, and `onAuthenticated` and `onError` belong on the controller in that case. The controller carries `allowPasswordSignIn`, `emailValidation`, and `passwordRequirements`, while the code configurations and the terms callbacks stay on the widget. + +```dart +EmailOtpSignInWidget( + controller: controller, +) +``` + +## Building a custom UI with the `EmailOtpAuthController` + +For more control over the UI, you can use the `EmailOtpAuthController` class, which provides all the logic of passwordless sign-in without any UI components. + +```dart +import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; + +final controller = EmailOtpAuthController( + client: client, + allowPasswordSignIn: true, + 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 + }, +); +``` + +It shares its state properties, text controllers, and navigation methods with the `EmailAuthController`. See [EmailAuthController state management](#emailauthcontroller-state-management) and [navigating between screens](#navigating-between-screens). Your widget should render the screen given by `currentScreen`, which is one of: + +- `EmailOtpFlowScreen.start` - Enter email to request a code, or sign in with a password +- `EmailOtpFlowScreen.verify` - Enter the one-time code +- `EmailOtpFlowScreen.completeRegistration` - Create an account for a new email +- `EmailOtpFlowScreen.requestPasswordReset` - Request password reset +- `EmailOtpFlowScreen.verifyPasswordReset` - Verify password reset code +- `EmailOtpFlowScreen.completePasswordReset` - Set new password + +### Passwordless sign-in flow + +The passwordless sign-in flow consists of up to three steps: + +1. **Request a code** - Send a one-time code to the email: + +```dart +controller.emailController.text = 'user@example.com'; +await controller.startOtpLogin(); +// Controller navigates to the verify screen +``` + +2. **Verify the code** - Sign in, or continue to account creation: + +```dart +controller.verificationCodeController.text = '123456'; +await controller.verifyOtpCode(); +// If an account exists, the user is now authenticated. +// Otherwise, the controller navigates to the completeRegistration screen. +``` + +3. **Create the account** - Only for a new email, once the user has accepted the terms: + +```dart +// Optional, only if the server allows a password on account creation. +if (controller.otpAllowPassword) { + controller.toggleRegistrationPassword(); + controller.passwordController.text = 'securePassword123'; +} +await controller.finishOtpRegistration(); +// User is now authenticated +``` + +If the code is accepted but completing the sign-in fails, for example because of a network error, call `verifyOtpCode()` again. It retries the sign-in without checking the code a second time. If the server rejects the retry, the controller's `error` is an `OtpLoginRejectedException`, `onError` receives a message asking for a new code, and the user needs to request one with `resendVerificationCode()`. + +### Password sign-in + +With `allowPasswordSignIn` set to `true`, call `togglePasswordSignIn()` to show the password field on the start screen, then `login()` to sign in. Password reset works the same as with the [`EmailAuthController`](#password-reset-flow). + +```dart +controller.togglePasswordSignIn(); +controller.emailController.text = 'user@example.com'; +controller.passwordController.text = 'password123'; +await controller.login(); +``` + ## Using the `EmailSignInWidget` The `EmailSignInWidget` handles the complete email authentication flow including login, registration, email verification, and password reset. @@ -92,7 +240,7 @@ The terms and conditions and privacy policy checkbox on the registration screen ### Customizing the default widget's appearance -Since the `EmailSignInWidget` uses the Material Design system, it will react to your app's Material theme. You can also wrap it in a `Theme` widget to apply a custom theme. +Since the `EmailSignInWidget` and the `EmailOtpSignInWidget` use the Material Design system, they will react to your app's Material theme. You can also wrap it in a `Theme` widget to apply a custom theme. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md b/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md index 050dc891..f326d136 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md @@ -94,7 +94,7 @@ This is useful for checking the status of a registration request or verifying if ## Cleanup operations -Over time, expired account requests, password reset requests, and failed login attempts can accumulate in the database, leading to database bloat and potential performance issues. It's important to periodically clean these up to prevent database bloat. Such requests are not automatically cleaned up since they can be useful for auditing purposes, so it is up to each application to decide when to clean them up. +Over time, expired account requests, password reset requests, one-time code requests, and failed login attempts can accumulate in the database, leading to database bloat and potential performance issues. It's important to periodically clean these up to prevent database bloat. Such requests are not automatically cleaned up since they can be useful for auditing purposes, so it is up to each application to decide when to clean them up. ### Cleaning up expired account requests @@ -128,6 +128,27 @@ await admin.deletePasswordResetRequestsAttemptsForEmail( ); ``` +### Cleaning up one-time code requests + +Each code sent for passwordless sign-in creates a request, which expires after the code lifetime, and an entry in the log used for rate limiting. Both should be cleaned up: + +```dart +// Delete all expired one-time code requests +await admin.deleteExpiredOtpLoginRequests(session); + +// Delete the logged requests outside the rate limit window +await admin.deleteOtpLoginAttempts(session); + +// Delete all logged requests for a specific email +// Useful when you want to allow a user to request a new code +// even though they have hit the rate limit +await admin.deleteOtpLoginAttempts( + session, + email: 'user@example.com', + olderThan: Duration.zero, +); +``` + ### Cleaning up failed login attempts Failed login attempts, tracked for rate limiting, should also be cleaned up when no longer useful for auditing purposes: diff --git a/docs/06-concepts/04-authentication/07-ui-components.md b/docs/06-concepts/04-authentication/07-ui-components.md index 40bc3d44..31691038 100644 --- a/docs/06-concepts/04-authentication/07-ui-components.md +++ b/docs/06-concepts/04-authentication/07-ui-components.md @@ -52,9 +52,9 @@ The `SignInWidget` builds a default widget for each identity provider it detects ```dart final signInWidget = SignInWidget( client: client, - emailSignInWidget: EmailSignInWidget( + emailOtpSignInWidget: EmailOtpSignInWidget( client: client, - startScreen: EmailFlowScreen.login, + allowPasswordSignIn: true, // NOTE: When you opt-out of the internal widget, you need to provide your // own `onAuthenticated` and `onError` callbacks. onAuthenticated: _onAuthenticated, @@ -84,6 +84,8 @@ void _onError(Object error) { } ``` +For email, pass the widget that matches your server endpoint: `emailOtpSignInWidget` for `EmailOtpIdpBaseEndpoint`, as in new projects, or `emailSignInWidget` for `EmailIdpBaseEndpoint`. See [Set up email sign-in](./providers/email/setup). + ### Styling the buttons Inside `SignInWidget`, every provider button shares one neutral, theme-aware appearance. To change it, pass a `SignInButtonStyle` as `buttonStyle`: @@ -109,7 +111,9 @@ For all options of each provider widget, see the provider's customizations page, All text shown by the authentication widgets is in English by default. To replace it, wrap `SignInWidget`, or any individual provider widget, in a `SignInLocalizationProvider` and pass the text objects for the parts you want to translate: - `BasicSignInTexts` for the shared messages and dividers. -- `EmailSignInTexts` for the email flow: sign-in, registration, account verification, password reset, and the terms and privacy labels. +- `EmailSignInTexts` for the email flow: sign-in, registration, account verification, password reset, and the terms and privacy labels. Passwordless sign-in uses the same texts for the email field, password reset, and terms. +- `EmailWithOtpSignInTexts` for the passwordless sign-in screens: requesting and verifying the code, and creating an account. +- `VerificationCodeTexts` for the button that resends a code, in both email flows. - `PasswordRequirementTexts` for the labels of the built-in password requirement widgets. - One text object per provider for the sign-in buttons: `AppleSignInTexts`, `GoogleSignInTexts`, `GitHubSignInTexts`, `MicrosoftSignInTexts`, `FacebookSignInTexts`, and `AnonymousSignInTexts`. @@ -158,6 +162,27 @@ const emailTexts = EmailSignInTexts( privacyPolicy: 'Privacy Policy', ); +const emailWithOtpTexts = EmailWithOtpSignInTexts( + startTitle: 'Sign In with email', + sendCode: 'Send code', + signInWithPassword: 'Sign in with password', + signInWithCode: 'Sign in with a code', + verifyTitle: 'Check your email', + verificationMessage: + 'A verification email has been sent. Please check your email and ' + 'enter the verification code below.', + verifyAction: 'Continue', + createAccountTitle: 'Create your account', + addPassword: 'Add a password (optional)', + removePassword: 'Continue without a password', + passwordLabel: 'Password', + createAccount: 'Create account', +); + +const verificationCodeTexts = VerificationCodeTexts( + resendCode: "Didn't receive? Send a new code", +); + const passwordTexts = PasswordRequirementTexts( minLengthTemplate: 'At least {length} characters', maxLengthTemplate: 'At most {length} characters', @@ -170,6 +195,8 @@ const passwordTexts = PasswordRequirementTexts( SignInLocalizationProvider( basic: basicTexts, email: emailTexts, + emailWithOtp: emailWithOtpTexts, + verificationCodeTexts: verificationCodeTexts, passwordRequirementTexts: passwordTexts, apple: const AppleSignInTexts(signInButton: 'Sign in with Apple'), google: const GoogleSignInTexts(signInButton: 'Sign in with Google'), diff --git a/docs/11-upgrading/03-migrate-from-legacy-auth.md b/docs/11-upgrading/03-migrate-from-legacy-auth.md index 31c0fbe3..aa558e69 100644 --- a/docs/11-upgrading/03-migrate-from-legacy-auth.md +++ b/docs/11-upgrading/03-migrate-from-legacy-auth.md @@ -148,7 +148,7 @@ class PasswordImportingEmailIdpEndpoint extends EmailIdpBaseEndpoint { } ``` -If your project already has an endpoint that extends `EmailIdpBaseEndpoint`, such as `lib/src/auth/email_idp_endpoint.dart` from the project template, delete it. With two such endpoints, the Flutter email sign-in UI throws `ServerpodClientMultipleEndpointsFound`. +If your project already has an endpoint that extends `EmailIdpBaseEndpoint` or `EmailOtpIdpBaseEndpoint`, such as `lib/src/auth/email_idp_endpoint.dart` from the project template, delete it. With two such endpoints, the Flutter email sign-in UI throws `ServerpodClientMultipleEndpointsFound`. Add the required entries to `_server/config/passwords.yaml` under the `development:` section (use distinct values per environment): From 3f0e385eedb8f93dadc1a4809b41c3024a1d2bd5 Mon Sep 17 00:00:00 2001 From: Alexander Sandor Date: Fri, 23 Oct 2026 08:39:19 +0200 Subject: [PATCH 2/4] docs: Present code sign-in as the default email sign-in Email sign-in with EmailOtpIdpBaseEndpoint handles both one-time code and password sign-in, so the docs now present it as the email sign-in rather than as one of two flows. Password-only sign-in with EmailIdpBaseEndpoint, the EmailSignInWidget, and the EmailAuthController moves to sections at the bottom of the setup, configuration, and customizing the UI pages. sendOtpLoginVerificationCode is listed last as optional, with its fallback to sendRegistrationVerificationCode. Co-Authored-By: Claude Opus 5.5 --- .../04-authentication/01-get-started.md | 2 +- .../05-providers/02-email/01-setup.md | 85 +++--- .../05-providers/02-email/02-configuration.md | 45 ++-- .../02-email/03-customizing-the-ui.md | 249 +++++++++--------- .../02-email/04-admin-operations.md | 2 +- .../04-authentication/07-ui-components.md | 8 +- 6 files changed, 192 insertions(+), 199 deletions(-) diff --git a/docs/06-concepts/04-authentication/01-get-started.md b/docs/06-concepts/04-authentication/01-get-started.md index 97b79d95..af152527 100644 --- a/docs/06-concepts/04-authentication/01-get-started.md +++ b/docs/06-concepts/04-authentication/01-get-started.md @@ -70,7 +70,7 @@ The app opens on the sign-in screen. To create an account: Once you are signed in, the app shows your content with a sign-out button. The next time you sign in with the same email, entering the code takes you straight to your account. :::note -Projects created before Serverpod 4.1 use password sign-in, so you set a password when you create the account. To switch, see [Switch an existing project to passwordless sign-in](./providers/email/setup#switch-an-existing-project-to-passwordless-sign-in). +Projects created before Serverpod 4.1 use password sign-in, so you set a password when you create the account. To switch, see [Switch from password-only sign-in](./providers/email/setup#switch-from-password-only-sign-in). ::: ## Next steps diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md b/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md index bc49c518..fb619dd1 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md @@ -5,12 +5,12 @@ description: Sign in with Email lets users authenticate with a one-time code or # Set up email sign-in -Sign in with Email supports two flows: +Sign in with Email offers two ways to sign in: -- **Passwordless sign-in.** The user enters their email and receives a one-time code. Entering the code signs them in, or creates an account if there is none for the email yet. New projects use this flow. -- **Password sign-in.** The user registers with an email and a password, and signs in with them. A code sent by email verifies the address on registration and on password reset. +- **One-time code.** The user enters their email and the code sent to it. A new email gets an account. +- **Password.** The user enters their email and password. This is off by default. -Both flows share the same accounts, and passwordless sign-in can optionally let users sign in with a password too. +Both work for the same account. :::caution You need to install the auth module before you continue, see [Setup](../../setup). @@ -51,25 +51,8 @@ import 'package:serverpod_auth_idp_server/providers/email.dart'; class EmailIdpEndpoint extends EmailOtpIdpBaseEndpoint {} ``` -For password sign-in only, extend `EmailIdpBaseEndpoint` instead. - Then start the server with `serverpod start` to generate the client code, and create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration). -#### Switch an existing project to passwordless sign-in - -Projects created before Serverpod 4.1 extend `EmailIdpBaseEndpoint` and use password sign-in. To switch to passwordless sign-in: - -1. In your email endpoint file, change the base class to `EmailOtpIdpBaseEndpoint`: - - ```dart - class EmailIdpEndpoint extends EmailOtpIdpBaseEndpoint {} - ``` - -2. Start the server with `serverpod start` to regenerate the client code, then create and apply the migration (in the `serverpod start` terminal, press **M**). -3. If you use [your own email provider](#use-your-own-email-provider), add a `sendOtpLoginVerificationCode` callback. - -Existing accounts keep working, and users who registered with a password can sign in with a code sent to the same email. To keep password sign-in available in the app as well, set `allowPasswordSignIn: true` on the widget, as shown in [client-side configuration](#client-side-configuration). - ### Use your own email provider Serverpod Cloud delivery is there to get sign-in working quickly, and it sends a standard message carrying your `appDisplayName`. You might prefer using a custom email provider to have full control over the body, layout, and language of the emails. For servers hosted outside of Serverpod Cloud, it is the only option. @@ -93,12 +76,13 @@ void run(List args) async { // Configure the Email Identity Provider // This is the basic configuration for the Email IDP to work. EmailIdpConfigFromPasswords( - // Callback to send the one-time sign-in code to the user. - sendOtpLoginVerificationCode: _sendOtpLoginCode, // Callback to send the registration verification code to the user. sendRegistrationVerificationCode: _sendRegistrationCode, // Callback to send the password reset verification code to the user. sendPasswordResetVerificationCode: _sendPasswordResetCode, + // Optional callback to send the one-time sign-in code to the user. + // Defaults to sendRegistrationVerificationCode if not set. + sendOtpLoginVerificationCode: _sendOtpLoginCode, ), ], ); @@ -106,18 +90,6 @@ void run(List args) async { await pod.start(); } -void _sendOtpLoginCode( - Session session, { - required String email, - required UuidValue otpLoginRequestId, - required String verificationCode, - required Transaction? transaction, -}) { - // NOTE: Here you call your mail service to send the sign-in code to the - // user. For testing, we will log the code. - session.log('[EmailIDP] Login code ($email): $verificationCode'); -} - void _sendRegistrationCode( Session session, { required String email, @@ -141,17 +113,25 @@ void _sendPasswordResetCode( // the user. For testing, we will log the verification code. session.log('[EmailIDP] Password reset code ($email): $verificationCode'); } + +void _sendOtpLoginCode( + Session session, { + required String email, + required UuidValue otpLoginRequestId, + required String verificationCode, + required Transaction? transaction, +}) { + // NOTE: Here you call your mail service to send the sign-in code to the + // user. For testing, we will log the code. + session.log('[EmailIDP] Login code ($email): $verificationCode'); +} ``` #### Basic configuration options -- `sendOtpLoginVerificationCode`: A callback that will be called to send the one-time code for passwordless sign-in. It is called whether or not the email already has an account, so write the email to work for both signing in and creating an account. - `sendRegistrationVerificationCode`: A callback that will be called to send the registration verification code to the user. Here you should call the email sending service to send the verification code to the user. - `sendPasswordResetVerificationCode`: A callback that will be called to send the password reset verification code to the user. Here you should call the email sending service to send the verification code to the user. - -:::warning -If you don't set `sendOtpLoginVerificationCode`, sign-in codes are sent through `sendRegistrationVerificationCode` instead, and a warning is logged the first time this happens. That callback then receives the ID of the sign-in request as its `accountRequestId`, so any links built from it, such as a link to complete the registration, won't work. Set `sendOtpLoginVerificationCode` if your registration emails contain more than the code. If neither callback is set, requesting a code fails with a `StateError`. -::: +- `sendOtpLoginVerificationCode`: An optional callback that will be called to send the one-time sign-in code. If it is not set, the code is sent with `sendRegistrationVerificationCode`. Set it to send a different email for signing in than for registering. A user signing in with a new email also receives this email, since the code is sent before the account is created. Both configurations hash passwords and verification codes with a secret pepper, which `EmailIdpConfigFromPasswords` reads from the `emailSecretHashPepper` key in `config/passwords.yaml` or the `SERVERPOD_PASSWORD_emailSecretHashPepper` environment variable. Newly generated projects already have a value for every run mode. To pass the pepper directly instead, use `EmailIdpConfig` with its `secretHashPepper` parameter. @@ -159,9 +139,9 @@ For more details on configuration options, such as customizing password requirem ## Client-side configuration -If you have configured the `SignInWidget` as described in the [setup section](../../setup#present-the-authentication-ui), the Email identity provider will be automatically detected and displayed in the sign-in widget. It shows passwordless sign-in when your endpoint extends `EmailOtpIdpBaseEndpoint`, and password sign-in when it extends `EmailIdpBaseEndpoint`. +If you have configured the `SignInWidget` as described in the [setup section](../../setup#present-the-authentication-ui), the Email identity provider will be automatically detected and displayed in the sign-in widget. -You can also use the `EmailOtpSignInWidget` to include passwordless sign-in in your own custom UI. +You can also use the `EmailOtpSignInWidget` to include email sign-in in your own custom UI. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; @@ -193,6 +173,23 @@ The widget automatically handles: - Password sign-in and password reset, if `allowPasswordSignIn` is `true`. - Navigation between screens. -For password sign-in, use the `EmailSignInWidget` with the same `client`, `onAuthenticated`, and `onError` parameters. It handles login with email and password, registration with email verification, and password reset. - For details on how to display the email authentication UI in your Flutter app, see the [customizing the UI section](./customizing-the-ui). + +## Switch from password-only sign-in + +Projects created before Serverpod 4.1 extend `EmailIdpBaseEndpoint`, which supports password sign-in only. For these projects, the `SignInWidget` shows the `EmailSignInWidget`, where users register with an email and a password. To switch to the `EmailOtpIdpBaseEndpoint`: + +1. In your email endpoint file, change the base class to `EmailOtpIdpBaseEndpoint`: + + ```dart + class EmailIdpEndpoint extends EmailOtpIdpBaseEndpoint {} + ``` + +2. Start the server with `serverpod start` to regenerate the client code, then create and apply the migration (in the `serverpod start` terminal, press **M**). +3. If you pass your own `EmailSignInWidget` to the `SignInWidget` as `emailSignInWidget`, replace it with an `EmailOtpSignInWidget` passed as `emailOtpSignInWidget`. + +Existing accounts keep working, and users can sign in with a code sent to the email they registered with. To let them keep signing in with their password, set `allowPasswordSignIn: true` on the widget, as shown in [client-side configuration](#client-side-configuration). + +:::warning +If you use [your own email provider](#use-your-own-email-provider) and don't set `sendOtpLoginVerificationCode`, sign-in codes are sent with `sendRegistrationVerificationCode`, and a warning is logged the first time this happens. That callback then receives the ID of the sign-in request as its `accountRequestId`, so any links built from it, such as a link to complete the registration, won't work. Set `sendOtpLoginVerificationCode` if your registration emails contain more than the code. +::: diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md b/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md index dff2e7a1..d8503876 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md @@ -9,7 +9,7 @@ This page covers configuration options for the email identity provider beyond th ## Configuration options -New projects use `ServerpodCloudEmailIdpConfig`. It reads the [pepper](#peppering) from the `emailSecretHashPepper` password, but it does not take the other options on this page. To set them, switch to `EmailIdpConfigFromPasswords` and pass your own `sendOtpLoginVerificationCode`, `sendRegistrationVerificationCode`, and `sendPasswordResetVerificationCode` callbacks. See [Use your own email provider](./setup#use-your-own-email-provider). +New projects use `ServerpodCloudEmailIdpConfig`. It reads the [pepper](#peppering) from the `emailSecretHashPepper` password, but it does not take the other options on this page. To set them, switch to `EmailIdpConfigFromPasswords` and pass your own `sendRegistrationVerificationCode` and `sendPasswordResetVerificationCode` callbacks, and optionally `sendOtpLoginVerificationCode`. See [Use your own email provider](./setup#use-your-own-email-provider). Below is a non-exhaustive list of some of the most common configuration options. For more details on all options, check the `EmailIdpConfig` in-code documentation. @@ -58,7 +58,7 @@ This is useful to ensure password policies on the server-side. It is a best prac ### Allowing a password on account creation -By default, accounts created through passwordless sign-in have no password. To let users set one while creating their account, enable `allowPasswordOnOtpRegistration`: +By default, accounts created with a one-time code have no password. To let users set one while creating their account, enable `allowPasswordOnOtpRegistration`: ```dart final emailIdpConfig = EmailIdpConfigFromPasswords( @@ -70,11 +70,11 @@ The account creation screen then shows an optional password field. To let users ### Custom verification code generation -One-time codes for passwordless sign-in are 6 digits by default, while registration and password reset codes are 8 digits. You can customize how each kind of code is generated: +One-time sign-in codes are 6 digits by default, while password reset codes are 8 digits. You can customize how each kind of code is generated: ```dart final emailIdpConfig = EmailIdpConfigFromPasswords( - // Use 8-digit codes for passwordless sign-in. + // Use 8-digit codes for sign-in. otpLoginVerificationCodeGenerator: () => defaultVerificationCodeGenerator(length: 8), ); @@ -84,22 +84,6 @@ final emailIdpConfig = EmailIdpConfigFromPasswords( Remember to configure the `otpVerificationCodeConfig` parameter on the `EmailOtpSignInWidget` to match the length and allowed characters of the one-time code you generate. The app does not get the code format from the server, so if the two differ, users will never be able to enter the code correctly. See the [customizing the UI section](./customizing-the-ui#using-the-emailotpsigninwidget) for more details. ::: -Registration codes are customized the same way: - -```dart -final emailIdpConfig = EmailIdpConfigFromPasswords( - registrationVerificationCodeGenerator: () { - // Generate a 6-digit numeric code - final random = Random.secure(); - return List.generate(6, (_) => random.nextInt(10)).join(); - }, -); -``` - -:::warning -Remember to configure the `verificationCodeConfig` parameter on the `EmailSignInWidget` to match the length and allowed characters of the verification code you generate. Otherwise, users will never be able to enter the verification code correctly. See the [customizing the UI section](./customizing-the-ui) for more details. -::: - #### Bypassing verification code in development To simplify testing, it is possible to bypass the verification code by using a custom verification code generator that always returns the same code. This can be configured only for the development run mode as below: @@ -114,9 +98,6 @@ pod.initializeAuthServices( otpLoginVerificationCodeGenerator: pod.runMode == 'development' ? () => '111111' // Digits only, and the same length as production. : defaultOtpVerificationCodeGenerator, - registrationVerificationCodeGenerator: pod.runMode == 'development' - ? () => '11111111' // Digits only, and the same length as production. - : defaultVerificationCodeGenerator, ), ], ); @@ -159,3 +140,21 @@ final emailIdpConfig = EmailIdpConfigFromPasswords( ), ); ``` + +## Password-only sign-in + +Projects that use the `EmailSignInWidget` for password-only sign-in, as described in [Switch from password-only sign-in](./setup#switch-from-password-only-sign-in), verify new accounts with a registration code. Registration codes are 8 digits by default, and are customized with `registrationVerificationCodeGenerator`: + +```dart +final emailIdpConfig = EmailIdpConfigFromPasswords( + registrationVerificationCodeGenerator: () { + // Generate a 6-digit numeric code + final random = Random.secure(); + return List.generate(6, (_) => random.nextInt(10)).join(); + }, +); +``` + +:::warning +Remember to configure the `verificationCodeConfig` parameter on the `EmailSignInWidget` to match the length and allowed characters of the verification code you generate. Otherwise, users will never be able to enter the verification code correctly. See the [customizing the UI section](./customizing-the-ui#using-the-emailsigninwidget) for more details. +::: diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md index f2ce2beb..19dd04d4 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md @@ -1,17 +1,14 @@ --- sidebar_label: Customizing the UI -description: Customize email sign-in with the EmailOtpSignInWidget and EmailSignInWidget, or build your own sign-in UI on top of their controllers. +description: Customize email sign-in with the EmailOtpSignInWidget, or build your own sign-in UI on top of the EmailOtpAuthController that drives it. --- # Customize the email sign-in UI -When using the email identity provider, you can customize the UI to your liking. Use a widget to display the email authentication flow in your own custom UI, or a controller to build a completely custom authentication interface. Each flow has its own widget and controller: - -- **Passwordless sign-in**: the `EmailOtpSignInWidget` and the `EmailOtpAuthController`, for endpoints that extend `EmailOtpIdpBaseEndpoint`, as in new projects. -- **Password sign-in**: the `EmailSignInWidget` and the `EmailAuthController`, for endpoints that extend `EmailIdpBaseEndpoint`. +When using the email identity provider, you can customize the UI to your liking. You can use the `EmailOtpSignInWidget` to display email sign-in in your own custom UI, or you can use the `EmailOtpAuthController` to build a completely custom authentication interface. Both handle sign-in with a one-time code and, optionally, with a password. :::info -The `SignInWidget` uses one of these widgets internally, depending on your endpoint. You can supply your own widget to the `SignInWidget` to override the default behavior, as either `emailOtpSignInWidget` or `emailSignInWidget`. +The `SignInWidget` uses the `EmailOtpSignInWidget` internally to display email sign-in. You can also supply a custom `EmailOtpSignInWidget` to the `SignInWidget` to override the default behavior. ```dart SignInWidget( @@ -30,7 +27,7 @@ SignInWidget( ## Using the `EmailOtpSignInWidget` -The `EmailOtpSignInWidget` handles the complete passwordless flow: requesting and verifying the one-time code, signing in to an existing account, and creating an account for a new email. It can also offer password sign-in and password reset. +The `EmailOtpSignInWidget` handles the complete email sign-in flow: requesting and verifying the one-time code, signing in to an existing account, and creating an account for a new email. It can also offer password sign-in and password reset. You can customize the widget's behavior using its constructor parameters: @@ -84,7 +81,7 @@ EmailOtpSignInWidget( If you change the code format on the server, update `otpVerificationCodeConfig` to match. See [custom verification code generation](./configuration#custom-verification-code-generation). -By default, both email widgets accept email aliases, such as `name+alias@example.com`. To reject them, pass `emailValidation: (email) => EmailFlowController.validateEmail(email, allowEmailAliases: false)`. +By default, the widget accepts email aliases, such as `name+alias@example.com`. To reject them, pass `emailValidation: (email) => EmailFlowController.validateEmail(email, allowEmailAliases: false)`. For a new email, the account creation screen shows the terms checkbox if you provide `onTermsAndConditionsPressed` or `onPrivacyPolicyPressed`. It also offers an optional password if the server enables [`allowPasswordOnOtpRegistration`](./configuration#allowing-a-password-on-account-creation). @@ -96,9 +93,30 @@ EmailOtpSignInWidget( ) ``` +### Customizing the default widget's appearance + +Since the `EmailOtpSignInWidget` uses the Material Design system, it will react to your app's Material theme. You can also wrap it in a `Theme` widget to apply a custom theme. + +```dart +import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; + +Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + // Use the AuthIdpTheme to customize the verification code input look + extensions: >[ + AuthIdpTheme.defaultTheme( + errorPinTheme: PinTheme(...), + ), + ], + ), + child: EmailOtpSignInWidget(client: client), +) +``` + ## Building a custom UI with the `EmailOtpAuthController` -For more control over the UI, you can use the `EmailOtpAuthController` class, which provides all the logic of passwordless sign-in without any UI components. +For more control over the UI, you can use the `EmailOtpAuthController` 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'; @@ -118,7 +136,32 @@ final controller = EmailOtpAuthController( ); ``` -It shares its state properties, text controllers, and navigation methods with the `EmailAuthController`. See [EmailAuthController state management](#emailauthcontroller-state-management) and [navigating between screens](#navigating-between-screens). Your widget should render the screen given by `currentScreen`, which is one of: +### State management + +Your widget should render the appropriate screen based on the `currentScreen` property of the controller. You can also use the below state properties to build your UI: + +```dart +// Check current screen +final currentScreen = controller.currentScreen; + +// 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 + }); +}); +``` + +The email sign-in flow uses the following screens: - `EmailOtpFlowScreen.start` - Enter email to request a code, or sign in with a password - `EmailOtpFlowScreen.verify` - Enter the one-time code @@ -127,9 +170,27 @@ It shares its state properties, text controllers, and navigation methods with th - `EmailOtpFlowScreen.verifyPasswordReset` - Verify password reset code - `EmailOtpFlowScreen.completePasswordReset` - Set new password -### Passwordless sign-in flow +### Navigating between screens + +The controller handles navigation between screens automatically when calling the methods associated with each screen, but you can also navigate manually: + +```dart +// Navigate to a specific screen +controller.navigateTo(EmailOtpFlowScreen.requestPasswordReset); + +// Navigate back +if (controller.canNavigateBack) { + controller.navigateBack(); +} +``` -The passwordless sign-in flow consists of up to three steps: +:::info +The `EmailOtpAuthController` already exposes internal text controllers for the email, password, and verification code inputs. You can use these to build your own custom UI. All controllers are shared between the sign-in, account creation, and password reset flows for good UX. The `EmailOtpAuthController` will handle the cleanup of the text controllers upon navigation as needed. +::: + +### Sign in with a one-time code + +The sign-in flow consists of up to three steps: 1. **Request a code** - Send a one-time code to the email: @@ -162,9 +223,9 @@ await controller.finishOtpRegistration(); If the code is accepted but completing the sign-in fails, for example because of a network error, call `verifyOtpCode()` again. It retries the sign-in without checking the code a second time. If the server rejects the retry, the controller's `error` is an `OtpLoginRejectedException`, `onError` receives a message asking for a new code, and the user needs to request one with `resendVerificationCode()`. -### Password sign-in +### Sign in with a password -With `allowPasswordSignIn` set to `true`, call `togglePasswordSignIn()` to show the password field on the start screen, then `login()` to sign in. Password reset works the same as with the [`EmailAuthController`](#password-reset-flow). +With `allowPasswordSignIn` set to `true`, call `togglePasswordSignIn()` to show the password field on the start screen, then `login()` to sign in. ```dart controller.togglePasswordSignIn(); @@ -173,7 +234,51 @@ controller.passwordController.text = 'password123'; await controller.login(); ``` -## Using the `EmailSignInWidget` +### Password reset flow + +The password reset flow consists of three steps: + +1. **Request Password Reset**: + +```dart +controller.emailController.text = 'user@example.com'; +await controller.startPasswordReset(); +// Controller navigates to verification screen +``` + +2. **Verify Reset Code**: + +```dart +controller.verificationCodeController.text = '12345678'; +await controller.verifyPasswordResetCode(); +// Controller navigates to new password screen +``` + +3. **Complete Password Reset**: + +```dart +controller.passwordController.text = 'newSecurePassword123'; +await controller.finishPasswordReset(); +// User is authenticated with new password +``` + +### Resending verification codes + +To resend a verification code: + +```dart +await controller.resendVerificationCode(); +``` + +This will resend the code based on the current screen (sign-in or password reset). + +## Password-only sign-in + +Projects that extend `EmailIdpBaseEndpoint`, such as those created before Serverpod 4.1, use the `EmailSignInWidget` and the `EmailAuthController` instead. They support sign-in with a password only, and users register with an email and a password. To move to the `EmailOtpSignInWidget`, see [Switch from password-only sign-in](./setup#switch-from-password-only-sign-in). + +To override the default widget, pass your own `EmailSignInWidget` to the `SignInWidget` as `emailSignInWidget`. + +### Using the `EmailSignInWidget` The `EmailSignInWidget` handles the complete email authentication flow including login, registration, email verification, and password reset. @@ -238,30 +343,9 @@ EmailSignInWidget( The terms and conditions and privacy policy checkbox on the registration screen are optional and disabled by default. The checkbox is shown as soon as you provide either `onTermsAndConditionsPressed` or `onPrivacyPolicyPressed`. ::: -### Customizing the default widget's appearance +### Building a custom UI with the `EmailAuthController` -Since the `EmailSignInWidget` and the `EmailOtpSignInWidget` use the Material Design system, they will react to your app's Material theme. You can also wrap it in a `Theme` widget to apply a custom theme. - -```dart -import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; - -Theme( - data: Theme.of(context).copyWith( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), - // Use the AuthIdpTheme to customize the verification code input look - extensions: >[ - AuthIdpTheme.defaultTheme( - errorPinTheme: PinTheme(...), - ), - ], - ), - child: EmailSignInWidget(client: client), -) -``` - -## Building a custom UI with the `EmailAuthController` - -For more control over the UI, you can use the `EmailAuthController` class, which provides all the authentication logic without any UI components. This allows you to build a completely custom authentication interface. +The `EmailAuthController` provides the same [state properties](#state-management), [navigation](#navigating-between-screens), [password reset](#password-reset-flow), and [code resending](#resending-verification-codes) as the `EmailOtpAuthController`, with its own screens and methods for login and registration. ```dart import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart'; @@ -281,34 +365,7 @@ final controller = EmailAuthController( ); ``` -### EmailAuthController state management - -Your widget should render the appropriate screen based on the `currentScreen` property of the controller. You can also use the below state properties to build your UI: - -```dart -// Check current screen -final currentScreen = controller.currentScreen; - -// 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 - }); -}); -``` - -## The email authentication flow - -The email authentication flow uses the following screens: +The controller uses the following screens: - `EmailFlowScreen.login` - Login with email and password - `EmailFlowScreen.startRegistration` - Start registration with email @@ -318,28 +375,6 @@ The email authentication flow uses the following screens: - `EmailFlowScreen.verifyPasswordReset` - Verify password reset code - `EmailFlowScreen.completePasswordReset` - Set new password -### Navigating between screens - -The controller handles navigation between screens automatically when calling the methods associated with each screen, but you can also navigate manually: - -```dart -// Navigate to a specific screen -controller.navigateTo(EmailFlowScreen.startRegistration); - -// Navigate back -if (controller.canNavigateBack) { - controller.navigateBack(); -} -``` - -### EmailAuthController methods - -The controller provides methods for each step of the authentication flow: - -:::info -The `EmailAuthController` already exposes internal text controllers for the email, password and verification code inputs. You can use these to build your own custom UI. All controllers are shared between login, registration and password reset flows for good UX. The `EmailAuthController` will handle the cleanup of the text controllers upon navigation as needed. -::: - #### Login ```dart @@ -378,41 +413,3 @@ controller.passwordController.text = 'securePassword123'; await controller.finishRegistration(); // User is now authenticated ``` - -#### Password reset flow - -The password reset flow also consists of three steps: - -1. **Request Password Reset**: - -```dart -controller.emailController.text = 'user@example.com'; -await controller.startPasswordReset(); -// Controller navigates to verification screen -``` - -2. **Verify Reset Code**: - -```dart -controller.verificationCodeController.text = '12345678'; -await controller.verifyPasswordResetCode(); -// Controller navigates to new password screen -``` - -3. **Complete Password Reset**: - -```dart -controller.passwordController.text = 'newSecurePassword123'; -await controller.finishPasswordReset(); -// User is authenticated with new password -``` - -### Resending verification codes - -To resend a verification code: - -```dart -await controller.resendVerificationCode(); -``` - -This will resend the code based on the current screen (registration or password reset). diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md b/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md index f326d136..8b87bafd 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/04-admin-operations.md @@ -130,7 +130,7 @@ await admin.deletePasswordResetRequestsAttemptsForEmail( ### Cleaning up one-time code requests -Each code sent for passwordless sign-in creates a request, which expires after the code lifetime, and an entry in the log used for rate limiting. Both should be cleaned up: +Each one-time sign-in code creates a request, which expires after the code lifetime, and an entry in the log used for rate limiting. Both should be cleaned up: ```dart // Delete all expired one-time code requests diff --git a/docs/06-concepts/04-authentication/07-ui-components.md b/docs/06-concepts/04-authentication/07-ui-components.md index 31691038..de87e3ad 100644 --- a/docs/06-concepts/04-authentication/07-ui-components.md +++ b/docs/06-concepts/04-authentication/07-ui-components.md @@ -84,7 +84,7 @@ void _onError(Object error) { } ``` -For email, pass the widget that matches your server endpoint: `emailOtpSignInWidget` for `EmailOtpIdpBaseEndpoint`, as in new projects, or `emailSignInWidget` for `EmailIdpBaseEndpoint`. See [Set up email sign-in](./providers/email/setup). +Projects that still use password-only email sign-in pass an `EmailSignInWidget` as `emailSignInWidget` instead. See [Switch from password-only sign-in](./providers/email/setup#switch-from-password-only-sign-in). ### Styling the buttons @@ -111,9 +111,9 @@ For all options of each provider widget, see the provider's customizations page, All text shown by the authentication widgets is in English by default. To replace it, wrap `SignInWidget`, or any individual provider widget, in a `SignInLocalizationProvider` and pass the text objects for the parts you want to translate: - `BasicSignInTexts` for the shared messages and dividers. -- `EmailSignInTexts` for the email flow: sign-in, registration, account verification, password reset, and the terms and privacy labels. Passwordless sign-in uses the same texts for the email field, password reset, and terms. -- `EmailWithOtpSignInTexts` for the passwordless sign-in screens: requesting and verifying the code, and creating an account. -- `VerificationCodeTexts` for the button that resends a code, in both email flows. +- `EmailWithOtpSignInTexts` for the email sign-in screens: requesting and verifying the code, and creating an account. +- `EmailSignInTexts` for the email field, password reset, and the terms and privacy labels, and for the screens of password-only sign-in. +- `VerificationCodeTexts` for the button that resends a code. - `PasswordRequirementTexts` for the labels of the built-in password requirement widgets. - One text object per provider for the sign-in buttons: `AppleSignInTexts`, `GoogleSignInTexts`, `GitHubSignInTexts`, `MicrosoftSignInTexts`, `FacebookSignInTexts`, and `AnonymousSignInTexts`. From f582480917a9810e7b97cedfd21ad82f8f0426a6 Mon Sep 17 00:00:00 2001 From: Alexander Sandor Date: Fri, 23 Oct 2026 10:32:10 +0200 Subject: [PATCH 3/4] docs: Name the code sign-in widgets and mark the old widget as legacy Co-Authored-By: Claude Opus 5.5 --- .../05-providers/02-email/02-configuration.md | 2 +- .../05-providers/02-email/03-customizing-the-ui.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md b/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md index d8503876..4dc5a172 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/02-configuration.md @@ -53,7 +53,7 @@ final emailIdpConfig = EmailIdpConfigFromPasswords( ``` :::tip -This is useful to ensure password policies on the server-side. It is a best practice to pair it with a configuration on the client-side to provide a better UX when creating a new password. The email widgets and controllers have a `passwordRequirements` parameter that can be used to configure the password requirements. +This is useful to ensure password policies on the server-side. It is a best practice to pair it with a configuration on the client-side to provide a better UX when creating a new password. The `EmailOtpSignInWidget` and `EmailOtpAuthController` have a `passwordRequirements` parameter that can be used to configure the password requirements. ::: ### Allowing a password on account creation diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md index 19dd04d4..a3f374dd 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md @@ -280,6 +280,10 @@ To override the default widget, pass your own `EmailSignInWidget` to the `SignIn ### Using the `EmailSignInWidget` +:::note +The `EmailSignInWidget` is the old way to add email sign-in. Use the [`EmailOtpSignInWidget`](#using-the-emailotpsigninwidget) instead, which supports both one-time code and password sign-in. +::: + The `EmailSignInWidget` handles the complete email authentication flow including login, registration, email verification, and password reset. You can customize the widget's behavior using its constructor parameters: From 639a23590666a607551c83fdefbea1d1be67ad94 Mon Sep 17 00:00:00 2001 From: Alexander Sandor Date: Wed, 28 Oct 2026 09:27:29 +0100 Subject: [PATCH 4/4] docs: Keep the terms checkbox optional for code sign-in Restores the note that the terms checkbox is optional and hidden until a terms or privacy callback is provided, next to the EmailOtpSignInWidget, where it only remained in the legacy EmailSignInWidget section. Co-Authored-By: Claude Opus 5.5 --- .../04-authentication/05-providers/02-email/01-setup.md | 2 +- .../05-providers/02-email/03-customizing-the-ui.md | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md b/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md index fb619dd1..2bf7ef98 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/01-setup.md @@ -169,7 +169,7 @@ EmailOtpSignInWidget( The widget automatically handles: - Sending and verifying the one-time code. -- Sign-in to an existing account, or account creation with terms acceptance for a new email. +- Sign-in to an existing account, or account creation for a new email, with optional terms acceptance. - Password sign-in and password reset, if `allowPasswordSignIn` is `true`. - Navigation between screens. diff --git a/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md b/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md index a3f374dd..1955b872 100644 --- a/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md +++ b/docs/06-concepts/04-authentication/05-providers/02-email/03-customizing-the-ui.md @@ -83,7 +83,11 @@ If you change the code format on the server, update `otpVerificationCodeConfig` By default, the widget accepts email aliases, such as `name+alias@example.com`. To reject them, pass `emailValidation: (email) => EmailFlowController.validateEmail(email, allowEmailAliases: false)`. -For a new email, the account creation screen shows the terms checkbox if you provide `onTermsAndConditionsPressed` or `onPrivacyPolicyPressed`. It also offers an optional password if the server enables [`allowPasswordOnOtpRegistration`](./configuration#allowing-a-password-on-account-creation). +:::info +The terms and conditions and privacy policy checkbox on the account creation screen is optional and disabled by default. The checkbox is shown as soon as you provide either `onTermsAndConditionsPressed` or `onPrivacyPolicyPressed`. +::: + +For a new email, the account creation screen also offers an optional password if the server enables [`allowPasswordOnOtpRegistration`](./configuration#allowing-a-password-on-account-creation). Optionally, you can provide an externally managed `EmailOtpAuthController` instance to the widget. A controller and a `client` are mutually exclusive, and `onAuthenticated` and `onError` belong on the controller in that case. The controller carries `allowPasswordSignIn`, `emailValidation`, and `passwordRequirements`, while the code configurations and the terms callbacks stay on the widget.