└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # supabase-user-password-migration 2 | How to migrate user passwords from an external authentication system to Supabase 3 | 4 | ## Scenario 5 | - You've migrated users from another platform to Supabase (GoTrue) 6 | - These users were authenticated using an email + password combination in the previous platform 7 | - You'd like to authenticate these users in your Supabase application **without**: 8 | - any additional steps on the part of the user 9 | - the user even needing to know their account has been migrated (a seamless transition) 10 | 11 | ## Plain-text passwords scenario 12 | If your prior platform did not hash the passwords and stored them as plain-text (which is highly unlikely) then this is a pretty easy task. In this case, you have access to the users' passwords and you can create the new user accounts in Supabase with the passwords intact. Simply use the [createUser()](https://supabase.com/docs/reference/javascript/auth-api-createuser) function when creating your users, passing in the user's password. 13 | 14 | ## Hashed passwords with the same hashing algorithm 15 | It's far more likely that your prior platform hashed the user passwords. If the hashing algorithm used by your prior platform was exactly the same as the Supabase (GoTrue) algorithm, you could move the passwords over by writing the hashed passwords directly in your `auth.users` table in Supabase. This, however, is also highly unlikely. 16 | 17 | ## Hashed passwords with two different algorithms 18 | Most-likely, your prior platform hashed the user passwords using a different algorithm than Supabase (GoTrue) uses. In this (most common) case, you should migrate your users to Supabase with a random password, but also store the **old password hash** in your Supabase database as a temporary step. Users who have an **old password hash** in their user records are considered `not yet migrated`. Once the password has been migrated to the Supabase (GoTrue) format, you will delete the **old password hash** and the user will be considered `migrated`. 19 | 20 | ### The workflow for a single user 21 | 1. The user was migrated from platform A to Supabase, `old_password_hash` as stored as 'XXXX-XXXX-XXXX' 22 | 2. After some time, the user tries to sign into your Supabase application 23 | 3. Your application sign in screen gathers the email address and password from the user 24 | 4. Your middleware function checks to see if the user is `migrated` (meaning, the password has been migrated) 25 | - If YES: the user is passed on to the normal Supabase [signIn()](https://supabase.com/docs/reference/javascript/auth-signin) flow 26 | - If NO: then your middleware function validates the old password hashing algorithm against the password entered by the user to see if it matches the `old_password_hash` 27 | 5. **validate password function** 28 | - If it DOES NOT MATCH: the sign in is rejected 29 | - If it MATCHES: 30 | - The current password entered by the user is written to Supabase (GoTrue) [See update()](https://supabase.com/docs/reference/javascript/auth-update#update-password-for-authenticated-user) 31 | - The **old password hash** field is removed from the user, thus indicated that this user's password is now `migrated` 32 | 33 | #### Where to store the **old password hash** 34 | You can store the **old password hash** in any of: 35 | - A simple PostgreSQL table (email text primary key, password_hash text) 36 | - The [user's metadata](https://supabase.com/docs/reference/javascript/auth-update#update-a-users-metadata) (note: this is accessible / readable by the user by default and is stored in `auth.users.raw_user_meta_data`) 37 | - User's app metadata (`auth.users.raw_app_meta_data`) (note: this field is not accessible / readable by the user) 38 | 39 | ## Requirements 40 | In order to create this seamless password migration system, you will need the following: 41 | - user accounts converted to Supabase (GoTrue) 42 | - a stored (temporary) `old_password_hash` field containing the user's password hash from the prior platform 43 | - knowledge of the algorithm used to hash the passwords on the prior platform 44 | - a **validation function** to validate the password against the `old_password_hash` 45 | - a middleware tier (this can be anywhere -- Supabase Edge Functions (Deno), a hosted NodeJS application, or any server-based function tier) 46 | 47 | ### The Validation Function 48 | This function should be created and tested separately with a known account that contains a password you know. All this function needs to do is to: 49 | - accept a password (entered by the user) and the `old_password_hash` for the account 50 | - return **TRUE** if the password hash matches the `old_password_hash` 51 | - return **FALSE** if the password hash does not match the `old_password_hash` 52 | 53 | You'll need knowledge of the old hashing algorithm in order to make this validation function work. 54 | 55 | ## Example Implementation 56 | Let's say I've migrated all my users to Supabase, and I've created a table called `old_password_hashes` with a record for each user. Now, as an un-migrated user I sign in for the first time. I enter my email address and password in the app's **Sign In** screen. 57 | 58 | 1. `email` and `password` are sent to the middle tier: 59 | 2. the middle tier looks up the user in the `old_password_hashes` table 60 | - NOT FOUND? then send `email` and `password` to the standard `signIn()` function (see **note** below) 61 | - FOUND? then send send `password` and `old_password_hash` to the **password validation function** 62 | 3. **password validation function** 63 | - FAILED? reject the sign in 64 | - SUCCESS? store the new password and sign the user in 65 | - call `update()` to update the user's password in Supabase 66 | - delete the corresponding `old_password_hash` record for this user 67 | - call `signIn()` to sign the user in (see **note** below) 68 | 69 | **Note:** For this last step, signing the user in, this is a client-side function. Since all of this processing has been done on the server side, you'll want to return control back to the client side here and let the client side call `signIn()` directly. So your middleware function should simply return TRUE or FALSE back to your client application. FALSE means the sign in failed (missing user or bad password). TRUE means the user's password was either already migrated, or it was just successfuly migrated (it doesn't matter). If the result is TRUE, we call `signIn()` from the client to complete the sign in process. 70 | 71 | --------------------------------------------------------------------------------