Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/06-concepts/04-authentication/02-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ By default, endpoints for all providers are disabled. To enable a provider:

```dart
pod.initializeAuthServices(
tokenManagerBuilders: [
JwtConfigFromPasswords(),
],
identityProviderBuilders: [
EmailIdpConfig( /* configuration options */ ),
],
Expand All @@ -127,7 +130,7 @@ By default, endpoints for all providers are disabled. To enable a provider:
$ serverpod start
```

4. Create and apply the migration that initializes the database for the provider. In the `serverpod start` terminal, press **M** to create the migration, then **A** to apply it.
4. Create and apply the migration that initializes the database for the provider. In the `serverpod start` terminal, press **M**. The migration is created and applied in one step. If applying fails, press **A** to retry it.

:::info
If this is the first time creating migrations after adding the module, besides the provider tables, all authentication module tables will also be created. More detailed migration instructions can be found in the [migration guide](../data-and-the-database/database/migrations).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ In your main `server.dart` file, configure the anonymous identity provider using
```dart
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';

import 'src/generated/endpoints.dart';
import 'src/generated/protocol.dart';
import 'package:serverpod_auth_idp_server/providers/anonymous.dart';

void run(List<String> args) async {
Expand Down Expand Up @@ -53,7 +56,7 @@ import 'package:serverpod_auth_idp_server/providers/anonymous.dart';
class AnonymousIdpEndpoint extends AnonymousIdpBaseEndpoint {}
```

Then, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**, then **A**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration).
Then, start the server with `serverpod start` to generate the client code, then create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration).

### Basic configuration options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ AnonymousSignInWidget(
)
```

### Configuring the Server
### Configuring the server

In `onBeforeAnonymousAccountCreated`, receive the optional `token` and verify it with your app-check provider. If verification fails or the token is missing (when you require it), throw an `AnonymousAccountBlockedException` with reason `denied` to block account creation.

Expand Down Expand Up @@ -69,7 +69,7 @@ For Firebase App Check, you can verify the token from a custom backend using the

## Reacting to anonymous account creation

Beside the `onBeforeAnonymousAccountCreated` callback to allow or deny creation, you can also use the `onAfterAnonymousAccountCreated` callback to run logic after a new anonymous account has been created (e.g. analytics or side effects).
Besides the `onBeforeAnonymousAccountCreated` callback to allow or deny creation, you can also use the `onAfterAnonymousAccountCreated` callback to run logic after a new anonymous account has been created (e.g. analytics or side effects).

```dart
AnonymousIdpConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ SignInWidget(
anonymousSignInWidget: AnonymousSignInWidget(
client: client,
createAnonymousToken: () async => await getAppCheckToken(),
size: AnonymousButtonSize.medium,
shape: AnonymousButtonShape.rectangular,
size: SignInButtonSize.medium,
shape: SignInButtonShape.rectangular,
// A custom widget replaces the built-in handling, so pass your own callbacks.
onAuthenticated: () { /* ... */ },
onError: (error) { /* ... */ },
),
)
```
Expand Down Expand Up @@ -45,40 +48,38 @@ AnonymousSignInWidget(
onError: (error) {
// Handle errors
},
size: AnonymousButtonSize.large, // large (default), medium, or small
shape: AnonymousButtonShape.pill, // pill (default) or rectangular
// Button customization. The values shown are the defaults.
size: SignInButtonSize.large, // or medium, small
shape: SignInButtonShape.pill, // or rounded, rectangular
)
```

Optionally, you can provide an externally managed `AnonymousAuthController` instance to the widget. When a controller is provided, `client`, `onAuthenticated`, and `onError` are ignored in favor of the controller's configuration.
Optionally, you can provide an externally managed `AnonymousAuthController` instance to the widget. A controller and a `client` are mutually exclusive, and `onAuthenticated` and `onError` belong on the controller in that case. Passing them alongside a controller trips an assertion, so a debug build throws.

```dart
AnonymousSignInWidget(
controller: controller,
size: AnonymousButtonSize.medium,
shape: AnonymousButtonShape.rectangular,
size: SignInButtonSize.medium,
shape: SignInButtonShape.rectangular,
)
```

### Customizing the button appearance

The widget renders a single **TextButton** with the label "Continue without account". The button uses Flutter's material design system, so it reacts to your app's `Theme`. You can wrap the widget in a `Theme` (or `ThemeData`) to change colors and typography:
The button renders flat, with no background fill and no border, and follows your app's theme brightness for its label color. It sets its own colors and corner radius, so a `TextButtonThemeData` does not reach it and an `ElevatedButtonThemeData` cannot change those. Properties the button leaves unset, such as `side` and `textStyle`, still fall through from that theme.

To change the label's text style, pass `textStyle` to the widget:

```dart
Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
),
),
),
child: AnonymousSignInWidget(client: client),
AnonymousSignInWidget(
client: client,
textStyle: const TextStyle(fontWeight: FontWeight.w600),
)
```

