Why Migrating SQL Server Logins Is Harder Than It Looks
Moving a database from one SQL Server instance to another is rarely as simple as detaching and reattaching files. The databases themselves transfer cleanly enough, but the logins are a different story. SQL Server logins are stored in the master database with password hashes and security identifiers (SIDs) that don't automatically follow your databases to the new instance. Get this wrong and your application users will hit authentication errors the moment they try to connect, even though the database itself looks perfectly healthy.
The correct way to migrate SQL Server logins between instances is to use Microsoft's sp_help_revlogin stored procedure, which scripts out existing logins with their original password hashes and SIDs intact. This preserves the link between logins and database users, avoiding the orphaned user problem that catches so many administrators off guard.
What Is the Orphaned User Problem?
Every SQL Server login has a SID, a unique binary identifier assigned when the login is created. Inside each database, users are mapped to logins using that SID. When you restore a database to a new instance and then manually recreate the logins, SQL Server assigns new SIDs to those logins. The result is a mismatch: the database user points to a SID that no longer exists in the master database. The user is "orphaned."
Orphaned users cause login failures even when the username and password are correct. You can identify them with EXEC sp_change_users_login 'Report' inside the affected database, but it's far better to avoid the problem entirely by migrating logins properly in the first place.
How to Migrate SQL Server Logins Using sp_help_revlogin
Microsoft's Knowledge Base article KB918992 provides the sp_help_revlogin procedure for SQL Server 2005 and later. The process has two stages: create the procedure on the source instance, run it to generate a login script, then execute that script on the destination instance.
Step 1: Create the Helper Stored Procedure on the Source Instance
Connect to your source SQL Server and run the following in the master database. This creates the sp_hexadecimal helper procedure and the main sp_help_revlogin procedure.
USE master
GO
IF OBJECT_ID ('sp_hexadecimal') IS NOT NULL
DROP PROCEDURE sp_hexadecimal
GO
CREATE PROCEDURE sp_hexadecimal
@binvalue varbinary(256),
@hexvalue varchar (514) OUTPUT
AS
DECLARE @charvalue varchar (514)
DECLARE @i int
DECLARE @length int
DECLARE @hexstring char(16)
SELECT @charvalue = '0x'
SELECT @i = 1
SELECT @length = DATALENGTH (@binvalue)
SELECT @hexstring = '0123456789ABCDEF'
WHILE (@i <= @length)
BEGIN
DECLARE @tempint int
DECLARE @firstint int
DECLARE @secondint int
SELECT @tempint = CONVERT(int, SUBSTRING(@binvalue,@i,1))
SELECT @firstint = FLOOR(@tempint/16)
SELECT @secondint = @tempint - (@firstint*16)
SELECT @charvalue = @charvalue +
SUBSTRING(@hexstring, @firstint+1, 1) +
SUBSTRING(@hexstring, @secondint+1, 1)
SELECT @i = @i + 1
END
SELECT @hexvalue = @charvalue
GO
IF OBJECT_ID ('sp_help_revlogin') IS NOT NULL
DROP PROCEDURE sp_help_revlogin
GO
CREATE PROCEDURE sp_help_revlogin @login_name sysname = NULL AS
DECLARE @name sysname
DECLARE @type varchar (1)
DECLARE @hasaccess int
DECLARE @denylogin int
DECLARE @is_disabled int
DECLARE @PWD_varbinary varbinary (256)
DECLARE @PWD_string varchar (514)
DECLARE @SID_varbinary varbinary (85)
DECLARE @SID_string varchar (514)
DECLARE @tmpstr varchar (1024)
DECLARE @is_policy_checked varchar (3)
DECLARE @is_expiration_checked varchar (3)
DECLARE @defaultdb sysname
IF (@login_name IS NULL)
DECLARE login_curs CURSOR FOR
SELECT p.sid, p.name, p.type, p.is_disabled, p.default_database_name,
l.hasaccess, l.denylogin
FROM sys.server_principals p
LEFT JOIN sys.syslogins l ON ( l.name = p.name )
WHERE p.type IN ( 'S', 'G', 'U' ) AND p.name <> 'sa'
ELSE
DECLARE login_curs CURSOR FOR
SELECT p.sid, p.name, p.type, p.is_disabled, p.default_database_name,
l.hasaccess, l.denylogin
FROM sys.server_principals p
LEFT JOIN sys.syslogins l ON ( l.name = p.name )
WHERE p.type IN ( 'S', 'G', 'U' ) AND p.name = @login_name
OPEN login_curs
FETCH NEXT FROM login_curs
INTO @SID_varbinary, @name, @type, @is_disabled, @defaultdb, @hasaccess, @denylogin
IF (@@fetch_status = -1)
BEGIN
PRINT 'No login(s) found.'
CLOSE login_curs
DEALLOCATE login_curs
RETURN -1
END
SET @tmpstr = '/* sp_help_revlogin script '
PRINT @tmpstr
SET @tmpstr = '** Generated ' + CONVERT (varchar, GETDATE()) + ' on ' + @@SERVERNAME + ' */'
PRINT @tmpstr
PRINT ''
WHILE (@@fetch_status <> -1)
BEGIN
IF (@@fetch_status <> -2)
BEGIN
PRINT ''
SET @tmpstr = '-- Login: ' + @name
PRINT @tmpstr
IF (@type IN ( 'G', 'U'))
BEGIN
SET @tmpstr = 'CREATE LOGIN ' + QUOTENAME( @name ) + ' FROM WINDOWS WITH DEFAULT_DATABASE = [' + @defaultdb + ']'
END
ELSE BEGIN
SET @PWD_varbinary = CAST( LOGINPROPERTY( @name, 'PasswordHash' ) AS varbinary (256) )
EXEC sp_hexadecimal @PWD_varbinary, @PWD_string OUT
EXEC sp_hexadecimal @SID_varbinary, @SID_string OUT
SELECT @is_policy_checked = CASE is_policy_checked WHEN 1 THEN 'ON' WHEN 0 THEN 'OFF' ELSE 'NULL' END
FROM sys.sql_logins WHERE name = @name
SELECT @is_expiration_checked = CASE is_expiration_checked WHEN 1 THEN 'ON' WHEN 0 THEN 'OFF' ELSE 'NULL' END
FROM sys.sql_logins WHERE name = @name
SET @tmpstr = 'CREATE LOGIN ' + QUOTENAME( @name ) + ' WITH PASSWORD = ' + @PWD_string + ' HASHED, SID = ' + @SID_string + ', DEFAULT_DATABASE = [' + @defaultdb + '], CHECK_POLICY = ' + @is_policy_checked + ', CHECK_EXPIRATION = ' + @is_expiration_checked
END
IF (@denylogin = 1)
BEGIN
SET @tmpstr = @tmpstr + '; DENY CONNECT SQL TO ' + QUOTENAME( @name )
END
ELSE IF (@hasaccess = 0)
BEGIN
SET @tmpstr = @tmpstr + '; REVOKE CONNECT SQL TO ' + QUOTENAME( @name )
END
IF (@is_disabled = 1)
BEGIN
SET @tmpstr = @tmpstr + '; ALTER LOGIN ' + QUOTENAME( @name ) + ' DISABLE'
END
PRINT @tmpstr
END
FETCH NEXT FROM login_curs
INTO @SID_varbinary, @name, @type, @is_disabled, @defaultdb, @hasaccess, @denylogin
END
CLOSE login_curs
DEALLOCATE login_curs
GO
Step 2: Generate the Login Script
Once the procedure exists, execute it to produce the migration script. You can script all logins at once, or target a single login by name.
-- Script all logins
EXEC sp_help_revlogin
-- Script a single login
EXEC sp_help_revlogin 'YourLoginName'
The output will be a series of CREATE LOGIN statements with hashed passwords and explicit SIDs. Copy this output and run it on your destination SQL Server instance.
Step 3: Execute the Script on the Destination Instance
Connect to the destination instance and run the generated script against the master database. Because the SIDs are preserved, any existing database users will automatically re-link to the correct logins without needing any orphan remediation.
What About Older Versions of SQL Server?
If you're migrating from SQL Server 2000 or earlier, the sp_help_revlogin procedure won't work because the system table structure is different. Microsoft's older Knowledge Base article KB246133 covers the equivalent process for SQL Server 7.0 and 2000. The principle is identical, but the script queries the legacy sysxlogins table rather than sys.server_principals.
In practice, if you're still running SQL Server 2000 in 2024, migrating logins is the least of your concerns. That version has been out of support since 2008 and carries serious security risk. A full upgrade path assessment is well overdue.
What to Watch Out For
Migrating logins with this method is reliable, but there are a few things worth checking before and after the migration.
- Windows logins: The procedure scripts these correctly, but they'll only work on the destination server if the Windows accounts or groups exist in Active Directory. Domain-joined servers in the same environment won't have any issues. Cross-domain migrations need additional planning.
- The sa account: The procedure deliberately excludes the sa login. You'll need to handle that separately if required.
- Server-level permissions:
sp_help_revlogincaptures the login itself, but not all server-level role memberships or explicit server permissions. Reviewsys.server_role_membersandsys.server_permissionson the source instance to make sure nothing is missed. - Linked server logins: These are not captured by this procedure and need to be scripted separately.
- Default database availability: If a login's default database doesn't exist on the destination instance, the login will be created but the user may receive errors on connection. Verify all referenced databases exist before running the script.
Key Takeaways
- SQL Server logins don't migrate automatically with databases. You must explicitly script and transfer them to avoid orphaned users and authentication failures.
- Microsoft's
sp_help_revloginprocedure (KB918992) is the correct tool for migrating logins between SQL Server 2005 and later instances. It preserves password hashes and SIDs. - Preserving SIDs is critical. It's what prevents the orphaned user problem that breaks application connectivity after a migration.
- The procedure excludes sa, Windows login validation, server-level permissions, and linked server logins. Check all four manually after migration.
- SQL Server 2000 and earlier require a different script from KB246133, but any environment still running those versions needs urgent attention beyond just a login migration.
Migrating logins is one of those tasks that looks straightforward until something goes wrong at 2am after a cutover. If you're planning a SQL Server migration and want confidence that logins, permissions, and security configurations will transfer cleanly, DBA Services can help. Our SQL Server health checks include a full review of login configurations, orphaned users, and security posture across your instances, and our managed support team has handled hundreds of SQL Server migrations across Australian organisations of all sizes. Get in touch to talk through your migration requirements.
Need help with your SQL Servers?
Find out what's really going on inside your SQL Server environment.
Our health checks uncover critical misconfigurations in 97% of reviews.