|
1 | | -# Upgrading from `serverpod_auth` (in one go) |
| 1 | +# Upgrading from `serverpod_auth` |
2 | 2 |
|
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`. |
4 | 5 |
|
5 | 6 | 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. |
6 | 7 | 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]. |
7 | 8 |
|
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] |
10 | 11 |
|
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. |
13 | 19 |
|
14 | 20 | ## General timeline |
15 | 21 |
|
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. |
17 | 23 |
|
18 | 24 | 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. |
25 | 30 | 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. |
31 | 34 | 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: |
35 | 44 |
|
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 |
37 | 49 |
|
38 | | -## Sessions |
| 50 | +#### Set up the one-time migration |
39 | 51 |
|
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: |
42 | 53 |
|
43 | 54 | ```dart |
44 | 55 | import 'package:serverpod_auth_migration_server/serverpod_auth_migration_server.dart' show AuthMigrations; |
45 | 56 |
|
| 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 | + }, |
46 | 77 |
|
47 | | -final pod = Serverpod( |
48 | | - args, |
49 | | - Protocol(), |
50 | | - Endpoints(), |
51 | | - authenticationHandler: AuthMigrations.authenticationHandler |
52 | | -); |
| 78 | + ); |
| 79 | +} |
| 80 | +``` |
53 | 81 |
|
| 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. |
54 | 83 |
|
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.) |
56 | 85 |
|
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 |
69 | 91 |
|
70 | | -// Start the server. |
71 | | -await pod.start(); |
| 92 | +By default all new auth packages use database-backed sessions from `serverpod_auth_session`. |
72 | 93 |
|
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 | +``` |
74 | 111 |
|
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, |
82 | 137 | }) 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, |
87 | 195 | ); |
88 | 196 | ``` |
89 | 197 |
|
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. |
91 | 202 |
|
92 | | -## User ID Migration |
| 203 | +#### Release the app update and deploy the server |
93 | 204 |
|
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. |
95 | 206 |
|
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. |
97 | 208 |
|
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 |
99 | 210 |
|
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. |
101 | 212 |
|
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. |
103 | 214 |
|
104 | | -## Email passwords and social logins |
| 215 | +#### Final deployment without the legacy tables |
105 | 216 |
|
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. |
107 | 218 |
|
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. |
109 | 220 |
|
110 | | -<!-- Document endpoint overwrite for login only --> |
| 221 | +#### Eventual removal of `serverpod_auth_backwards_compatibility` |
111 | 222 |
|
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. |
113 | 224 |
|
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! |
115 | 234 |
|
116 | 235 | [^1]: Which also still works with Serverpod 3.0, if you want to continue using that. |
117 | 236 |
|
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