Skip to content
Notifications
Clear all

Step-by-step: How to migrate users and permissions without re-creating everything

1 Posts
1 Users
0 Reactions
0 Views
(@danag)
Estimable Member
Joined: 1 week ago
Posts: 89
Topic starter   [#4651]

Hey folks! I recently helped a team migrate their user and permission system from a custom in-house solution to Auth0, and the biggest win was doing it without forcing a re-registration or losing existing relationships. It's totally possible if you plan the data mapping carefully.

The core idea is to treat your legacy user IDs as the source of truth during migration. We kept the old database running in read-only mode alongside the new Auth0 tenant, and wrote a sync script that created users in Auth0 using the `import_hash` for passwords and custom `user_metadata` to store the original internal user ID. This meant zero password resets for users.

Here's the simplified Python script we used for the initial bulk import. It reads from our old PostgreSQL `users` table and creates them in Auth0 via the Management API.

```python
import asyncio
from auth0.authentication import GetToken
from auth0.management import Auth0
import asyncpg

async def migrate_users():
# 1. Connect to legacy DB
old_db = await asyncpg.connect(DATABASE_URL)
users = await old_db.fetch("SELECT id, email, password_hash FROM users WHERE active = true")

# 2. Auth0 Management API setup
token = GetToken(AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET)
creds = token.client_credentials(audience=f'https://{AUTH0_DOMAIN}/api/v2/')
mgmt_api = Auth0(AUTH0_DOMAIN, creds['access_token'])

# 3. Create each user in Auth0, preserving the old ID
for user in users:
mgmt_api.users.create({
'email': user['email'],
'password': 'dummy', # Required but ignored due to import_hash
'connection': 'Username-Password-Authentication',
'email_verified': True,
'user_metadata': {'legacy_user_id': user['id']},
'import_hash': user['password_hash'] # The key to keeping passwords
})
```

The cutover was a two-step process. First, we ran this script during low-traffic hours. Then, we updated our FastAPI application to check Auth0 first, and fall back to the old database for any users not yet synced (though there shouldn't be any). After a week of monitoring, we decommissioned the old auth logic. The whole transition, including testing, took about three sprints, with the actual cutover weekend being surprisingly smooth.

The permission mapping was simpler—we stored role names in Auth0's `app_metadata`, so our backend could still authorize based on familiar roles. The biggest lesson? Test the password migration flow *twice* in a staging tenant with a copy of production data. We caught a few encoding issues with older password hashes that way.

Hope this gives you a solid starting point! ~d



   
Quote