Skip to content

Commit 3c30a89

Browse files
committed
Rewrite for new one-shot migration with client upgrade requirement
1 parent 2002c24 commit 3c30a89

2 files changed

Lines changed: 191 additions & 211 deletions

File tree

Lines changed: 191 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,237 @@
1-
# Upgrading from `serverpod_auth` (in one go)
1+
# Upgrading from `serverpod_auth`
22

3-
With the release of Serverpod 3.0, the single legacy `serverpod_auth` package was replaced with a set of modular packages providing flexible modules for users, authentication providers, profiles, and session management.
3+
With the release of Serverpod 3.0, the “monolith” `serverpod_auth` package was deprecated and replaced with a set of modular packages providing flexible modules for user management, authentication providers, profiles, and sessions. Switching to the new authentication package enables you to make use of the updated and extended authentication methods (and upcoming ones like Passkeys and magic lines).
4+
The new package also makes used of the recently introduced support for `UUID` primary key on all its entities. Thus in addition to migrating from the legacy package to the new ones, one also has to update all their own entities which previously referenced the `UserInfo`’s `id`.
45

56
For an existing Serverpod application which makes use of `serverpod_auth`[^1] to upgrade to the new packages, there exists the packages `serverpod_auth_migration` to facilitate moving over all authentication- and user-related data into the new modules.
67
The package `serverpod_auth_backwards_compatibility` was created to support existing clients with legacy sessions and the migration of social logins and email passwords[^2].
78

8-
Once the one-time migration is complete, the legacy `serverpod_auth` module and the `serverpod_auth_migration` dependencies can be removed. This will also remove the then obsolete tables from the database.
9-
The backwards compatibility needs to kept until all (or all relevant) data has been fully migrated.
9+
Once the one-time migration is complete, the legacy `serverpod_auth` module and the `serverpod_auth_migration` dependencies can be removed. This will also remove the obsolete tables from the database.
10+
The backwards compatibility package needs to kept until all relevant data has been fully migrated.[^3]
1011

11-
Due to the modular approach, each used authentication provider (email, Apple, Google, etc.) needs to be configured individually for the migration.
12-
No matter through which authentication provider(s) a user is migrated, their profile will always be migrated as well by default.
12+
ℹ️ The currently provided migration helpers make the following assumptions:
13+
14+
1. That the size of your database is small enough to be migrated in whatever maintenance window you can allot.
15+
2. That clients either get updated immediately (e.g. Flutter Web) or that an update can be forced (for installed apps), in order to align with breaking changes on the API when switching from the legacy to the new endpoints. \
16+
Clients which update will be able to keep running on their existing session, but clients that do not update won’t work anymore.
17+
18+
As there is no urgency to migrate to the new packages for existing applications, the transition should be carefully planned and tested.
1319

1420
## General timeline
1521

16-
The overall timeline to migrate from `serverpod_auth` to the new modules generally looks like this:
22+
The overall timeline to migrate from `serverpod_auth` to the new modules is given below.
1723

