Keycloak:使用自定义 JWT 对用户进行身份验证
Keycloak: Authenticate user with a custom JWT
情况: 我们使用 keycloak 使用 JavaScript 适配器通过正常的浏览器身份验证流程对 Web 应用程序 (A) 中的用户进行身份验证。这非常有效!
目标:现在,一组新的用户应该可以访问A。但是他们在受信任的第三方应用程序(B)中使用用户名和密码登录而没有钥匙斗篷。在 B 中,他们有一个 link 到 A,带有自定义 JWT(主要包含用户名和角色)作为查询参数。因此,当用户单击 link 时,他会进入我们的应用程序入口点,我们可以在其中从 URL 中读取 JWT。现在需要做的是某种代币交换。我们想将这个自定义 JWT 发送到 Keycloak,Keycloak 会发回一个类似于正常登录过程的访问令牌。
问题:Keycloak 是否内置支持这样的用例?
尝试次数:
我尝试按照文档中的建议创建一个 "Signed JWT" 作为“Client Authenticator”的机密客户端。经过一些测试后,我认为这不是正确的轨道,即使名称很有前途。
另一个轨道是“Client suggested identity provider”,通过实施自定义身份提供者。但我看不出如何在请求中发送 JWT。
目前我正在尝试使用 Autentication SPI 通过自定义身份验证器扩展身份验证流程。
也许比我想的要简单得多。谁能指引我正确的方向?
所以我终于能够用问题中提到的Authentication SPI来解决它。
在 Keycloak 中,我复制了 "browser" 身份验证流程(因为您无法修改 built-in 流程)并引入了额外的步骤 "Portal JWT"(见下图)。然后我将它绑定到 "Bindings" 选项卡
中的 "Browser Flow"
"Portal JWT" 后面是我的自定义身份验证器,它从重定向 uri 中的查询参数中提取 JWT,并对其进行解析以从中获取用户名和角色。然后使用自定义属性 "isExternal" 将用户添加到 keycloak。这是它的摘录:
public class JwtAuthenticator implements Authenticator {
private final JwtReader reader;
JwtAuthenticator(JwtReader reader) {
this.reader = reader;
}
@Override
public void authenticate(AuthenticationFlowContext context) {
Optional<String> externalCredential = hasExternalCredential(context);
if (externalCredential.isPresent()) {
ExternalUser externalUser = reader.read(context.getAuthenticatorConfig(), externalCredential.get());
String username = externalUser.getUsername();
UserModel user = context.getSession().users().getUserByUsername(username, context.getRealm());
if (user == null) {
user = context.getSession().users().addUser(context.getRealm(), username);
user.setEnabled(true);
user.setSingleAttribute("isExternal", "true");
}
for (String roleName : externalUser.getRoles()) {
RoleModel role = context.getRealm().getRole(roleName);
if (role == null) {
role = context.getRealm().addRole(roleName);
}
user.grantRole(role);
}
context.setUser(user);
context.success();
} else {
context.attempted();
}
}
private Optional<String> hasExternalCredential(AuthenticationFlowContext context) {
String redirectUri = context.getUriInfo().getQueryParameters().getFirst("redirect_uri);
try {
List<NameValuePair> queryParams = URLEncodedUtils.parse(new URI(redirectUri), "UTF-8");
Optional<NameValuePair> jwtParam = queryParams.stream()
.filter(nv -> "jwt".equalsIgnoreCase(nv.getName())).findAny();
if (jwtParam.isPresent()) {
String jwt = jwtParam.get().getValue();
if (LOG.isDebugEnabled()) {
LOG.debug("JWT found: " + jwt);
}
return Optional.of(jwt);
}
} catch (URISyntaxException e) {
LOG.error("Redirect URL not as expected: " + redirectUri);
}
return Optional.empty();
}
情况: 我们使用 keycloak 使用 JavaScript 适配器通过正常的浏览器身份验证流程对 Web 应用程序 (A) 中的用户进行身份验证。这非常有效!
目标:现在,一组新的用户应该可以访问A。但是他们在受信任的第三方应用程序(B)中使用用户名和密码登录而没有钥匙斗篷。在 B 中,他们有一个 link 到 A,带有自定义 JWT(主要包含用户名和角色)作为查询参数。因此,当用户单击 link 时,他会进入我们的应用程序入口点,我们可以在其中从 URL 中读取 JWT。现在需要做的是某种代币交换。我们想将这个自定义 JWT 发送到 Keycloak,Keycloak 会发回一个类似于正常登录过程的访问令牌。
问题:Keycloak 是否内置支持这样的用例?
尝试次数:
我尝试按照文档中的建议创建一个 "Signed JWT" 作为“Client Authenticator”的机密客户端。经过一些测试后,我认为这不是正确的轨道,即使名称很有前途。
另一个轨道是“Client suggested identity provider”,通过实施自定义身份提供者。但我看不出如何在请求中发送 JWT。
目前我正在尝试使用 Autentication SPI 通过自定义身份验证器扩展身份验证流程。
也许比我想的要简单得多。谁能指引我正确的方向?
所以我终于能够用问题中提到的Authentication SPI来解决它。
在 Keycloak 中,我复制了 "browser" 身份验证流程(因为您无法修改 built-in 流程)并引入了额外的步骤 "Portal JWT"(见下图)。然后我将它绑定到 "Bindings" 选项卡
中的 "Browser Flow""Portal JWT" 后面是我的自定义身份验证器,它从重定向 uri 中的查询参数中提取 JWT,并对其进行解析以从中获取用户名和角色。然后使用自定义属性 "isExternal" 将用户添加到 keycloak。这是它的摘录:
public class JwtAuthenticator implements Authenticator {
private final JwtReader reader;
JwtAuthenticator(JwtReader reader) {
this.reader = reader;
}
@Override
public void authenticate(AuthenticationFlowContext context) {
Optional<String> externalCredential = hasExternalCredential(context);
if (externalCredential.isPresent()) {
ExternalUser externalUser = reader.read(context.getAuthenticatorConfig(), externalCredential.get());
String username = externalUser.getUsername();
UserModel user = context.getSession().users().getUserByUsername(username, context.getRealm());
if (user == null) {
user = context.getSession().users().addUser(context.getRealm(), username);
user.setEnabled(true);
user.setSingleAttribute("isExternal", "true");
}
for (String roleName : externalUser.getRoles()) {
RoleModel role = context.getRealm().getRole(roleName);
if (role == null) {
role = context.getRealm().addRole(roleName);
}
user.grantRole(role);
}
context.setUser(user);
context.success();
} else {
context.attempted();
}
}
private Optional<String> hasExternalCredential(AuthenticationFlowContext context) {
String redirectUri = context.getUriInfo().getQueryParameters().getFirst("redirect_uri);
try {
List<NameValuePair> queryParams = URLEncodedUtils.parse(new URI(redirectUri), "UTF-8");
Optional<NameValuePair> jwtParam = queryParams.stream()
.filter(nv -> "jwt".equalsIgnoreCase(nv.getName())).findAny();
if (jwtParam.isPresent()) {
String jwt = jwtParam.get().getValue();
if (LOG.isDebugEnabled()) {
LOG.debug("JWT found: " + jwt);
}
return Optional.of(jwt);
}
} catch (URISyntaxException e) {
LOG.error("Redirect URL not as expected: " + redirectUri);
}
return Optional.empty();
}