From bb0df681dcd54c5ce9c78a0b250a39bdcc0e8778 Mon Sep 17 00:00:00 2001 From: Isak Date: Mon, 20 May 2024 11:12:50 +0200 Subject: [PATCH] docs: Rework auth documentation for breaking changes in 2.0. (#108) --- .../05-concepts/10-authentication/01-setup.md | 21 +++- .../04-providers/06-custom-providers.md | 46 +++++++- .../10-authentication/05-custom-overrides.md | 104 ++++-------------- docs/12-upgrading/01-upgrade-to-two.md | 92 +++++++++++++++- 4 files changed, 172 insertions(+), 91 deletions(-) diff --git a/docs/05-concepts/10-authentication/01-setup.md b/docs/05-concepts/10-authentication/01-setup.md index b471dca8..67c35dfe 100644 --- a/docs/05-concepts/10-authentication/01-setup.md +++ b/docs/05-concepts/10-authentication/01-setup.md @@ -14,10 +14,25 @@ Serverpod's auth module makes it easy to authenticate users through email or 3rd Add the module as a dependency to the server projects `pubspec.yaml`. -```yaml -dependencies: +```sh +$ dart pub add serverpod_auth_server +``` + +Add the authentication handler to the Serverpod instance. + +```dart +import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth; + +void run(List args) async { + var pod = Serverpod( + args, + Protocol(), + Endpoints(), + authenticationHandler: auth.authenticationHandler, // Add this line + ); + ... - serverpod_auth_server: ^1.x.x +} ``` Add a nickname for the module in the `config/generator.yaml` file. This nickname will be used as the name of the module in the code. diff --git a/docs/05-concepts/10-authentication/04-providers/06-custom-providers.md b/docs/05-concepts/10-authentication/04-providers/06-custom-providers.md index 82fdffaf..65685dec 100644 --- a/docs/05-concepts/10-authentication/04-providers/06-custom-providers.md +++ b/docs/05-concepts/10-authentication/04-providers/06-custom-providers.md @@ -31,7 +31,7 @@ if (userInfo == null) { } ``` -The example above tries to find a user by email and user identifier. If no user is found, a new user is created with the provided information. The methods that you must implement yourself is `authenticateUser` and `findOrCreateUser`, keep in mind that they possibly take different parameters than in this simplified example. +The example above tries to find a user by email and user identifier. If no user is found, a new user is created with the provided information. :::note @@ -41,7 +41,7 @@ For many authentication platforms the `userIdentifier` is the user's email, but ### Custom identification methods -If other identification methods are required you can easly implement them by accessing the database directly. The `UserInfo` model can be interacted with in the same way as any other model with a database in Serverpod. +If other identification methods are required you can easily implement them by accessing the database directly. The `UserInfo` model can be interacted with in the same way as any other model with a database in Serverpod. ```dart var userInfo = await UserInfo.db.findFirstRow( @@ -56,12 +56,12 @@ The example above shows how to find a user by name using the `UserInfo` model. When a user has been found or created, an auth token that is connected to the user should be created. -To create an auth token, call the `signInUser` method in the `UserAuthentication` class, accessible through the `session.auth` field on the `session` object. +To create an auth token, call the `signInUser` method in the `UserAuthentication` class, accessible as a static method, e.g. `UserAuthentication.signInUser`. -The `signInUser` method takes three arguments: the first is the user ID, the second is information about the method of authentication, and the third is a set of scopes granted to the auth token. +The `signInUser` method takes four arguments: the first is the session object, the second is the user ID, the third is information about the method of authentication, and the fourth is a set of scopes granted to the auth token. ```dart -var authToken = await session.auth.signInUser(userInfo.id, 'myAuthMethod', scopes: { +var authToken = await UserAuthentication.signInUser(userInfo.id, 'myAuthMethod', scopes: { Scope('delete'), Scope('create'), }); @@ -69,6 +69,10 @@ var authToken = await session.auth.signInUser(userInfo.id, 'myAuthMethod', scope The example above creates an auth token for a user with the unique identifier taken from the `userInfo`. The auth token preserves that it was created using the method `myAuthMethod` and has the scopes `delete` and `create`. +:::info +The unique identifier for the user should uniquely identify the user regardless of authentication method. The information allows authentication tokens associated with the same user to be grouped. +::: + ### Send auth token to client Once the auth token is created, it should be sent to the client. We recommend doing this using an `AuthenticationResponse`. This ensures compatibility with the client-side authentication module. @@ -89,7 +93,8 @@ class MyAuthenticationEndpoint extends Endpoint { var userInfo = findOrCreateUser(session, username); // Creates an authentication key for the user. - var authToken = await session.auth.signInUser( + var authToken = await UserAuthentication.signInUser( + session, userInfo.id!, 'myAuth', scopes: {}, @@ -108,6 +113,35 @@ class MyAuthenticationEndpoint extends Endpoint { The example above shows how to create an `AuthenticationResponse` with the auth token and user information. +### Remove auth token + +Signing out a user on all devices is made simple with the `signOutUser` method in the `UserAuthentication` class. The method removes all auth tokens associated with the user. + +```dart +class AuthenticatedEndpoint extends Endpoint { + @override + bool get requireLogin => true; + Future logout(Session session) async { + await UserAuthentication.signOutUser(session); + } +} +``` + +In the above example, the `logout` endpoint removes all auth tokens associated with the user. The user is then signed out and loses access to any protected endpoints. + +#### Remove specific tokens + +The `AuthKey` table stores all auth tokens and can be interacted with in the same way as any other model with a database in Serverpod. To remove specific tokens, the `AuthKey` table can be interacted with directly. + +```dart +await AuthKey.db.deleteWhere( + session, + where: (t) => t.userId.equals(userId) & t.method.equals('username'), +); +``` + +In the above example, all auth tokens associated with the user `userId` and created with the method `username` are removed from the `AuthKey` table. + ## Client setup The client must store and include the auth token in communication with the server. Luckily, the client-side authentication module handles this for you through the `SessionManager`. diff --git a/docs/05-concepts/10-authentication/05-custom-overrides.md b/docs/05-concepts/10-authentication/05-custom-overrides.md index 7458e892..bdf7d98f 100644 --- a/docs/05-concepts/10-authentication/05-custom-overrides.md +++ b/docs/05-concepts/10-authentication/05-custom-overrides.md @@ -1,48 +1,14 @@ # Custom overrides -Serverpod is designed to make it as simple as possible to implement custom authentication overrides. The framework comes with an integrated auth token creation, validation, and communication system. With a simple setup, it is easy to generate custom tokens and include them in authenticated communication with the server. +It is recommended to use the `serverpod_auth` package but if you have special requirements not fulfilled by it, you can implement your authentication module. Serverpod is designed to make it easy to add custom authentication overrides. ## Server setup -After successfully authenticating a user, for example, through a username and password, an auth token can be created to preserve the authenticated user's permissions. This token is used to identify the user and facilitate endpoint authorization validation. When the user signs out, the token can be removed to prevent further access. +When running a custom auth integration it is up to you to build the authentication model and issuing auth tokens. -### Create auth token +### Token validation -To create an auth token, call the `signInUser` method in the `UserAuthentication` class, accessible through the `session.auth` field on the `session` object. - -The `signInUser` method takes three arguments: the first is a unique `integer` identifier for the user, the second is information about the method used to authenticate the user, and the third is a set of scopes granted to the auth token. - -```dart -var authToken = await session.auth.signInUser(myUserObject.id, 'myAuthMethod', scopes: { - Scope('delete'), - Scope('create'), -}); -``` - -The example above creates an auth token for a user with the unique identifier taken from `myUserObject`. The auth token preserves that it was created using the method `myAuthMethod` and has the scopes `delete` and `create`. - -:::info -The unique identifier for the user should uniquely identify the user regardless of authentication method. The information allows authentication tokens associated with the same user to be grouped. -::: - -#### Custom auth tokens - -The `UserAuthentication` class simplifies the token management but makes assumptions about what information should be stored in the auth token. If your project has different requirements, managing auth tokens manually with your defined model is possible. Custom auth tokens require that the token validation is overridden and adjusted to the new auth token format, explained in [override token validation](#override-token-validation). - -### Token validation format - -The framework requires tokens to be of `String` type, and the default token validation expects the token to be in the format `userId:key`. The `userId` is the unique identifier for the user, and the `key` is a generated auth token key. The `userId` and `key` are then retrieved from the token and validated towards the auth token stored as a result of the call to `session.auth.signInUser(...)`. - -```dart -var authToken = await session.auth.signInUser(....); -var verifiableToken = '${authToken.userId}:${authToken.key}'; -``` - -In the above example, the `verifiableToken` is created by concatenating the `userId` and `key` from the `authToken`. This token is then verifiable by the default token validation. - -#### Override token validation - -The token validation method can be overridden by providing a custom `authenticationHandler` callback when initializing Serverpod. The callback should return an `AuthenticationInfo` object if the token is valid, otherwise `null`. +The token validation is performed by providing a custom `authenticationHandler` callback when initializing Serverpod. The callback should return an `AuthenticationInfo` object if the token is valid, otherwise `null`. ```dart // Initialize Serverpod and connect it with your generated code. @@ -61,13 +27,28 @@ final pod = Serverpod( In the above example, the `authenticationHandler` callback is overridden with a custom validation method. The method returns an `AuthenticationInfo` object with user id `1` and no scopes if the token is valid, otherwise `null`. +:::note +In the authenticationHandler callback the `authenticated` field on the session will always be `null` as it is the authenticationHandler that figures out who the user is. +::: + +#### Scopes + +The scopes returned from the `authenticationHandler` is used to grant access to scope restricted endpoints. The `Scope` class is a simple wrapper around a nullable `String` in dart. This means that you can format your scopes however you want as long as they are in a String format. + +Normally if you implement a JWT you would store the scopes inside the token. When extracting them all you have to do is convert the String stored in the token into a Scope object by calling the constructor. + +```dart +List scopes = extractScopes(token); +Set userScopes = scopes.map((scope) => Scope(scope)).toSet(); +``` + ### Send token to client -After creating the token, it should be sent to the client. The client is then responsible for storing the token and including it in communication with the server. The token is usually sent in response to a successful sign-in request. +You are responsible for implementing the endpoints to authenticate/authorize the user. But as an example such an endpoint could look like the following. ```dart class UserEndpoint extends Endpoint { - Future login( + Future login( Session session, String username, String password, @@ -75,51 +56,12 @@ class UserEndpoint extends Endpoint { var identifier = authenticateUser(session, username, password); if (identifier == null) return null; - var authToken = await session.auth.signInUser( - identifier, - 'username', - scopes: {}, - ); - - return '${authToken.id}:${authToken.key}'; - } -} -``` - -In the above example, the `login` method authenticates the user and creates an auth token. The token is then returned to the client in the format expected by the default token validation. - -### Remove auth token - -When the default token validation is used, signing out a user on all devices is made simple with the `signOutUser` method in the `UserAuthentication` class. The method removes all auth tokens associated with the user. - -```dart -class AuthenticatedEndpoint extends Endpoint { - @override - bool get requireLogin => true; - Future logout(Session session) async { - await session.auth.signOutUser(); + return issueMyToken(identifier, scopes: {}); } } ``` -In the above example, the `logout` endpoint removes all auth tokens associated with the user. The user is then signed out and loses access to any protected endpoints. - -#### Remove specific tokens - -The `AuthKey` table stores all auth tokens and can be interacted with in the same way as any other model with a database in Serverpod. To remove specific tokens, the `AuthKey` table can be interacted with directly. - -```dart -await AuthKey.db.deleteWhere( - session, - where: (t) => t.userId.equals(userId) & t.method.equals('username'), -); -``` - -In the above example, all auth tokens associated with the user `userId` and created with the method `username` are removed from the `AuthKey` table. - -#### Custom token solution - -If a [custom auth token](#custom-auth-tokens) solution has been implemented, auth token removal must be handled manually. The `signOutUser` method does not provide an interface to interact with other database tables. +In the above example, the `login` method authenticates the user and creates an auth token. The token is then returned to the client. ## Client setup diff --git a/docs/12-upgrading/01-upgrade-to-two.md b/docs/12-upgrading/01-upgrade-to-two.md index f164f7a9..a02b329d 100644 --- a/docs/12-upgrading/01-upgrade-to-two.md +++ b/docs/12-upgrading/01-upgrade-to-two.md @@ -1,5 +1,69 @@ # Upgrade to 2.0 +## Changes to authentication + +The base auth implementation has been removed from Serverpod core and moved into the `serverpod_auth` package. If you are not using authentication at all this change does not impact you. If you are using the auth module already the transition is simple. + +The default authentication handler will now throw an `UnimplementedError`. It is now required to supply the authentication handler to the Serverpod object, in your server.dart file make the following change: + +```dart +import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth; + +void run(List args) async { + var pod = Serverpod( + args, + Protocol(), + Endpoints(), + authenticationHandler: auth.authenticationHandler, // Add this line + ); + + ... +} +``` + +### Advanced integrations + +The methods `signInUser` and `signOutUser` now takes the session object as a param and is no longer available on the session object. Instead import the class `UserAuthentication` from the auth module to access these static methods. + +```dart +UserAuthentication.signInUser(session, userId, 'provider'); + +UserAuthentication.signOutUser(session); +``` + +The table `serverpod_auth_key` has been removed from Serverpod core but is available in the serverpod_auth module instead. This means that if you wrote a custom integration before without using the serverpod_auth module you have to take care of managing your token implementation. + +Adding the definition of the `serverpod_auth_key` table to your project is the simplest way to do a seamless migration. + +The table was defined in the following way: + +```yaml +### Provides a method of access for a user to authenticate with the server. +class: AuthKey +table: serverpod_auth_key +fields: + ### The id of the user to provide access to. + userId: int + + ### The hashed version of the key. + hash: String + + ### The key sent to the server to authenticate. + key: String?, !persist + + ### The scopes this key provides access to. + scopeNames: List + + ### The method of signing in this key was generated through. This can be email + ### or different social logins. + method: String +indexes: + serverpod_auth_key_userId_idx: + fields: userId +``` + +Your are then responsible for creating/removing entries in this table, the old `signInUser` and `signOutUser` that used to provide this functionality can be found [here](https://github.com/serverpod/serverpod/blob/13795a7bd4c0cc5a03101b6f378cb914673046dd/packages/serverpod/lib/src/server/session.dart#L359-L394). + ## Changes to the Session Object ### Removed deprecated fields @@ -18,7 +82,33 @@ session.db.find(...); ### Authenticated user information retrieval -In Serverpod 2.0, we have removed the getters `scopes` and `authenticatedUser` from session. This information is now retrievable through the `authenticationInfo` getter as fields of the returned object. +In Serverpod 2.0, we have removed the getters `scopes` and `authenticatedUser` from session. This information is now retrievable through the `authenticated` getter as fields of the returned object. + +Replace this: + +```dart +int? userId = await session.auth.authenticatedUser; + +Set? scopes = await session.scopes; +``` + +With this: + +```dart +final authenticated = await session.authenticated; + +//Read authenticated userId +int? userId = authenticated?.userId; + +//Read scopes +Set? scopes = authenticated?.scopes; +``` + +If the `authenticated` property is set on the session it effectively means there is an authenticated user making the request. + +### Authentication helpers + +The field `auth` has been removed and the methods `signInUser` and `signOutUser` have been moved to the `serverpod_auth` module. ## Changes to database queries