Home / Guides / Transfer SQL Server logins with their SID and password
Transfer SQL Server logins with their SID and password
A backup or restore moves the database users to the new instance, but not the logins they belong to. If you create the logins again by hand, the users are orphaned and people get "Login failed". This guide copies the logins with their original SID and password hash, fixes the users that are already orphaned, and checks that both servers really match.
Short answer.
On the source instance, install Microsoft's sp_help_revlogin procedure and run it. It prints a
CREATE LOGIN ... WITH PASSWORD = 0x... HASHED, SID = 0x... statement for every login. Review that output and run it on the
destination. In PowerShell, dbatools' Copy-DbaLogin does the same job. Then fix any orphaned user with
ALTER USER ... WITH LOGIN and compare the two servers with the query further down: user-defined server roles and
server-level permissions are not copied by the script.
Why the SID and the password hash matter
A database user points at its login by SID, not by name. A SQL Server login gets a new SID every time it is created, so
CREATE LOGIN app_user WITH PASSWORD = '...' on the new server gives you the right name with the wrong SID. The user in the
restored database no longer matches any login: it is orphaned, and sign-ins fail with error 18456. Copying the login with its
original SID avoids that, and copying the password hash (instead of a new password) means nobody has to change a connection string.
Windows logins are different: their SID comes from Windows, so a domain account has the same SID on every server in the domain. The availability group guide covers the SID rules in more detail.
Method 1: Microsoft's sp_help_revlogin
Microsoft publishes this method in the article Transfer logins and passwords between instances of SQL Server (the old KB 918992 and 246133). The article holds the full script; copy it from there, because Microsoft keeps it up to date. The steps:
- On the source instance, connect as a member of sysadmin and run Microsoft's script. It creates two procedures in
master:sp_hexadecimalandsp_help_revlogin. - In SSMS, switch to Results to Text (the procedure prints the script as messages).
- Run the procedure for every login, or for one login:
USE [master];
-- every login (sa and ## system logins are left out by the procedure)
EXEC dbo.sp_help_revlogin;
-- or one login
EXEC dbo.sp_help_revlogin @login_name = N'app_user';
- Review the output (see the next section), then run it on the destination instance.
Each login comes out as one block like this (the password hash is shortened here):
/* Login [app_user] */
IF NOT EXISTS (
SELECT 1
FROM sys.server_principals
WHERE [name] = N'app_user'
)
BEGIN
CREATE LOGIN [app_user]
WITH PASSWORD = 0x030040C9...(hash shortened)... HASHED
,SID = 0x7E45CCBAF96C9042BC40B91FC3DA9054
,DEFAULT_DATABASE = [master]
,DEFAULT_LANGUAGE = [us_english]
,CHECK_POLICY = OFF
,CHECK_EXPIRATION = OFF
END
Treat the output like a password file: it holds every SQL login's password hash. Do not leave it in a ticket, a share or a repository.
Read the output before you run it
- It includes logins you may not want. In our test it also scripted the instance's own administrator login and an
NT SERVICEaccount. Delete the blocks you do not want to create on the destination. - An existing login is skipped without a message. Every block starts with
IF NOT EXISTS. If the destination already has a login with that name but a different SID (someone created it by hand), the script leaves it alone and the database user stays orphaned. Our verify query below finds these. - The default database must exist. The current script writes
DEFAULT_DATABASEinto each statement (Microsoft's article says the default database is not transferred, but the script published now does write it). If that database is not on the destination yet, that oneCREATE LOGINfails with Msg 15010 and the login is not created. Restore the databases first, or change it to[master]and set it later withALTER LOGIN ... WITH DEFAULT_DATABASE. The script is safe to run again: logins that already exist are skipped. - Name or SID clashes. Msg 15025 means the name is already there; Msg 15433 ("Supplied parameter sid is in use") means another login on the destination already has that SID. Microsoft's article says to resolve these by hand.
- Different domains or collations. Windows logins from another domain get a different SID, so their users are orphaned. A case-sensitive destination can reject passwords that worked on a case-insensitive source. Both are covered in Microsoft's article.
What the script copies, and what it does not
We ran the script against a test instance and compared the result with the source. This is what arrived:
| Item | Copied by sp_help_revlogin? |
|---|---|
| SID and password hash of SQL logins | Yes. The login signed in on the destination with the source password. |
| Password policy and expiration (CHECK_POLICY, CHECK_EXPIRATION) | Yes |
| Disabled logins | Yes, they stay disabled |
| Default database and language | Written into the script; the database must already exist (see above) |
| Fixed server roles (sysadmin, dbcreator and the others) | Yes, through sp_addsrvrolemember |
| User-defined server roles and their members | No. Create the role and add the members yourself. |
| Server-level permissions (for example VIEW SERVER STATE) | No. Grant them again on the destination. |
| Windows logins and groups | Created by name (FROM WINDOWS); the SID comes from Windows |
| Database users, roles and permissions | Not by the script: they live inside each database and arrive with the backup |
| Credentials mapped to logins | No; the script does not include them |
Version caveats: which password hash you have
CREATE LOGIN ... HASHED stores the hash exactly as it was. Which servers can then check a password against it depends on
the hash format, and the first bytes of the hash tell you which one you have:
USE [master];
SELECT name, LEFT(CONVERT(varchar(10), password_hash, 1), 6) AS hash_version
FROM sys.sql_logins
WHERE name NOT LIKE '##%'
ORDER BY name;
| Source | Hash | What it means for the copy |
|---|---|---|
| SQL Server 2000 to 2008 R2 | SHA-1 | Out of support. Microsoft notes you meet these only when migrating away from old instances. |
| SQL Server 2012 to 2022 | SHA-512, starts with 0x0200 | The usual case. |
| SQL Server 2025, or 2022 CU12 and later with trace flag 4671 | PBKDF2 (SHA-512, 100,000 iterations), starts with 0x0300 | Microsoft: earlier versions that do not support these "version 3" hashes cannot authenticate the login. Copying to an older instance means setting the password again there. |
Our test instances were SQL Server 2025 LocalDB, so every hash started with 0x0300. We did not test a copy to an older version.
Method 2: dbatools Copy-DbaLogin
If you work in PowerShell, the free dbatools module copies logins directly from one instance to another, with the original SID and
password. According to its documentation it also copies server roles, permissions and password policy settings, skips a login that
already exists unless you add -Force (which drops and recreates it), and copies logins from SQL Server 2012 or later only to
SQL Server 2012 or later. Typical calls, from the dbatools documentation:
# every login, leaving out NT SERVICE and other system logins
Copy-DbaLogin -Source SQL01 -Destination SQL02 -ExcludeSystemLogins
# only these logins
Copy-DbaLogin -Source SQL01 -Destination SQL02 -Login app_user, report_reader
We did not run dbatools for this guide; the commands and the behavior above come from its documentation (linked below). Run the verify query afterwards either way.
Fix orphaned users after the restore
Run this in each restored database on the destination. It is the query from Microsoft's orphaned-users article:
-- USE [YourDatabase];
SELECT dp.type_desc, dp.sid, dp.name AS user_name
FROM sys.database_principals AS dp
LEFT JOIN sys.server_principals AS sp
ON dp.sid = sp.sid
WHERE sp.sid IS NULL
AND dp.authentication_type_desc = 'INSTANCE';
There are two ways to fix a user it lists:
Quick fix: point the user at the login that is there
USE [YourDatabase];
ALTER USER [legacy_user] WITH LOGIN = [legacy_user];
ALTER USER ... WITH LOGIN replaces the user's SID in the database with the SID of the login on this server. It replaces the
deprecated sp_change_users_login. The catch: the login keeps its own SID and password, so the next restore from the source
orphans the user again.
Lasting fix: give the login the source SID
Drop the login on the destination, run sp_help_revlogin for that one login on the source, run its output on the
destination, and run the ALTER USER above once more. After that, the login has the same SID and password as on the source,
and later restores need no fix. DROP LOGIN refuses a login that is connected or that owns a server-level object or an
Agent job, so check that first.
-- 1. On the DESTINATION
USE [master];
DROP LOGIN [legacy_user];
-- 2. On the SOURCE: script that one login, then run its output on the destination
USE [master];
EXEC dbo.sp_help_revlogin @login_name = N'legacy_user';
-- 3. On the DESTINATION, in each database that has the user
USE [YourDatabase];
ALTER USER [legacy_user] WITH LOGIN = [legacy_user];
Verify after the copy: compare SIDs and hashes
Run this on both servers. It lists each login with its SID, a fingerprint of the password hash (a SHA-256 of the hash, so the hash itself is never shown), and the settings, server roles and server permissions that most often differ:
USE [master];
SELECT p.name,
p.type_desc,
CONVERT(varchar(200), p.sid, 1) AS sid,
CONVERT(varchar(66), HASHBYTES('SHA2_256', l.password_hash), 1) AS password_fingerprint,
p.is_disabled,
p.default_database_name,
l.is_policy_checked,
l.is_expiration_checked,
ISNULL(STUFF((SELECT ', ' + r.name
FROM sys.server_role_members AS rm
JOIN sys.server_principals AS r ON r.principal_id = rm.role_principal_id
WHERE rm.member_principal_id = p.principal_id
ORDER BY r.name
FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, ''), '') AS server_roles,
ISNULL(STUFF((SELECT ', ' + sp.state_desc + ' ' + sp.permission_name
FROM sys.server_permissions AS sp
WHERE sp.grantee_principal_id = p.principal_id
AND NOT (sp.permission_name = 'CONNECT SQL' AND sp.state = 'G')
ORDER BY sp.permission_name
FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, ''), '') AS server_permissions
FROM sys.server_principals AS p
LEFT JOIN sys.sql_logins AS l ON l.principal_id = p.principal_id
WHERE p.type IN ('S', 'U', 'G')
AND p.name NOT LIKE '##%'
AND p.name NOT LIKE 'NT SERVICE\%'
AND p.name NOT LIKE 'NT AUTHORITY\%'
AND p.name <> 'sa'
ORDER BY p.name;
Two logins with the same fingerprint have identical hashes, so the same password. To compare two servers in one step, save the query as
login-fingerprint.sql and run this PowerShell script (Windows authentication; it only reads):
param(
[Parameter(Mandatory)] [string]$Source,
[Parameter(Mandatory)] [string]$Target,
[string]$QueryFile = '.\login-fingerprint.sql'
)
$ErrorActionPreference = 'Stop'
$query = Get-Content -Raw $QueryFile
function Get-LoginRows([string]$server) {
$cn = New-Object System.Data.SqlClient.SqlConnection "Server=$server;Database=master;Integrated Security=True"
$cn.Open()
try {
$da = New-Object System.Data.SqlClient.SqlDataAdapter ($query, $cn)
$t = New-Object System.Data.DataTable
[void]$da.Fill($t)
$t.Rows | ForEach-Object {
[pscustomobject]@{ Login = $_.name; Line = ($_.ItemArray -join ' | ') }
}
} finally { $cn.Close() }
}
$a = Get-LoginRows $Source
$b = Get-LoginRows $Target
Compare-Object $a $b -Property Line -PassThru |
Sort-Object Login, SideIndicator |
Select-Object @{n='Server'; e={ if ($_.SideIndicator -eq '<=') { $Source } else { $Target } }}, Line |
Format-Table -AutoSize -Wrap
No output means the logins match. In our test, before the fixes, it showed exactly the gaps described above: a login created by hand
with a different SID and fingerprint, and a login whose user-defined server role and VIEW SERVER STATE permission had not
been copied (server names replaced and fingerprints shortened here).
Server Line
------ ----
SQL01 legacy_user | SQL_LOGIN | 0x7C65F636E8FE7E4FAFF8D5AFB848AE53 | 0x22F5A33A... | False | master | False | False | |
SQL02 legacy_user | SQL_LOGIN | 0x4A9915B64ED2B94ABF53AF28F1A5699C | 0xAFA24562... | False | master | False | False | |
SQL01 ops_admin | SQL_LOGIN | 0x342196AD6ACBA44B8D51001D737E7EAF | 0xBA029790... | False | master | False | False | dbcreator, guide_monitor | GRANT VIEW SERVER STATE
SQL02 ops_admin | SQL_LOGIN | 0x342196AD6ACBA44B8D51001D737E7EAF | 0xBA029790... | False | master | False | False | dbcreator |
Keeping them the same afterwards
A transfer is a one-off. After it, a password changed on one server, a new login, or a role granted on one side puts the servers out of
step again, and you find out at the next restore or failover. You can rerun the verify script on a schedule, rerun
Copy-DbaLogin, or use a tool that keeps watching.
Questions
What is sp_help_revlogin?
A stored procedure that Microsoft publishes (formerly KB 918992). You create it in master on the source instance, and it prints a CREATE LOGIN statement for every login with the original SID and password hash, which you then run on the other instance.
Why do users get "Login failed" after I restore a database on another server?
The database users came with the backup, but the logins they belong to are missing on the new server or have a different SID. Those users are orphaned. Copy the logins with their SID, or remap each user with ALTER USER ... WITH LOGIN.
Does sp_help_revlogin copy server roles and permissions?
It adds fixed server roles such as sysadmin and dbcreator. In our test it did not copy membership of a user-defined server role or server-level permissions such as VIEW SERVER STATE, so grant those again on the destination.
Should I still use sp_change_users_login to fix orphaned users?
No. Microsoft lists sp_change_users_login as deprecated. Use ALTER USER user_name WITH LOGIN = login_name instead.
Can I copy logins from SQL Server 2025 to an older version?
SQL Server 2025 hashes new and changed SQL login passwords with PBKDF2, a "version 3" hash that starts with 0x0300. Microsoft states that versions that do not support these hashes cannot authenticate such logins, so set the password again on the older instance.
Does SQLParity copy logins?
It compares logins across a group of servers, including SIDs and password hashes, and can create a missing login with the source SID and password hash after you review the plan and click Go. A login with the same name but a different SID is reported as a conflict for you to resolve.
Related
- Sync SQL Server logins and Agent jobs across availability group replicas
- SQLParity: keep your SQL Server instances the same
- Remote SQL Server DBA services
- All guides
Sources (Microsoft Learn): Transfer logins and passwords between instances of SQL Server, Troubleshoot orphaned users, ALTER USER, CREATE LOGIN, DROP LOGIN, LOGINPROPERTY (hash versions), Trace flag 4671. dbatools: Copy-DbaLogin.