The widget constrains the button to a minimum width of 240 and maximum width of 400; you can place it in a `SizedBox`, `Expanded`, or `Flex` to control layout.
Inside a `SignInWidget`, style every provider button at once with `buttonStyle` instead. See [Styling the buttons](../../ui-components#styling-the-buttons).

The button is at least 240 pixels wide and at most 400. Place it in a `SizedBox`, `Expanded`, or `Flex` to control layout.

## Building a custom UI with the `AnonymousAuthController`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,28 @@ If a code cannot be sent, the failure is recorded in the session log and the sig

Newly generated projects already include the email endpoint at `lib/src/auth/email_idp_endpoint.dart` and the migration that initializes the database, so running `serverpod start` is all that is needed.

If you are adding the auth module to an existing project, extend the abstract endpoint yourself. Create the file anywhere under your server's `lib/` directory (for example, `<project>_server/lib/src/endpoints/`); the generator picks it up:
If you are adding the auth module to an existing project, extend the abstract endpoint yourself. Create the file anywhere under your server's `lib/` directory (for example, `<project>_server/lib/src/endpoints/`). The generator picks it up:

```dart
import 'package:serverpod_auth_idp_server/providers/email.dart';

class EmailIdpEndpoint extends EmailIdpBaseEndpoint {}
```

Then start the server with `serverpod start` to generate the client code, and create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**, then **A**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration).
Then start the server with `serverpod start` to generate the client code, and create and apply the migration that initializes the database for the provider (in the `serverpod start` terminal, press **M**). More detailed instructions can be found in the general [identity providers setup section](../../setup#identity-providers-configuration).

### Use your own email provider

Serverpod Cloud delivery is there to get sign-in working quickly, and it sends a standard message carrying your `appDisplayName`. You might prefer using a custom email provider to have full control over the body, layout, and language of the emails. For servers hosted outside of Serverpod Cloud, it is the only option.

Changing the email provider is done by replacing `ServerpodCloudEmailIdpConfig` with `EmailIdpConfigFromPasswords`, which requires you to pass your own callbacks for the two codes. One convenient option is the [mailer](https://pub.dev/packages/mailer) package, which can send emails through any SMTP service. Most email providers, such as Resend, Sendgrid or Mandrill, support SMTP.
Changing the email provider is done by replacing `ServerpodCloudEmailIdpConfig` with `EmailIdpConfigFromPasswords`, which requires you to pass your own callbacks for the two codes. One convenient option is the [mailer](https://pub.dev/packages/mailer) package, which can send emails through any SMTP service. Most email providers, such as Resend, SendGrid, or Mandrill, support SMTP.

```dart
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';

import 'src/generated/endpoints.dart';
import 'src/generated/protocol.dart';
import 'package:serverpod_auth_idp_server/providers/email.dart';

void run(List<String> args) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Below is a non-exhaustive list of some of the most common configuration options.

### Peppering

A pepper is a server-side secret that is added, along with a unique salt, to a password before it is hashed and stored. The pepper makes it harder for an attacker to crack password hashes if they have only gained access to the database.
A pepper is a server-side secret mixed into a password before it is hashed and stored, so a database leak alone is not enough to crack the hashes. See [storing secrets](../../setup#storing-secrets) for where to keep it.

The pepper is configured via the `secretHashPepper` property in `EmailIdpConfig`, or read from the `emailSecretHashPepper` password by `EmailIdpConfigFromPasswords`, as described in the [server-side configuration](./setup#server-side-configuration) section. Its [recommended pepper length](https://www.ietf.org/archive/id/draft-ietf-kitten-password-storage-04.html#name-storage-2) is 32 bytes.

Expand All @@ -33,7 +33,7 @@ final emailIdpConfig = EmailIdpConfigFromPasswords(
);
```

### Customizing Password Requirements
### Customizing password requirements

By default, the minimum password length is set to 8 characters. If you wish to modify this requirement, you can use the `passwordValidationFunction` configuration option.

Expand All @@ -54,22 +54,22 @@ final emailIdpConfig = EmailIdpConfigFromPasswords(
This is useful to ensure password policies on the server-side. It is a best practice to pair it with a configuration on the client-side to provide a better UX when creating a new password. The `EmailSignInWidget` and `EmailAuthController` have a `passwordRequirements` parameter that can be used to configure the password requirements.
:::

### Custom Verification Code Generation
### Custom verification code generation

You can customize how verification codes are generated:

```dart
final emailIdpConfig = EmailIdpConfigFromPasswords(
registrationVerificationCodeGenerator: () {
// Generate a 6-digit numeric code
final random = Random();
final random = Random.secure();
return List.generate(6, (_) => random.nextInt(10)).join();
},
);
```

:::warning
Remember to configure the `verificationCodeConfig` parameter on the `EmailSignInWidget` to match the length of the verification code you generate. Otherwise, users will never be able to enter the verification code correctly. See the [customizing the UI section](./customizing-the-ui) for more details.
Remember to configure the `verificationCodeConfig` parameter on the `EmailSignInWidget` to match the length and allowed characters of the verification code you generate. Otherwise, users will never be able to enter the verification code correctly. See the [customizing the UI section](./customizing-the-ui) for more details.
:::

#### Bypassing verification code in development
Expand All @@ -84,7 +84,7 @@ pod.initializeAuthServices(
identityProviderBuilders: [
EmailIdpConfigFromPasswords(
registrationVerificationCodeGenerator: pod.runMode == 'development'
? () => 'aaaaaaaa' // Be sure to match the length used in production.
? () => '11111111' // Digits only, and the same length as production.
: defaultVerificationCodeGenerator,
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ SignInWidget(
client: client,
emailSignInWidget: EmailSignInWidget(
client: client,
// Change the initial screen to start registration
startScreen: EmailFlowScreen.startRegistration,
// Open on the login screen instead of the default registration screen
startScreen: EmailFlowScreen.login,
// A custom widget replaces the built-in handling, so pass your own callbacks.
onAuthenticated: () { /* ... */ },
onError: (error) { /* ... */ },
),
)
```
Expand All @@ -37,14 +40,15 @@ EmailSignInWidget(
verificationCodeConfig: VerificationCodeConfig(
length: 6,
keyboardType: TextInputType.number,
allowedLetterCase: LetterCase.lowercase,
allowedCharactersPattern: RegExp(r'[0-9]'),
// The wait before the user can request a new code.
resendCountdownDuration: Duration(minutes: 1),
),
// Custom email validation function
// Custom email validation function. Throw InvalidEmailException so the
// widget shows the message. Other exceptions block sign-in silently.
emailValidation: (email) {
if (!email.contains('@example.com')) {
throw FormatException('Only @example.com emails allowed');
throw const InvalidEmailException('Only @example.com emails allowed');
}
},
// Customize the password requirements
Expand All @@ -71,27 +75,24 @@ EmailSignInWidget(
onError: (error) {
// Handle errors
},
// Change the wait time before a user can request a new verification code
resendCountdownDuration: Duration(minutes: 2),
)
```

Optionally, you can provide an externally managed `EmailAuthController` instance to the widget, which will ignore all other configuration options in favor of the controller's state. The controller contains all options above - with the exception of the `verificationCodeConfig` option, which is only used by the widget.
Optionally, you can provide an externally managed `EmailAuthController` instance to the widget. A controller and a `client` are mutually exclusive, and `onAuthenticated` and `onError` belong on the controller in that case. Passing either alongside a controller trips an assertion, so a debug build throws. The controller carries the sign-in options, while `verificationCodeConfig`, `onTermsAndConditionsPressed`, and `onPrivacyPolicyPressed` stay on the widget.

```dart
EmailSignInWidget(
client: client,
controller: controller,
)
```

:::info
The terms and conditions and privacy policy checkbox on the registration screen are optional and disabled by default. The checkbox will only be shown if you provide both `onTermsAndConditionsPressed` and `onPrivacyPolicyPressed` callbacks.
The terms and conditions and privacy policy checkbox on the registration screen are optional and disabled by default. The checkbox is shown as soon as you provide either `onTermsAndConditionsPressed` or `onPrivacyPolicyPressed`.
:::

### Customizing the default widget's appearance

Since the `EmailSignInWidget` uses the material design system, it will react to your app's material theme. You can also wrap it in a `Theme` widget to apply a custom theme.
Since the `EmailSignInWidget` uses the Material Design system, it will react to your app's Material theme. You can also wrap it in a `Theme` widget to apply a custom theme.

```dart
import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart';
Expand All @@ -101,10 +102,8 @@ Theme(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
// Use the AuthIdpTheme to customize the verification code input look
extensions: <ThemeExtension<dynamic>>[
AuthIdpTheme(
defaultPinTheme: PinTheme(...),
AuthIdpTheme.defaultTheme(
errorPinTheme: PinTheme(...),
successPinTheme: PinTheme(...),
),
],
),
Expand Down Expand Up @@ -134,7 +133,7 @@ final controller = EmailAuthController(
);
```

### EmailAuthController State Management
### EmailAuthController state management

Your widget should render the appropriate screen based on the `currentScreen` property of the controller. You can also use the below state properties to build your UI:

Expand Down Expand Up @@ -185,7 +184,7 @@ if (controller.canNavigateBack) {
}
```

### EmailAuthController Methods
### EmailAuthController methods

The controller provides methods for each step of the authentication flow:

Expand All @@ -204,7 +203,7 @@ controller.passwordController.text = 'password123';
await controller.login();
```

#### Registration Flow
#### Registration flow

The registration flow consists of three steps:

Expand Down Expand Up @@ -232,7 +231,7 @@ await controller.finishRegistration();
// User is now authenticated
```

#### Password Reset Flow
#### Password reset flow

The password reset flow also consists of three steps:

Expand Down Expand Up @@ -260,7 +259,7 @@ await controller.finishPasswordReset();
// User is authenticated with new password
```

### Resending Verification Codes
### Resending verification codes

To resend a verification code:

Expand Down
Loading
Loading