0

我们正在尝试实施 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 中添加额外的声明?请在这方面帮助我。

提前感谢您的帮助。

4

1 回答 1

0

我已按照您的步骤开发我们的自定义协议。当我们迁移我们公司现有的身份验证协议时,我使用了 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;
    }
}

Xyz登录协议

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) {
        ....
    }
于 2020-12-12T22:03:18.590 回答