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 中添加额外的声明的主要内容,如果未能解决你的问题,请参考以下文章

OAuth 2.0只是授权协议,OIDC才是认证授权协议

Keycloak/OIDC : 检索用户组属性

Net CORE Keycloak 和 OIDC

[认证授权] 5.OIDC(OpenId Connect)身份认证授权(扩展部分)

如何在 Kubernetes 上的 Quarkus 中设置与 Keycloak 的 OIDC 连接

Keycloak - 通过OIDC端点检索JWT令牌