使用完整的密码将用户从 Keycloak 迁移到 LDAP
Migrating users from Keycloak to LDAP with passwords intact
我们目前有一个 Keycloak 领域,我们希望将所有现有用户导出到 LDAP(他们的密码完好无损),因此我们可以为不支持 OpenID Connect 或 SAML 但支持的其他位置添加登录支持LDAP.
鉴于密码凭证使用 pbkdf2-sha256 算法存储在 Keycloak MySQL 用户数据库中,我不得不使用 OpenLDAP 2.4.47 和 contrib 模块来在 LDAP 端也支持这一点(我也相信我已经在配置中启用了这个模块。
我在使用此处描述的格式将现有的(从 Keycloak)转移到 LDAP 时遇到问题:https://github.com/hamano/openldap-pbkdf2
原谅我使用PHP,但这是我目前最熟悉的编程语言。
我尝试使用 HASH_ITERATIONS、SALT 和 VALUE 字段,以及 PHP 中 base64_encode 下评论中提供的 base64url_encode 自定义函数manual,创建一个 "Adapted Base64" 字符串,并在创建 LDAP 用户时提供构建的字符串作为 userPassword 字段。已在 LDAP 中成功创建用户,但我无法使用刚刚创建的用户的凭据对 LDAP 进行身份验证。
/*
This example migrates a single user, to be adapted to a loop later, if it works.
NOTE: KC_* constants are defined in an external config.php file, such as:
KC_LDAP_BASEDN = "ou=users,dc=sso,dc=example,dc=com"
KC_REALM_ID = "KeycloakUserRealm"
(not actual code from config.php, but you get the point)
*/
$ldc = ldap_connect(KC_LDAP_SERVER);
ldap_set_option($ldc, LDAP_OPT_PROTOCOL_VERSION, KC_LDAP_PROTOCOL); // should be 3 in config.php
$ldb = ldap_bind($ldc, KC_LDAP_BINDDN, KC_LDAP_BINDPW);
$usertest = 'username_of_user_to_be_migrated';
// fetch user info
$u_sel = $db->query("SELECT * FROM `USER_ENTITY` WHERE `USERNAME`='".$db->real_escape_string($usertest)."' AND `REALM_ID`='".$db->real_escape_string(KC
_REALM_ID)."'");
$uinfo = $u_sel->fetch_assoc();
// fetch credential of user, type="password"
$c_sel = $db->query("SELECT * FROM `CREDENTIAL` WHERE `USER_ID`='".$db->real_escape_string($uinfo['ID'])."' AND `TYPE`='password'");
$cred = $c_sel->fetch_assoc();
$uprop = array();
$uprop['objectClass'] = array('top', 'person', 'organizationalPerson', 'inetOrgPerson');
$uprop['uid'] = $uinfo['USERNAME'];
$uprop['mail'] = $uinfo['EMAIL'];
$uprop['cn'] = $uinfo['FIRST_NAME'];
$uprop['sn'] = $uinfo['LAST_NAME'];
// also base64_decode() VALUE, since this seems to be already enocoded in base64, before re-encoding it with base64url_encode()
$uprop['userPassword'] = '{'.strtoupper($cred['ALGORITHM']).'}'.$cred['HASH_ITERATIONS'].'$'.base64url_encode($cred['SALT']).'$'.base64url_encode(base6
4_decode($cred['VALUE']));
ldap_add($ldc, 'uid='.$uprop['uid'].','.KC_LDAP_BASEDN, $uprop);
完成后,我 运行 第二个脚本尝试与该用户连接:
$ldc = ldap_connect(KC_LDAP_SERVER);
ldap_set_option($ldc, LDAP_OPT_PROTOCOL_VERSION, KC_LDAP_PROTOCOL); // should be 3 in config.php
$userdn = 'uid=username_of_user_to_be_migrated,'.KC_LDAP_BASEDN;
$userpw = 'asdASD123';
$ldb = ldap_bind($ldc, $userdn, $userpw);
在执行上面的第二个脚本时,我得到:
PHP Warning: ldap_bind(): Unable to bind to server: Invalid credentials in /home/user/projectdir/test_ldap_user.php on line 22
CREDENTIAL 数据库中的示例条目如下所示:
ID: 5718a65c-1927-4ac7-87ce-aec0c7dda296
DEVICE: NULL
HASH_ITERATIONS: 27500
SALT: � �??�Pz�e��X,
TYPE: password
VALUE: DdCJAJvuhidAC2by7TZY8I0E8HF4V6FXrPa4nSXduvSzbb+xHW3D4QiiiPpvuzL2bdk6k0kNQKS/477k5kiLzA==
USER_ID: b06ce13f-4e8e-474e-b5ee-5d664d6f9575
CREATED_DATE: 1561051801144
COUNTER: 0
DIGITS: 0
PERIOD: 0
ALGORITHM: pbkdf2-sha256
用户密码的输出通常是这样的:
{PBKDF2-SHA256}27500$FrcRlj8SGJJQet1l9LNYLA$DdCJAJvuhidAC2by7TZY8I0E8HF4V6FXrPa4nSXduvSzbb-xHW3D4QiiiPpvuzL2bdk6k0kNQKS_477k5kiLzA
此代码中是否缺少任何确保正确迁移密码哈希的内容?
或者是否已经存在我尚未找到的解决 Keycloak->LDAP 的迁移 script/solution?
提前感谢您在正确方向上提供的任何帮助或推动。
快速更新:我设法与我的一位优秀同事一起找到了解决方案,我想post这里的解决方案,以供将来其他人遇到困难时参考。
我们发现无论使用哪种算法,Keycloak中用于散列的密钥长度都是64字节。这也意味着对于那些默认密钥长度短于 64 字节(以使其恰好 64 字节长)的算法重复哈希的字节序列。在 PBKDF2 中,SHA-1 的默认密钥长度为 20 字节,SHA-256 为 32 字节,SHA-512 为 64 字节。这使我可以创建以下 PHP 函数,该函数 returns 与 slappasswd 返回的字符串类型完全相同:
// Convert password credentials from Keycloak database to LDAP format (specific to PBKDF2 module)
// Parameter $cred is array from Keycloak database fields for 'CREDENTIAL' table
function password_keycloak_to_ldap($cred) {
switch (strtolower($cred['ALGORITHM'])) {
case 'pbkdf2':
case 'pbkdf2-sha1':
$keybytes = 20;
break;
case 'pbkdf2-sha256':
$keybytes = 32;
break;
default:
$keybytes = 64;
break;
}
$out = '{'.strtoupper($cred['ALGORITHM']).'}'.$cred['HASH_ITERATIONS'].'$'.ab64_encode($cred['SALT']).'$';
$oldhash = base64_decode($cred['VALUE']);
$newhash = substr($oldhash, 0, $keybytes);
$out .= ab64_encode($newhash);
return $out;
}
此外,我设置了额外的辅助函数(从别处复制):
// ab64_* functions adapted from Python's Passlib v1.7.1
function ab64_encode($data){
return rtrim(strtr(base64_encode($data), '+', '.'), '=');
}
function ab64_decode($data){
return base64_decode(strtr($data,'.', '+').str_repeat('=', 3-(3+strlen($data))%4));
}
最后,我在问题中 post 编写的脚本中的用法将 userPassword 行替换为以下内容:
$uprop['userPassword'] = password_keycloak_to_ldap($cred);
我希望这可以帮助其他人将用户从 Keycloak 迁移到 LDAP,并保留用户密码。
给出的答案在 Keycloak 16.1.1
中仍然相关。唯一的区别是 salt 在数据库中存储为 base64 编码的字符串。这意味着它只需要转换为经过改编的 base64。修改上面的函数来做到这一点看起来像这样:
function b64_to_ab64($data){
return rtrim(strtr($data, '+', '.'),'=');
}
我们目前有一个 Keycloak 领域,我们希望将所有现有用户导出到 LDAP(他们的密码完好无损),因此我们可以为不支持 OpenID Connect 或 SAML 但支持的其他位置添加登录支持LDAP.
鉴于密码凭证使用 pbkdf2-sha256 算法存储在 Keycloak MySQL 用户数据库中,我不得不使用 OpenLDAP 2.4.47 和 contrib 模块来在 LDAP 端也支持这一点(我也相信我已经在配置中启用了这个模块。
我在使用此处描述的格式将现有的(从 Keycloak)转移到 LDAP 时遇到问题:https://github.com/hamano/openldap-pbkdf2
原谅我使用PHP,但这是我目前最熟悉的编程语言。
我尝试使用 HASH_ITERATIONS、SALT 和 VALUE 字段,以及 PHP 中 base64_encode 下评论中提供的 base64url_encode 自定义函数manual,创建一个 "Adapted Base64" 字符串,并在创建 LDAP 用户时提供构建的字符串作为 userPassword 字段。已在 LDAP 中成功创建用户,但我无法使用刚刚创建的用户的凭据对 LDAP 进行身份验证。
/*
This example migrates a single user, to be adapted to a loop later, if it works.
NOTE: KC_* constants are defined in an external config.php file, such as:
KC_LDAP_BASEDN = "ou=users,dc=sso,dc=example,dc=com"
KC_REALM_ID = "KeycloakUserRealm"
(not actual code from config.php, but you get the point)
*/
$ldc = ldap_connect(KC_LDAP_SERVER);
ldap_set_option($ldc, LDAP_OPT_PROTOCOL_VERSION, KC_LDAP_PROTOCOL); // should be 3 in config.php
$ldb = ldap_bind($ldc, KC_LDAP_BINDDN, KC_LDAP_BINDPW);
$usertest = 'username_of_user_to_be_migrated';
// fetch user info
$u_sel = $db->query("SELECT * FROM `USER_ENTITY` WHERE `USERNAME`='".$db->real_escape_string($usertest)."' AND `REALM_ID`='".$db->real_escape_string(KC
_REALM_ID)."'");
$uinfo = $u_sel->fetch_assoc();
// fetch credential of user, type="password"
$c_sel = $db->query("SELECT * FROM `CREDENTIAL` WHERE `USER_ID`='".$db->real_escape_string($uinfo['ID'])."' AND `TYPE`='password'");
$cred = $c_sel->fetch_assoc();
$uprop = array();
$uprop['objectClass'] = array('top', 'person', 'organizationalPerson', 'inetOrgPerson');
$uprop['uid'] = $uinfo['USERNAME'];
$uprop['mail'] = $uinfo['EMAIL'];
$uprop['cn'] = $uinfo['FIRST_NAME'];
$uprop['sn'] = $uinfo['LAST_NAME'];
// also base64_decode() VALUE, since this seems to be already enocoded in base64, before re-encoding it with base64url_encode()
$uprop['userPassword'] = '{'.strtoupper($cred['ALGORITHM']).'}'.$cred['HASH_ITERATIONS'].'$'.base64url_encode($cred['SALT']).'$'.base64url_encode(base6
4_decode($cred['VALUE']));
ldap_add($ldc, 'uid='.$uprop['uid'].','.KC_LDAP_BASEDN, $uprop);
完成后,我 运行 第二个脚本尝试与该用户连接:
$ldc = ldap_connect(KC_LDAP_SERVER);
ldap_set_option($ldc, LDAP_OPT_PROTOCOL_VERSION, KC_LDAP_PROTOCOL); // should be 3 in config.php
$userdn = 'uid=username_of_user_to_be_migrated,'.KC_LDAP_BASEDN;
$userpw = 'asdASD123';
$ldb = ldap_bind($ldc, $userdn, $userpw);
在执行上面的第二个脚本时,我得到:
PHP Warning: ldap_bind(): Unable to bind to server: Invalid credentials in /home/user/projectdir/test_ldap_user.php on line 22
CREDENTIAL 数据库中的示例条目如下所示:
ID: 5718a65c-1927-4ac7-87ce-aec0c7dda296
DEVICE: NULL
HASH_ITERATIONS: 27500
SALT: � �??�Pz�e��X,
TYPE: password
VALUE: DdCJAJvuhidAC2by7TZY8I0E8HF4V6FXrPa4nSXduvSzbb+xHW3D4QiiiPpvuzL2bdk6k0kNQKS/477k5kiLzA==
USER_ID: b06ce13f-4e8e-474e-b5ee-5d664d6f9575
CREATED_DATE: 1561051801144
COUNTER: 0
DIGITS: 0
PERIOD: 0
ALGORITHM: pbkdf2-sha256
用户密码的输出通常是这样的:
{PBKDF2-SHA256}27500$FrcRlj8SGJJQet1l9LNYLA$DdCJAJvuhidAC2by7TZY8I0E8HF4V6FXrPa4nSXduvSzbb-xHW3D4QiiiPpvuzL2bdk6k0kNQKS_477k5kiLzA
此代码中是否缺少任何确保正确迁移密码哈希的内容?
或者是否已经存在我尚未找到的解决 Keycloak->LDAP 的迁移 script/solution?
提前感谢您在正确方向上提供的任何帮助或推动。
快速更新:我设法与我的一位优秀同事一起找到了解决方案,我想post这里的解决方案,以供将来其他人遇到困难时参考。
我们发现无论使用哪种算法,Keycloak中用于散列的密钥长度都是64字节。这也意味着对于那些默认密钥长度短于 64 字节(以使其恰好 64 字节长)的算法重复哈希的字节序列。在 PBKDF2 中,SHA-1 的默认密钥长度为 20 字节,SHA-256 为 32 字节,SHA-512 为 64 字节。这使我可以创建以下 PHP 函数,该函数 returns 与 slappasswd 返回的字符串类型完全相同:
// Convert password credentials from Keycloak database to LDAP format (specific to PBKDF2 module)
// Parameter $cred is array from Keycloak database fields for 'CREDENTIAL' table
function password_keycloak_to_ldap($cred) {
switch (strtolower($cred['ALGORITHM'])) {
case 'pbkdf2':
case 'pbkdf2-sha1':
$keybytes = 20;
break;
case 'pbkdf2-sha256':
$keybytes = 32;
break;
default:
$keybytes = 64;
break;
}
$out = '{'.strtoupper($cred['ALGORITHM']).'}'.$cred['HASH_ITERATIONS'].'$'.ab64_encode($cred['SALT']).'$';
$oldhash = base64_decode($cred['VALUE']);
$newhash = substr($oldhash, 0, $keybytes);
$out .= ab64_encode($newhash);
return $out;
}
此外,我设置了额外的辅助函数(从别处复制):
// ab64_* functions adapted from Python's Passlib v1.7.1
function ab64_encode($data){
return rtrim(strtr(base64_encode($data), '+', '.'), '=');
}
function ab64_decode($data){
return base64_decode(strtr($data,'.', '+').str_repeat('=', 3-(3+strlen($data))%4));
}
最后,我在问题中 post 编写的脚本中的用法将 userPassword 行替换为以下内容:
$uprop['userPassword'] = password_keycloak_to_ldap($cred);
我希望这可以帮助其他人将用户从 Keycloak 迁移到 LDAP,并保留用户密码。
给出的答案在 Keycloak 16.1.1
中仍然相关。唯一的区别是 salt 在数据库中存储为 base64 编码的字符串。这意味着它只需要转换为经过改编的 base64。修改上面的函数来做到这一点看起来像这样:
function b64_to_ab64($data){
return rtrim(strtr($data, '+', '.'),'=');
}