1824
1. Add and configure the new auth modules for the desired providers, e.g. [email](../concepts/authentication/setup_new#email).
19-
2. Add the migration module `serverpod_auth_migration` and configure the migration your `run` method
20-
3. Update the `authenticationHandler` to support both legacy (migrated) and new sessions
21-
4. Set up the `serverpod_auth_backwards_compatibility` in your email and social account endpoints
22-
<!-- Since `serverpod_auth` is going to get broken with the next deploy in a few seconds,
23-
we should probably disable it already at this point. Else the developer needs to use the other guide. -->
24-
4. Release client working against the new endpoints, make sure the legacy `serverpod_auth` ones will not be used anymore
25+
2. Add the migration module `serverpod_auth_migration` and configure the migration the server’s `run` method.
26+
3. Update the `authenticationHandler` to use the new `serverpod_auth_session` package.
27+
4. Add the `serverpod_auth_backwards_compatibility` module and connect its helpers in the account login methods.
28+
5. Disable the `serverpod_auth` endpoint.
29+
6. Update the client to only use the new endpoints and the `SessionManager` from `serverpod_auth_session_flutter`. Ensure that `serverpod_auth_client` and `serverpod_auth_shared_flutter` are not used anymore.
2530
If deploying to an app store with long lead times, prepare this well in advance, so that new updates / downloads will be able to login and register against the new APIs.
26-
5. Start the server and run the migration to finish
27-
6. Remove the `serverpod_auth_migration` and `serverpod_auth` module
28-
⚠️ This will remove the ability to login or register through the old endpoints. If this is a problem for your application, see the [guide for a continuous migration].
29-
<!-- TODO: The old `SessionManger` might to a check which then fails. Ensure that the user does not get logged out because of that -> 'serverpod_auth.status.getUserInfo' will cause issues here -->
30-
7. Deploy the server with the legacy and migration dependencies removed
31+
7. Once the updated app is available for users, deploy the backed and force the client to upgrade.
32+
8. Remove the `serverpod_auth_migration` and `serverpod_auth` module from the server.
33+
9. Deploy the server with the legacy and migration dependencies removed.
3134
For any migrated entities, drop the `int` user ID column and make the new `UUID` auth user ID required
32-
<!-- TODO: Probably the `int` column needs to be kept around (and the other stay optional),
33-
in order to support old clients adding new data? -->
34-
8. Once the backwards compatibility is not needed anymore (because all passwords have been imported and the legacy session are not in use anymore), drop the dependency on `serverpod_auth_backwards_compatibility`
35+
10. Once the backwards compatibility is not needed anymore (because all passwords and sessions have been fully imported into the new module), drop the dependency on `serverpod_auth_backwards_compatibility`
36+
37+
Since the actual data migration in step 5 should only take a few seconds to complete in most cases, it is recommended to already build out the "next" release beforehand, so that you can directly switch to the post-migration version of your backend.
38+
39+
### Detailed Steps
40+
41+
#### Add new authentication modules
3542

36-
Since the actual data migration in step 5 should only take a few seconds to complete in most cases, it is recommended to already build out the "next" release beforehand, so that you can directly switch to the post-migration version of your backend. This then also ensures the final version works properly and that no further entities are created against in `serverpod_auth`, which would then not be migrated anymore.
43+
Add all desired authentication packages as described [here](../concepts/authentication/setup_new#email). The general flow is always the same:
3744

38-
## Sessions
45+
- Add the dependency
46+
- Configure the package
47+
- Subclass the endpoint in the application’s code to get it exposed
48+
- Make use of the new endpoint from the client
3949

40-
In order to support both sessions created by the legacy `serverpod_auth` and through the new `serverpod_auth_session` package, the `authenticationHandler` has to be updated to try both of them.
41-
Since an invalid/unknown session key just yields a `null` result, they can be chained like this:
50+
#### Set up the one-time migration
51+
52+
In order to run the migration once with the next deployment of the sever, add a dependency on `serverpod_auth_migration_server` and modify the `run` method as follows:
4253

4354
```dart
4455
import 'package:serverpod_auth_migration_server/serverpod_auth_migration_server.dart' show AuthMigrations;
4556
57+
58+
59+
void run(final List<String> args) async {
60+
61+
// Start the server.
62+
await pod.start();
63+
64+
// This is how a "one stop" migration could look like
65+
await AuthMigrations.migrateUsers(
66+
await pod.createSession(),
67+
userMigration: (
68+
final session, {
69+
required final newAuthUserId,
70+
required final oldUserId,
71+
final transaction,
72+
}) async {
73+
// Run any custom migration updating the mapping from old to new IDs here.
74+
// Be sure to run the migration in the `transaction`, so a failure can be fully reverted.
75+
print('Migrated account $newAuthUserId');
76+
},
77+
);
78+
}
79+
```
4680

47-
final pod = Serverpod(
48-
args,
49-
Protocol(),
50-
Endpoints(),
51-
authenticationHandler: AuthMigrations.authenticationHandler
52-
);
81+
The `userMigration` parameter shown above is also the place where you should migrate all references to a legacy `UserInfo` `int` id to the new `AuthUser` `UUID` id.
5382

83+
A possible way to upgrade is to add an optional relation to the `AuthUser` (`authUser: module:auth_user:AuthUser?, relation(optional)`) to every entity which currently references `UserInfo` or the user ID. (Beware that you have to configure the `serverpod_auth_user` module in your `config/generator.yaml` for the code generator to find it.)
5484

55-
// …
85+
Inside the callback then find all entities for the currently migrated user and set the `authUserId` for them.
5686

57-
AuthMigrations.config = AuthMigrationConfig(
58-
userMigrationHook: (
59-
final session, {
60-
required final newAuthUserId,
61-
required final oldUserId,
62-
final transaction,
63-
}) async {
64-
// Run any custom migration updating the mapping from old to new IDs here.
65-
// ignore: avoid_print
66-
print('Migrated account $newAuthUserId');
67-
},
68-
);
87+
Later in step 9, once the migration has completed, drop the field relating to the legacy `UserInfo` and make the `AuthUser` one required (non-optional) where possible.
6988

70-
// Start the server.
71-
await pod.start();
89+
#### Switch to the new authentication handler
7290

73-
// This is how a "one stop" migration could look like
91+
By default all new auth packages use database-backed sessions from `serverpod_auth_session`.
7492

75-
await AuthMigrations.migrateUsers(
76-
await pod.createSession(),
77-
customUserMigration: (
78-
final session, {
79-
required final newAuthUserId,
80-
required final oldUserId,
81-
required final transaction,
93+
Replace the `authenticationHandler` in the `Serverpod` instance with the new one like this:
94+
95+
```dart
96+
import 'package:serverpod_auth_email_server/serverpod_auth_email_server.dart'
97+
show AuthSessions;
98+
99+
void run(final List<String> args) async {
100+
final pod = Serverpod(
101+
args,
102+
Protocol(),
103+
Endpoints(),
104+
authenticationHandler: AuthSessions.authenticationHandler,
105+
);
106+
107+
108+
}
109+
```
110+
111+
#### Add backwards compatibility to be able to import legacy sessions and passwords
112+
113+
The migration package stores all legacy sessions and password mapped to the new user IDs in a transitional table. But since we can only fully migrate the passwords once the clients send the clear-text one upon login and migrate sessions on a “per use” basis on demand from the client, we need to add the `serverpod_auth_backwards_compatibility_server` module to the server.
114+
115+
This will automatically expose a new endpoint where updated clients can exchange their legacy session for a new one backed by the `serverpod_auth_session` module.
116+
117+
In order to support importing passwords set in the legacy module into the new `serverpod_auth_email_account` one you have to update your email account endpoint subclass to see whether the password can be imported like this:
118+
119+
```dart
120+
import 'package:serverpod/serverpod.dart';
121+
import 'package:serverpod_auth_backwards_compatibility_server/serverpod_auth_backwards_compatibility_server.dart';
122+
import 'package:serverpod_auth_email_server/serverpod_auth_email_server.dart'
123+
as email_account;
124+
125+
/// Endpoint for email-based authentication which imports the legacy passwords.
126+
class PasswordImportingEmailAccountEndpoint extends email_account.EmailAccountEndpoint {
127+
/// Logs in the user and returns a new session.
128+
///
129+
/// In case an expected error occurs, this throws a `EmailAccountLoginException`.
130+
@override
131+
Future<email_account.AuthSuccess> login(
132+
final Session session, {
133+
required final String email,
134+
required final String password,
135+
}) async {
136+
await AuthBackwardsCompatibility.importLegacyPasswordIfNeeded(
137+
session,
138+
email: email,
139+
password: password,
140+
);
141+
142+
return super.login(session, email: email, password: password);
143+
}
144+
145+
/// Starts the registration for a new user account with an email-based login associated to it.
146+
///
147+
/// Upon successful completion of this method, an email will have been
148+
/// sent to [email] with a verification link, which the user must open to complete the registration.
149+
@override
150+
Future<void> startRegistration(
151+
final Session session, {
152+
required final String email,
153+
required final String password,
82154
}) async {
83-
// Run any custom migration updating the mapping from old to new IDs here.
84-
// Be sure to run the migration in the `transaction`, so a failure can be fully reverted.
85-
print('Migrated account $newAuthUserId');
86-
},
155+
await AuthBackwardsCompatibility.importLegacyPasswordIfNeeded(
156+
session,
157+
email: email,
158+
password: password,
159+
);
160+
161+
return super.startRegistration(session, email: email, password: password);
162+
}
163+
}
164+
```
165+
166+
This checks on every login and registration request whether the email and password existed in the legacy system, and if the account does not yet have a password set in the new module migrates the previous one over.
167+
168+
#### Disable `serverpod_auth`
169+
170+
The `serverpod_auth` module can not yet be removed from the server’s source code, but nonetheless we should disable its endpoint. This will make sure that for example no new registration take place once the migration is underway.
171+
172+
❗️ TODO: Support config disabling the endpoint
173+
174+
#### Update the client’s `SessionManager`
175+
176+
The client should drop all dependencies on `serverpod_auth_client` and `serverpod_auth_shared_flutter` and instead make use of the new `SessionManager` from `serverpod_auth_session_flutter` like this:
177+
178+
```dart
179+
import 'package:serverpod_auth_session_flutter/serverpod_auth_session_flutter.dart';
180+
import 'package:serverpod_auth_backwards_compatibility_flutter/serverpod_auth_backwards_compatibility_flutter.dart';
181+
182+
// Ensure the one from `serverpod_auth_session_flutter` is used
183+
final sessionManager = SessionManager();
184+
185+
final client = Client(
186+
'http://localhost:8080/', // leave this as it's in your app
187+
authenticationKeyManager: sessionManager,
188+
);
189+
190+
await sessionManager.initAndImportLegacySessionIfNeeded(
191+
client.modules.serverpod_auth_backwards_compatibility,
192+
legacyStringGetter: null,
87193
);
88194
```
89195

90-
All high-level authentication provider packages use `serverpod_auth_session` under the hood, and only a single handler needs to be registered to support all of them.
196+
In case the app was using a custom `Storage` for its session manager, the `legacyStringGetter` would need to be set to point to the correct source. By default it will use `SharedPreferences`, where the legacy package stored the session key.
197+
198+
The `initAndImportLegacySessionIfNeeded` checks whether the session manager does not already have a session attached. If not, then it tries to obtain the previous session key from the legacy module’s storage location. In case it receives one, it’ll exchange the legacy session key for a new session on the server and set that session on the session manager. Returning a new session from the server automatically deletes it from the database, so this can only be done once.
199+
On subsequent launches, it will detect that a key is present and not attempt any further imports.
200+
201+
#### Release the app update and deploy the server
91202

92-
## User ID Migration
203+
Once (or in conjunction with, when talking about a Flutter web app) the client application is made available to consumers, deploy the backend and force the clients to update by your preferred means.
93204

94-
The `userMigrationHook` shown above is also the place where you should migraten all references to a legacy `UserInfo` id to the new `AuthUser` `UUID`.
205+
Upon first start the server will now run the migration for all entities.
95206

96-
A possible way to upgrade with this hook is to add an optional relation to the `AuthUser` (`authUser: module:auth_user:AuthUser?, relation(optional)`) to every entity which currently references `UserInfo` or the user ID.
207+
#### Remove the `serverpod_auth_migration` and `serverpod_auth` module from the server
97208

98-
Inside the hook then find all entities for the currently migration user and set the `authUserId` for them.
209+
Now the legacy and migration module can be fully removed from the server’s codebase.
99210

100-
Once the migration has completed, drop the field relating to the legacy `UserInfo` and make the `AuthUser` one required (non-optional) if possible.
211+
Furthermore the database schemas can be updated to drop the old `int` user ID columns, and instead make the new `UUID` columns required wherever the previous `int` ID was mandatory.
101212

102-
Then complete the migration by deploying the updated schema, dropping the legacy columns.
213+
#### Final deployment without the legacy tables
103214

104-
## Email passwords and social logins
215+
Generate the code & migrations, and deploy the server without the legacy modules. This will remove the no-longer-needed tables.
105216

106-
Unfortunately the migration can not migrate email passwords or social logins automatically.
217+
The previous created client is already compatible with this backend, as no further usages of legacy code path should be included.
107218

108-
The storage of passwords changed between the modules, and thus they need to be written anew in the database. Since the plain text password is only available upon login, we have to migrate them at that point if the credentials are valid in the old system and the user does not yet have a password in the new one.
219+
#### Eventual removal of `serverpod_auth_backwards_compatibility`
109220

110-
<!-- Document endpoint overwrite for login only -->
221+
As mentioned above, the legacy sessions and email authentication password get migrated upon use. Each session when the user’s client application is started on the new version, and the passwords whenever the user logs in again.
111222

112-
Similar the legacy "user identifiers" used for social logins could not be migrated as they did not store any information which provider they were from.
223+
Whenever the migration of such an entity is thus completed, the respective row gets deleted from the compatibility module’s database table. This way the progress can be monitored.
113224

114-
This is why you must keep a dependency on `serverpod_auth_migration` and keep the migration in the endpoints until all (relevant) accounts have been migrated.
225+
For the sessions is might be appropriate the drop all unused ones after for example 30 days, at which points clients are probably unlikely to update and the session can be deemed abandoned.
226+
227+
Passwords and social login “external user identifiers” should probably be kept around longer, as the whole migration was build in a way that there was no need for the user to log in again.
228+
229+
---
230+
231+
🥳 Congratulations, your server is now up to date with the latest authentication modules and best practices!
115232

116233
[^1]: Which also still works with Serverpod 3.0, if you want to continue using that.
117234

118-
[^2]: As passwords are stored in a secure fashion (hashed, salted, and peppered), they can not be directly moved from the old system to the new one. Thus the migration can only happen once a client logs in again, providing the backend with the plain-text password, at which point the migrated account can be updated.
235+
[^2]: As passwords are stored in a secure fashion (hashed, salted, and peppered), they can not be directly moved from the legacy system to the new one. Thus the migration can only happen once a client logs in again, providing the backend with the plain-text password, at which point the migrated authentication method can be updated.
236+
237+
[^3]: The remaining un-migrated data in the backwards compatibility package can be monitored by inspecting the size of the package’s tables.

0 commit comments

Comments
 (0)