Skip to content

Commit

Permalink
docs: Rework auth documentation for breaking changes in 2.0. (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
Isakdl authored May 20, 2024
1 parent 018af83 commit bb0df68
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 91 deletions.
21 changes: 18 additions & 3 deletions docs/05-concepts/10-authentication/01-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand All @@ -56,19 +56,23 @@ 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'),
});
```

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.
Expand All @@ -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: {},
Expand All @@ -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<void> 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`.
Expand Down
104 changes: 23 additions & 81 deletions docs/05-concepts/10-authentication/05-custom-overrides.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -61,65 +27,41 @@ 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<String> scopes = extractScopes(token);
Set<Scope> 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<String?> login(
Future<LoginResponse> login(
Session session,
String username,
String password,
) async {
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<void> 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

Expand Down
92 changes: 91 additions & 1 deletion docs/12-upgrading/01-upgrade-to-two.md
Original file line number Diff line number Diff line change
@@ -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<String> 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<String>

### 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
Expand All @@ -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>? scopes = await session.scopes;
```

With this:

```dart
final authenticated = await session.authenticated;
//Read authenticated userId
int? userId = authenticated?.userId;
//Read scopes
Set<Scopes>? 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

Expand Down

0 comments on commit bb0df68

Please sign in to comment.