Migration strategy for email/password users from another auth system #638

Closed
opened 2026-03-13 07:58:04 -05:00 by GiteaMirror · 7 comments
Owner

Originally created by @WonderPanda on GitHub (Feb 7, 2025).

Is this suited for github?

  • Yes, this is suited for github

We're evaluating better-auth as a replacement for our current Auth system which is currently running on SuperTokens. better-auth looks amazing and we'd be very interested in being able to drop an additional networked dependency from our stack.

SuperTokens uses a different approach to hashing passwords though, so migrating the records directly into the user/account tables in the better-auth schema doesn't currently work.

Describe the solution you'd like

It would be great if there were some kind of strategy that could be provided when configuring better-auth's email/password to have a fallback method for attempting to compare the password hash.

Ideally, once the fallback method is used to correctly validate a username and password the field in the DB could then be updated to the expected format for better-auth

Describe alternatives you've considered

Without something like this in place we wouldn't be able to migrate all users in one go. We'd have to introduce a bunch of custom code in our backend that allows both auth systems to exist side by side and then every time a user signs in we could insert records into the better-auth schema to slowly backfill accounts. This relies on all users logging in at least once before we could safely deprecate SuperTokens from our stack.

Additional context

No response

Originally created by @WonderPanda on GitHub (Feb 7, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. We're evaluating better-auth as a replacement for our current Auth system which is currently running on SuperTokens. better-auth looks amazing and we'd be very interested in being able to drop an additional networked dependency from our stack. SuperTokens uses a different approach to hashing passwords though, so migrating the records directly into the user/account tables in the better-auth schema doesn't currently work. ### Describe the solution you'd like It would be great if there were some kind of strategy that could be provided when configuring better-auth's email/password to have a fallback method for attempting to compare the password hash. Ideally, once the fallback method is used to correctly validate a username and password the field in the DB could then be updated to the expected format for better-auth ### Describe alternatives you've considered Without something like this in place we wouldn't be able to migrate all users in one go. We'd have to introduce a bunch of custom code in our backend that allows both auth systems to exist side by side and then every time a user signs in we could insert records into the better-auth schema to slowly backfill accounts. This relies on all users logging in at least once before we could safely deprecate SuperTokens from our stack. ### Additional context _No response_
Author
Owner

@WonderPanda commented on GitHub (Feb 7, 2025):

Oh I just realized that there is some support for this. I missed it in my first pass at https://www.better-auth.com/docs/authentication/email-password#configuration

This will probably unblock us but I'd love if there were additional configuration here to provide a migration strategy such that the custom hashing is only used when the default one falls back and could then update DB records to convert the hash to an expected format so eventually all records will match the better-auth built in hashing strategy

@WonderPanda commented on GitHub (Feb 7, 2025): Oh I just realized that there is some support for this. I missed it in my first pass at https://www.better-auth.com/docs/authentication/email-password#configuration This will probably unblock us but I'd love if there were additional configuration here to provide a migration strategy such that the custom hashing is only used when the default one falls back and could then update DB records to convert the hash to an expected format so eventually all records will match the better-auth built in hashing strategy
Author
Owner

@j-fdion commented on GitHub (Feb 8, 2025):

I'm also really interested in this. I'm currently migrating an app from lucia auth. While I've also devised a strategy to migrate the users, I am curious what others can come up with :)

@j-fdion commented on GitHub (Feb 8, 2025): I'm also really interested in this. I'm currently migrating an app from lucia auth. While I've also devised a strategy to migrate the users, I am curious what others can come up with :)
Author
Owner

@cbodin commented on GitHub (Feb 13, 2025):

Taking inspiration from .NET and PHP something like this could be pretty straight forward to use:

type PasswordVerifier = (data: {
  hash: string;
  password: string;
}) => Promise<boolean | "success" | "success-rehash-needed" | "failed">;

Example usage:

verify: async ({ hash, password }) => {
  // Using imported bcrypt password
  if (hash.startsWith('$2a$')) {
   const verified = await bcrypt.compare(password, hash);
   return verified ? "success-rehash-needed" : "failed";
  }

  // Using verifier from better-auth somehow
  return await verifyPassword({ hash, password });
}

Returning success-rehash-needed would allow the library to call the hash function (the default if not overridden) and update the database table. This would also allow the built-in verifier to return success-rehash-needed if passwords were ever to be migrated to argon2id in the future.

Keeping the boolean as a valid return value retains api compatibility with version 1.x.

@cbodin commented on GitHub (Feb 13, 2025): Taking inspiration from [.NET](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasswordhasher-1.verifyhashedpassword?view=aspnetcore-9.0) and [PHP](https://www.php.net/manual/en/function.password-needs-rehash.php) something like this could be pretty straight forward to use: ```ts type PasswordVerifier = (data: { hash: string; password: string; }) => Promise<boolean | "success" | "success-rehash-needed" | "failed">; ``` Example usage: ```ts verify: async ({ hash, password }) => { // Using imported bcrypt password if (hash.startsWith('$2a$')) { const verified = await bcrypt.compare(password, hash); return verified ? "success-rehash-needed" : "failed"; } // Using verifier from better-auth somehow return await verifyPassword({ hash, password }); } ``` Returning `success-rehash-needed` would allow the library to call the hash function (the default if not overridden) and update the database table. This would also allow the built-in verifier to return `success-rehash-needed` if passwords were ever to be migrated to argon2id in the future. Keeping the boolean as a valid return value retains api compatibility with version 1.x.
Author
Owner

@WonderPanda commented on GitHub (Feb 14, 2025):

Taking inspiration from .NET and PHP something like this could be pretty straight forward to use:

type PasswordVerifier = (data: {
hash: string;
password: string;
}) => Promise<boolean | "success" | "success-rehash-needed" | "failed">;
Example usage:

verify: async ({ hash, password }) => {
// Using imported bcrypt password
if (hash.startsWith('$2a$')) {
const verified = await bcrypt.compare(password, hash);
return verified ? "success-rehash-needed" : "failed";
}

// Using verifier from better-auth somehow
return await verifyPassword({ hash, password });
}
Returning success-rehash-needed would allow the library to call the hash function (the default if not overridden) and update the database table. This would also allow the built-in verifier to return success-rehash-needed if passwords were ever to be migrated to argon2id in the future.

Keeping the boolean as a valid return value retains api compatibility with version 1.x.

Nice this is exactly the kind of API I was envisioning in my head as well

@WonderPanda commented on GitHub (Feb 14, 2025): > Taking inspiration from [.NET](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasswordhasher-1.verifyhashedpassword?view=aspnetcore-9.0) and [PHP](https://www.php.net/manual/en/function.password-needs-rehash.php) something like this could be pretty straight forward to use: > > type PasswordVerifier = (data: { > hash: string; > password: string; > }) => Promise<boolean | "success" | "success-rehash-needed" | "failed">; > Example usage: > > verify: async ({ hash, password }) => { > // Using imported bcrypt password > if (hash.startsWith('$2a$')) { > const verified = await bcrypt.compare(password, hash); > return verified ? "success-rehash-needed" : "failed"; > } > > // Using verifier from better-auth somehow > return await verifyPassword({ hash, password }); > } > Returning `success-rehash-needed` would allow the library to call the hash function (the default if not overridden) and update the database table. This would also allow the built-in verifier to return `success-rehash-needed` if passwords were ever to be migrated to argon2id in the future. > > Keeping the boolean as a valid return value retains api compatibility with version 1.x. Nice this is exactly the kind of API I was envisioning in my head as well
Author
Owner

@j-fdion commented on GitHub (Feb 16, 2025):

Taking inspiration from .NET and PHP something like this could be pretty straight forward to use:

type PasswordVerifier = (data: {
hash: string;
password: string;
}) => Promise<boolean | "success" | "success-rehash-needed" | "failed">;
Example usage:

verify: async ({ hash, password }) => {
// Using imported bcrypt password
if (hash.startsWith('$2a$')) {
const verified = await bcrypt.compare(password, hash);
return verified ? "success-rehash-needed" : "failed";
}

// Using verifier from better-auth somehow
return await verifyPassword({ hash, password });
}
Returning success-rehash-needed would allow the library to call the hash function (the default if not overridden) and update the database table. This would also allow the built-in verifier to return success-rehash-needed if passwords were ever to be migrated to argon2id in the future.

Keeping the boolean as a valid return value retains api compatibility with version 1.x.

Thanks It gives me inspiration on how to organize my migration code :)

@j-fdion commented on GitHub (Feb 16, 2025): > Taking inspiration from [.NET](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasswordhasher-1.verifyhashedpassword?view=aspnetcore-9.0) and [PHP](https://www.php.net/manual/en/function.password-needs-rehash.php) something like this could be pretty straight forward to use: > > type PasswordVerifier = (data: { > hash: string; > password: string; > }) => Promise<boolean | "success" | "success-rehash-needed" | "failed">; > Example usage: > > verify: async ({ hash, password }) => { > // Using imported bcrypt password > if (hash.startsWith('$2a$')) { > const verified = await bcrypt.compare(password, hash); > return verified ? "success-rehash-needed" : "failed"; > } > > // Using verifier from better-auth somehow > return await verifyPassword({ hash, password }); > } > Returning `success-rehash-needed` would allow the library to call the hash function (the default if not overridden) and update the database table. This would also allow the built-in verifier to return `success-rehash-needed` if passwords were ever to be migrated to argon2id in the future. > > Keeping the boolean as a valid return value retains api compatibility with version 1.x. Thanks It gives me inspiration on how to organize my migration code :)
Author
Owner

@dosubot[bot] commented on GitHub (Jun 15, 2025):

Hi, @WonderPanda. I'm Dosu, and I'm helping the better-auth team manage their backlog. I'm marking this issue as stale.

Issue Summary:

  • The issue involved migrating users from SuperTokens to better-auth due to differing password hashing methods.
  • You initially overlooked existing support but suggested enhancing it for seamless migration.
  • @cbodin proposed a PasswordVerifier function inspired by .NET and PHP, which was well-received.
  • The proposal aligns with your envisioned solution and has been effectively implemented.

Next Steps:

  • Please confirm if this issue is still relevant to the latest version of the better-auth repository by commenting here.
  • If no further updates are provided, the issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

@dosubot[bot] commented on GitHub (Jun 15, 2025): Hi, @WonderPanda. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog. I'm marking this issue as stale. **Issue Summary:** - The issue involved migrating users from SuperTokens to better-auth due to differing password hashing methods. - You initially overlooked existing support but suggested enhancing it for seamless migration. - @cbodin proposed a `PasswordVerifier` function inspired by .NET and PHP, which was well-received. - The proposal aligns with your envisioned solution and has been effectively implemented. **Next Steps:** - Please confirm if this issue is still relevant to the latest version of the better-auth repository by commenting here. - If no further updates are provided, the issue will be automatically closed in 7 days. Thank you for your understanding and contribution!
Author
Owner

@arenddeboer commented on GitHub (Jun 16, 2025):

Even though this is now closed, I think better migration tooling can help adoption. Especially enterprise customers who will likely also be the ones who will need premium subscriptions when they become available.

@arenddeboer commented on GitHub (Jun 16, 2025): Even though this is now closed, I think better migration tooling can help adoption. Especially enterprise customers who will likely also be the ones who will need premium subscriptions when they become available.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#638