KEYCLOAK - 扩展 OIDC 协议 |缺少凭据选项卡 |在 AccessTokenResponse 中添加额外的声明
Posted
技术标签:
【中文标题】KEYCLOAK - 扩展 OIDC 协议 |缺少凭据选项卡 |在 AccessTokenResponse 中添加额外的声明【英文标题】:KEYCLOAK - Extending OIDC Protocol | Missing Credentials Tab | Add extra claims in AccessTokenResponse 【发布时间】:2021-03-14 17:36:10 【问题描述】:我们正在尝试实施 SMART On FHIR 医疗保健授权协议规范。该规范是 OIDC(开放 ID 连接协议)的扩展。在 FHIR 上的 SMART 中,我们需要在 OAUTH 舞蹈期间在 AccessTokenResponse 对象中添加名为“患者”的额外声明,其值为“123”。
为了实现这一点,我尝试扩展 OIDCLoginProtocol 和 OIDCLoginProtocolFactory 类,并为此协议赋予了一个新名称,称为“smart-openid-connect”。我将其创建为 SPI(服务提供者接口)JAR 并将其复制到 /standalone/deployments 文件夹。现在,我可以在 UI 中看到名为“smart-openid-connect”的新协议,但它没有在客户端创建屏幕中显示访问类型选项以选择作为机密客户端。因此,我无法创建客户端机密,因为此新协议没有出现凭据菜单。
我有以下问题:
如何使用 SPI 为我创建的新协议启用客户端创建屏幕中的凭据选项卡。? 我需要覆盖哪个类才能在 AccessTokenResponse 中添加额外的声明? 请在这方面帮助我。
提前感谢您的帮助。
【问题讨论】:
我们使用 KEYCLOAK 作为授权服务器来完成这个任务。 【参考方案1】:我已按照您的步骤开发我们的自定义协议。当我们迁移公司现有的身份验证协议时,我使用了 org.keycloak.adapters.authentication.ClientCredentialsProvider、org.keycloak.authentication.ClientAuthenticatorFactory、org.keycloak.authentication.ClientAuthenticator 类来定义我们的自定义协议。凭据选项卡仅在选择 oidc 和机密选项时可见。它是在 UI javascript 代码上定义的。所以我们选择 oidc 选项来设置自定义协议。之后,我们返回自定义协议。
XyzClientAuthenticatorFactory
public class XyzClientAuthenticatorFactory implements ClientAuthenticatorFactory, ClientAuthenticator
public static final String PROVIDER_ID = "xyz-client-authenticator";
public static final String DISPLAY_TEXT = "Xyz Client Authenticator";
public static final String REFERENCE_CATEGORY = null;
public static final String HELP_TEXT = null;
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
private AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES =
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
AuthenticationExecutionModel.Requirement.DISABLED;
static
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(Constants.CLIENT_SETTINGS_APP_ID);
property.setLabel("Xyz App Id");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(Constants.CLIENT_SETTINGS_APP_KEY);
property.setLabel("Xyz App Key");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
@Override
public void authenticateClient(ClientAuthenticationFlowContext context)
@Override
public String getDisplayType()
return DISPLAY_TEXT;
@Override
public String getReferenceCategory()
return REFERENCE_CATEGORY;
@Override
public ClientAuthenticator create()
return this;
@Override
public boolean isConfigurable()
return false;
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices()
return REQUIREMENT_CHOICES;
@Override
public boolean isUserSetupAllowed()
return false;
@Override
public List<ProviderConfigProperty> getConfigPropertiesPerClient()
return configProperties;
@Override
public Map<String, Object> getAdapterConfiguration(ClientModel client)
Map<String, Object> result = new HashMap<>();
result.put(Constants.CLIENT_SETTINGS_APP_ID, client.getAttribute(Constants.CLIENT_SETTINGS_APP_ID));
result.put(Constants.CLIENT_SETTINGS_APP_KEY, client.getAttribute(Constants.CLIENT_SETTINGS_APP_KEY));
return result;
@Override
public Set<String> getProtocolAuthenticatorMethods(String loginProtocol)
if (loginProtocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL))
Set<String> results = new LinkedHashSet<>();
results.add(Constants.CLIENT_SETTINGS_APP_ID);
results.add(Constants.CLIENT_SETTINGS_APP_KEY);
return results;
else
return Collections.emptySet();
@Override
public String getHelpText()
return HELP_TEXT;
@Override
public List<ProviderConfigProperty> getConfigProperties()
return new LinkedList<>();
@Override
public ClientAuthenticator create(KeycloakSession session)
return this;
@Override
public void init(Config.Scope config)
@Override
public void postInit(KeycloakSessionFactory factory)
@Override
public void close()
@Override
public String getId()
return PROVIDER_ID;
XyzClientCredential
public class XyzClientCredential implements ClientCredentialsProvider
public static final String PROVIDER_ID = "xyz-client-credential";
@Override
public String getId()
return PROVIDER_ID;
@Override
public void init(KeycloakDeployment deployment, Object config)
@Override
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams)
XyzLoginProtocolFactory
public class XyzLoginProtocolFactory implements LoginProtocolFactory
static
@Override
public Map<String, ProtocolMapperModel> getBuiltinMappers()
return new HashMap<>();
@Override
public Object createProtocolEndpoint(RealmModel realm, EventBuilder event)
return new XyzLoginProtocolService(realm, event);
protected void addDefaultClientScopes(RealmModel realm, ClientModel newClient)
addDefaultClientScopes(realm, Arrays.asList(newClient));
protected void addDefaultClientScopes(RealmModel realm, List<ClientModel> newClients)
Set<ClientScopeModel> defaultClientScopes = realm.getDefaultClientScopes(true).stream()
.filter(clientScope -> getId().equals(clientScope.getProtocol()))
.collect(Collectors.toSet());
for (ClientModel newClient : newClients)
for (ClientScopeModel defaultClientScopeModel : defaultClientScopes)
newClient.addClientScope(defaultClientScopeModel, true);
Set<ClientScopeModel> nonDefaultClientScopes = realm.getDefaultClientScopes(false).stream()
.filter(clientScope -> getId().equals(clientScope.getProtocol()))
.collect(Collectors.toSet());
for (ClientModel newClient : newClients)
for (ClientScopeModel nonDefaultClientScope : nonDefaultClientScopes)
newClient.addClientScope(nonDefaultClientScope, true);
@Override
public void createDefaultClientScopes(RealmModel newRealm, boolean addScopesToExistingClients)
// Create default client scopes for realm built-in clients too
if (addScopesToExistingClients)
addDefaultClientScopes(newRealm, newRealm.getClients());
@Override
public void setupClientDefaults(ClientRepresentation rep, ClientModel newClient)
@Override
public LoginProtocol create(KeycloakSession session)
return new XyzLoginProtocol().setSession(session);
@Override
public void init(Config.Scope config)
log.infof("XyzLoginProtocolFactory init");
@Override
public void postInit(KeycloakSessionFactory factory)
factory.register(event ->
if (event instanceof RealmModel.ClientCreationEvent)
ClientModel client = ((RealmModel.ClientCreationEvent)event).getCreatedClient();
addDefaultClientScopes(client.getRealm(), client);
addDefaults(client);
);
protected void addDefaults(ClientModel client)
@Override
public void close()
@Override
public String getId()
return XyzLoginProtocol.LOGIN_PROTOCOL;
XyzLogin协议
public class XyzLoginProtocol implements LoginProtocol
public static final String LOGIN_PROTOCOL = "xyz";
protected KeycloakSession session;
protected RealmModel realm;
protected UriInfo uriInfo;
protected HttpHeaders headers;
protected EventBuilder event;
public XyzLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, EventBuilder event)
this.session = session;
this.realm = realm;
this.uriInfo = uriInfo;
this.headers = headers;
this.event = event;
public XyzLoginProtocol()
@Override
public XyzLoginProtocol setSession(KeycloakSession session)
this.session = session;
return this;
@Override
public XyzLoginProtocol setRealm(RealmModel realm)
this.realm = realm;
return this;
@Override
public XyzLoginProtocol setUriInfo(UriInfo uriInfo)
this.uriInfo = uriInfo;
return this;
@Override
public XyzLoginProtocol setHttpHeaders(HttpHeaders headers)
this.headers = headers;
return this;
@Override
public XyzLoginProtocol setEventBuilder(EventBuilder event)
this.event = event;
return this;
@Override
public Response authenticated(AuthenticationSessionModel authSession, UserSessionModel userSession, ClientSessionContext clientSessionCtx)
log.debugf("Authenticated.. User: %s, Session Id: %s", userSession.getUser().getUsername(), userSession.getId());
try
....
catch (Exception ex)
// TODO handle TokenNotFoundException exception
log.error(ex.getMessage(), ex);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
@Override
public Response sendError(AuthenticationSessionModel authSession, Error error)
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
String redirect = authSession.getRedirectUri();
try
URI uri = new URI(redirect);
return Response.status(302).location(uri).build();
catch (Exception ex)
log.error(ex.getMessage(), ex);
return Response.noContent().build();
@Override
public void backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession)
ClientModel client = clientSession.getClient();
new ResourceAdminManager(session).logoutClientSession(realm, client, clientSession);
@Override
public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession)
throw new RuntimeException("NOT IMPLEMENTED");
@Override
public Response finishLogout(UserSessionModel userSession)
return Response.noContent().build();
@Override
public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession)
return false;
@Override
public boolean sendPushRevocationPolicyRequest(RealmModel realm, ClientModel resource, int notBefore, String managementUrl)
PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getClientId(), notBefore);
String token = session.tokens().encode(adminAction);
log.tracev("pushRevocation resource: 0 url: 1", resource.getClientId(), managementUrl);
URI target = UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build();
try
int status = session.getProvider(HttpClientProvider.class).postText(target.toString(), token);
boolean success = status == 204 || status == 200;
log.tracef("pushRevocation success for %s: %s", managementUrl, success);
return success;
catch (IOException e)
ServicesLogger.LOGGER.failedToSendRevocation(e);
return false;
@Override
public void close()
XyzLoginProtocolService
public class XyzLoginProtocolService
private final RealmModel realm;
private final EventBuilder event;
@Context
private KeycloakSession session;
@Context
private HttpHeaders headers;
@Context
private HttpRequest request;
@Context
private ClientConnection clientConnection;
public XyzLoginProtocolService(RealmModel realm, EventBuilder event)
this.realm = realm;
this.event = event;
this.event.realm(realm);
@POST
@Path("request")
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Response request(ApipmLoginRequest loginRequest)
....
【讨论】:
非常感谢您分享代码 Ismail。我的要求是有一个新的协议名称,比如“smart-open-id”,它具有与 OIDC 类似的功能,以及创建机密客户端和自定义访问令牌响应字段的能力。所以,从你所说的来看,我了解我只能通过修改 Java 脚本代码以启用此新协议的凭据选项卡来实现它。我是否正确阅读了您的答案? 是的,您应该编辑 html 模板代码。模板文件为kc-tabs-client.html,可以通过搜索凭证或oidc字轻松找到。以上是关于KEYCLOAK - 扩展 OIDC 协议 |缺少凭据选项卡 |在 AccessTokenResponse 中添加额外的声明的主要内容,如果未能解决你的问题,请参考以下文章
[认证授权] 5.OIDC(OpenId Connect)身份认证授权(扩展部分)