OpenshiftV4IdentityProvider.java

package org.keycloak.social.openshift;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;

import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Optional;

/**
 * Identity provider for Openshift V4.
 *
 * @author David Festal and Sebastian ��askawiec
 */
public class OpenshiftV4IdentityProvider extends AbstractOAuth2IdentityProvider<OpenshiftV4IdentityProviderConfig> implements SocialIdentityProvider<OpenshiftV4IdentityProviderConfig> {

    public static final String BASE_URL = "https://api.preview.openshift.com";
    public static final String OPENSHIFT_OAUTH_METADATA_ENDPOINT = "/.well-known/oauth-authorization-server";
    public static final String PROFILE_RESOURCE = "/apis/user.openshift.io/v1/users/~";
    public static final String DEFAULT_SCOPE = "user:info";
    private static final String KUBEADM_NAME = "kube:admin";

    public OpenshiftV4IdentityProvider(KeycloakSession session, OpenshiftV4IdentityProviderConfig config) {
        super(session, config);
        final String baseUrl = Optional.ofNullable(config.getBaseUrl()).orElse(BASE_URL);
        Map<String, Object> oauthDescriptor = getAuthJson(session, config.getBaseUrl());
        logger.debugv("Openshift v4 OAuth descriptor: {0}", oauthDescriptor);
        config.setAuthorizationUrl((String) oauthDescriptor.get("authorization_endpoint"));
        config.setTokenUrl((String) oauthDescriptor.get("token_endpoint"));
        config.setUserInfoUrl(baseUrl + PROFILE_RESOURCE);
    }

    Map<String, Object> getAuthJson(KeycloakSession session, String baseUrl) {
        try {
            InputStream response = getOauthMetadataInputStream(session, baseUrl);
            Map<String, Object> map = mapMetadata(response);
            return map;
        } catch (Exception e) {
            throw new IdentityBrokerException("Could not initialize oAuth metadata", e);
        }
    }

    InputStream getOauthMetadataInputStream(KeycloakSession session, String baseUrl) throws IOException {
        HttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
        HttpGet getRequest = new HttpGet(baseUrl + OPENSHIFT_OAUTH_METADATA_ENDPOINT);
        getRequest.addHeader("accept", "application/json");

        HttpResponse response = httpClient.execute(getRequest);

        if (response.getStatusLine().getStatusCode() != 200) {
            throw new RuntimeException("Failed : HTTP error code : " + response.getStatusLine().getStatusCode());
        }
        return response.getEntity().getContent();
    }

    Map mapMetadata(InputStream response) throws IOException {
        return new ObjectMapper().readValue(response, Map.class);
    }

    @Override
    protected String getDefaultScopes() {
        return DEFAULT_SCOPE;
    }

    @Override
    protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
        try {
            final JsonNode profile = fetchProfile(accessToken);
            final BrokeredIdentityContext user = extractUserContext(profile);
            AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
            return user;
        } catch (Exception e) {
            throw new IdentityBrokerException("Could not obtain user profile from Openshift.", e);
        }
    }

    private BrokeredIdentityContext extractUserContext(JsonNode profile) {
        JsonNode metadata = profile.get("metadata");
        logger.debugv("extractUserContext: metadata = {0}", metadata);
        final BrokeredIdentityContext user = new BrokeredIdentityContext(
                getJsonProperty(metadata, "uid") != null
                        ? getJsonProperty(metadata, "uid")
                        : tryGetKubeAdmin(metadata)
        );
        user.setUsername(getJsonProperty(metadata, "name"));
        user.setName(getJsonProperty(profile, "fullName"));
        user.setIdpConfig(getConfig());
        user.setIdp(this);
        return user;
    }

    private String tryGetKubeAdmin(JsonNode metadata) {
        String nameProperty = getJsonProperty(metadata, "name");
        if(!KUBEADM_NAME.equals(nameProperty)){
            return null;
        }
        return nameProperty;
    }

    private JsonNode fetchProfile(String accessToken) throws IOException {
        return SimpleHttp.doGet(getConfig().getUserInfoUrl(), this.session)
                .header("Authorization", "Bearer " + accessToken)
                .asJson();
    }

    @Override
    protected boolean supportsExternalExchange() {
        return true;
    }

    @Override
    protected String getProfileEndpointForValidation(EventBuilder event) {
        return getConfig().getUserInfoUrl();
    }

    @Override
    protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
        final BrokeredIdentityContext user = extractUserContext(profile);
        AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
        return user;
    }

}