-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Add section around how to set up custom authentication integrati…
…on. (#86) * chore: run npm audit fix * fix: change authentication setup page from mdx to md format. * fix: add custom integration authentication section. * fix: improve setup introduction and link to custom integration. * fix: add custom provider documentation. * fix: Minor fixes to auth docs. * fix: Fixes broken link --------- Co-authored-by: Viktor Lidholt <[email protected]>
- Loading branch information
Showing
9 changed files
with
743 additions
and
60 deletions.
There are no files selected for viewing
8 changes: 3 additions & 5 deletions
8
...5-concepts/10-authentication/01-setup.mdx → ...05-concepts/10-authentication/01-setup.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
docs/05-concepts/10-authentication/04-providers/06-custom-providers.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
# Custom providers | ||
|
||
Serverpod's authentication module makes it easy to implement custom authentication providers. This allows you to leverage all the existing providers supplied by the module along with the specific providers your project requires. | ||
|
||
## Server setup | ||
|
||
After successfully authenticating a user through a customer provider, an auth token can be created and connected to the user to preserve the authenticated user's permissions. This token is used to identify the user and facilitate endpoint authorization validation. The token can be removed when the user signs out to prevent further access. | ||
|
||
### Connect user | ||
|
||
The authentication module provides methods to find or create users. This ensures that all authentication tokens from the same user are connected. | ||
|
||
Users can be identified either by their email through the `Users.findUserByEmail(...)` method or by a unique identifier through the `Users.findUserByIdentifier(...)` method. | ||
|
||
If no user is found, a new user can be created through the `Users.createUser(...)` method. | ||
|
||
```dart | ||
UserInfo? userInfo; | ||
userInfo = await Users.findUserByEmail(session, email); | ||
userInfo ??= await Users.findUserByIdentifier(session, userIdentifier); | ||
if (userInfo == null) { | ||
userInfo = UserInfo( | ||
userIdentifier: userIdentifier, | ||
userName: name, | ||
email: email, | ||
blocked: false, | ||
created: DateTime.now().toUtc(), | ||
scopeNames: [], | ||
); | ||
userInfo = await Users.createUser(session, userInfo, _authMethod); | ||
} | ||
``` | ||
|
||
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. | ||
|
||
:::note | ||
|
||
For many authentication platforms the `userIdentifier` is the user's email, but it can also be another unique identifier such as a phone number or a social security number. | ||
|
||
::: | ||
|
||
### 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. | ||
|
||
```dart | ||
var userInfo = await UserInfo.db.findFirstRow( | ||
session, | ||
where: (t) => t.fullName.equals(name), | ||
); | ||
``` | ||
|
||
The example above shows how to find a user by name using the `UserInfo` model. | ||
|
||
### Create auth token | ||
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. | ||
|
||
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. | ||
|
||
```dart | ||
var authToken = await session.auth.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`. | ||
|
||
### 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. | ||
|
||
```dart | ||
class MyAuthenticationEndpoint extends Endpoint { | ||
Future<AuthenticationResponse> login( | ||
Session session, | ||
String username, | ||
String password, | ||
) async { | ||
// Authenticates a user with email and password. | ||
if (!authenticateUser(session, username, password)) { | ||
return AuthenticationResponse(success: false); | ||
} | ||
// Finds or creates a user in the database using the User methods. | ||
var userInfo = findOrCreateUser(session, username); | ||
// Creates an authentication key for the user. | ||
var authToken = await session.auth.signInUser( | ||
userInfo.id!, | ||
'myAuth', | ||
scopes: {}, | ||
); | ||
// Returns the authentication response. | ||
return AuthenticationResponse( | ||
success: true, | ||
keyId: authToken.id, | ||
key: authToken.key, | ||
userInfo: userInfo, | ||
); | ||
} | ||
} | ||
``` | ||
|
||
The example above shows how to create an `AuthenticationResponse` with the auth token and user information. | ||
|
||
## 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`. | ||
|
||
The session manager is responsible for storing the auth token and user information. It is initialized on client startup and will restore any existing user session from local storage. | ||
|
||
After a successful authentication where an authentication response is returned from the server, the user should be registered in the session manager through the `sessionManager.registerSignedInUser(...)` method. The session manager singleton is accessible by calling `SessionManager.instance`. | ||
|
||
```dart | ||
var serverResponse = await caller.myAuthentication.login(username, password); | ||
if (serverResponse.success) { | ||
// Store the user info in the session manager. | ||
AuthenticationResponse sessionManager = await SessionManager.instance; | ||
await sessionManager.registerSignedInUser( | ||
serverResponse.userInfo!, | ||
serverResponse.keyId!, | ||
serverResponse.key!, | ||
); | ||
} | ||
``` | ||
|
||
The example above shows how to register a signed-in user in the session manager. |
194 changes: 194 additions & 0 deletions
194
docs/05-concepts/10-authentication/05-custom-overrides.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
# 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. | ||
|
||
## 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. | ||
|
||
### Create auth token | ||
|
||
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`. | ||
|
||
```dart | ||
// Initialize Serverpod and connect it with your generated code. | ||
final pod = Serverpod( | ||
args, | ||
Protocol(), | ||
Endpoints(), | ||
authenticationHandler: (Session session, String token) async { | ||
/// Custom validation handler | ||
if (token != 'valid') return null; | ||
return AuthenticationInfo(1, <Scope>{}); | ||
}, | ||
); | ||
``` | ||
|
||
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`. | ||
|
||
### 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. | ||
|
||
```dart | ||
class UserEndpoint extends Endpoint { | ||
Future<String?> 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(); | ||
} | ||
} | ||
``` | ||
|
||
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-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. | ||
|
||
## Client setup | ||
Enabling authentication in the client is as simple as configuring a key manager and placing any token in it. If a key manager is configured, the client will automatically query the manager for a token and include it in communication with the server. | ||
|
||
### Configure key manager | ||
Key managers need to implement the `AuthenticationKeyManager` interface. The key manager is configured when creating the client by passing it as the named parameter `authenticationKeyManager`. If no key manager is configured, the client will not include tokens in requests to the server. | ||
|
||
```dart | ||
class SimpleAuthKeyManager extends AuthenticationKeyManager { | ||
String? _key; | ||
@override | ||
Future<String?> get() async { | ||
return _key; | ||
} | ||
@override | ||
Future<void> put(String key) async { | ||
_key = key; | ||
} | ||
@override | ||
Future<void> remove() async { | ||
_key = null; | ||
} | ||
} | ||
var client = Client('http://$localhost:8080/', | ||
authenticationKeyManager: SimpleAuthKeyManager()) | ||
..connectivityMonitor = FlutterConnectivityMonitor(); | ||
``` | ||
|
||
In the above example, the `SimpleAuthKeyManager` is configured as the client's authentication key manager. The `SimpleAuthKeyManager` stores the token in memory. | ||
|
||
:::info | ||
|
||
The `SimpleAuthKeyManager` is not practical and should only be used for testing. A secure implementation of the key manager is available in the `serverpod_auth_shared_flutter` package named `FlutterAuthenticationKeyManager`. It provides safe, persistent storage for the auth token. | ||
|
||
::: | ||
|
||
The key manager is then available through the client's `authenticationKeyManager` field. | ||
|
||
```dart | ||
var keyManager = client.authenticationKeyManager; | ||
``` | ||
|
||
### Store token | ||
When the client receives a token from the server, it is responsible for storing it in the key manager using the `put` method. The key manager will then include the token in all requests to the server. | ||
|
||
```dart | ||
await client.authenticationKeyManager?.put(token); | ||
``` | ||
|
||
In the above example, the `token` is placed in the key manager. It will now be included in communication with the server. | ||
|
||
### Remove token | ||
To remove the token from the key manager, call the `remove` method. | ||
|
||
```dart | ||
await client.authenticationKeyManager?.remove(); | ||
``` | ||
|
||
The above example removes any token from the key manager. | ||
|
||
### Retrieve token | ||
To retrieve the token from the key manager, call the `get` method. | ||
|
||
```dart | ||
var token = await client.authenticationKeyManager?.get(); | ||
``` | ||
|
||
The above example retrieves the token from the key manager and stores it in the `token` variable. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
{ | ||
"label": "Authenticaton", | ||
"label": "Authentication", | ||
"collapsed": true | ||
} | ||
|
Oops, something went wrong.