Skip to content

Commit d668b62

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

File tree

2 files changed

+193
-211
lines changed

2 files changed

+193
-211
lines changed
Lines changed: 193 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,239 @@
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
42+
43+
Add all desired authentication packages as described [here](../concepts/authentication/setup_new#email). The general flow is always the same:
3544

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.
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
3749

38-
## Sessions
50+
#### Set up the one-time migration
3951

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:
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+
},
4677
47-
final pod = Serverpod(
48-
args,
49-
Protocol(),
50-
Endpoints(),
51-
authenticationHandler: AuthMigrations.authenticationHandler
52-
);
78+
);
79+
}
80+
```
5381

82+
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.
5483

55-
// …
84+
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.)
5685

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-
);
86+
Inside the callback then find all entities for the currently migrated user and set the `authUserId` for them.
87+
88+
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.
89+
90+
#### Switch to the new authentication handler
6991

70-
// Start the server.
71-
await pod.start();
92+
By default all new auth packages use database-backed sessions from `serverpod_auth_session`.
7293

73-
// This is how a "one stop" migration could look like
94+
Replace the `authenticationHandler` in the `Serverpod` instance with the new one like this:
95+
96+
```dart
97+
import 'package:serverpod_auth_email_server/serverpod_auth_email_server.dart'
98+
show AuthSessions;
99+
100+
void run(final List<String> args) async {
101+
final pod = Serverpod(
102+
args,
103+
Protocol(),
104+
Endpoints(),
105+
authenticationHandler: AuthSessions.authenticationHandler,
106+
);
107+
108+
109+
}
110+
```
74111

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,
112+
#### Add backwards compatibility to be able to import legacy sessions and passwords
113+
114+
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.
115+
116+
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.
117+
118+
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:
119+
120+
```dart
121+
import 'package:serverpod/serverpod.dart';
122+
import 'package:serverpod_auth_backwards_compatibility_server/serverpod_auth_backwards_compatibility_server.dart';
123+
import 'package:serverpod_auth_email_server/serverpod_auth_email_server.dart'
124+
as email_account;
125+
126+
/// Endpoint for email-based authentication which imports the legacy passwords.
127+
class PasswordImportingEmailAccountEndpoint
128+
extends email_account.EmailAccountEndpoint {
129+
/// Logs in the user and returns a new session.
130+
///
131+
/// In case an expected error occurs, this throws a `EmailAccountLoginException`.
132+
@override
133+
Future<email_account.AuthSuccess> login(
134+
final Session session, {
135+
required final String email,
136+
required final String password,
82137
}) 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-
},
138+
await AuthBackwardsCompatibility.importLegacyPasswordIfNeeded(
139+
session,
140+
email: email,
141+
password: password,
142+
);
143+
144+
return super.login(session, email: email, password: password);
145+
}
146+
147+
/// Starts the registration for a new user account with an email-based login associated to it.
148+
///
149+
/// Upon successful completion of this method, an email will have been
150+
/// sent to [email] with a verification link, which the user must open to complete the registration.
151+
@override
152+
Future<void> startRegistration(
153+
final Session session, {
154+
required final String email,
155+
required final String password,
156+
}) async {
157+
await AuthBackwardsCompatibility.importLegacyPasswordIfNeeded(
158+
session,
159+
email: email,
160+
password: password,
161+
);
162+
163+
return super.startRegistration(session, email: email, password: password);
164+
}
165+
}
166+
```
167+
168+
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.
169+
170+
#### Disable `serverpod_auth`
171+
172+
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.
173+
174+
❗️ TODO: Support config disabling the endpoint
175+
176+
#### Update the client’s `SessionManager`
177+
178+
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:
179+
180+
```dart
181+
import 'package:serverpod_auth_session_flutter/serverpod_auth_session_flutter.dart';
182+
import 'package:serverpod_auth_backwards_compatibility_flutter/serverpod_auth_backwards_compatibility_flutter.dart';
183+
184+
// Ensure the one from `serverpod_auth_session_flutter` is used
185+
final sessionManager = SessionManager();
186+
187+
final client = Client(
188+
'http://localhost:8080/', // leave this as it's in your app
189+
authenticationKeyManager: sessionManager,
190+
);
191+
192+
await sessionManager.initAndImportLegacySessionIfNeeded(
193+
client.modules.serverpod_auth_backwards_compatibility,
194+
legacyStringGetter: null,
87195
);
88196
```
89197

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.
198+
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.
199+
200+
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.
201+
On subsequent launches, it will detect that a key is present and not attempt any further imports.
91202

92-
## User ID Migration
203+
#### Release the app update and deploy the server
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+
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.
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+
Upon first start the server will now run the migration for all entities.
97208

98-
Inside the hook then find all entities for the currently migration user and set the `authUserId` for them.
209+
#### Remove the `serverpod_auth_migration` and `serverpod_auth` module from the server
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+
Now the legacy and migration module can be fully removed from the server’s codebase.
101212

102-
Then complete the migration by deploying the updated schema, dropping the legacy columns.
213+
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.
103214

104-
## Email passwords and social logins
215+
#### Final deployment without the legacy tables
105216

106-
Unfortunately the migration can not migrate email passwords or social logins automatically.
217+
Generate the code & migrations, and deploy the server without the legacy modules. This will remove the no-longer-needed tables.
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+
The previous created client is already compatible with this backend, as no further usages of legacy code path should be included.
109220

110-
<!-- Document endpoint overwrite for login only -->
221+
#### Eventual removal of `serverpod_auth_backwards_compatibility`
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+
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.
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+
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.
226+
227+
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.
228+
229+
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.
230+
231+
---
232+
233+
🥳 Congratulations, your server is now up to date with the latest authentication modules and best practices!
115234

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

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.
237+
[^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.
238+
239+
[^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)