io.apicurio.hub.api.security.KeycloakLinkedAccountsProvider.java Source code

Java tutorial

Introduction

Here is the source code for io.apicurio.hub.api.security.KeycloakLinkedAccountsProvider.java

Source

/*
 * Copyright 2017 JBoss Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.apicurio.hub.api.security;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.net.ssl.SSLContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.representations.AccessToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.apicurio.hub.api.beans.InitiatedLinkedAccount;
import io.apicurio.hub.core.beans.LinkedAccountType;
import io.apicurio.hub.core.config.HubConfiguration;

/**
 * An implementation of {@link ILinkedAccountsProvider} that used Keycloak to manage
 * the account links.  This uses the Identity Provider support built into Keycloak to
 * create and manage the account links.  Keycloak also stores the external token needed
 * to access the external services.
 * 
 * @author eric.wittmann@gmail.com
 */
@ApplicationScoped
public class KeycloakLinkedAccountsProvider implements ILinkedAccountsProvider {

    private static Logger logger = LoggerFactory.getLogger(KeycloakLinkedAccountsProvider.class);

    @Inject
    private ISecurityContext security;
    @Inject
    private HubConfiguration config;

    @Inject
    private HttpServletRequest request;

    private CloseableHttpClient httpClient;

    @PostConstruct
    protected void postConstruct() {
        try {
            if (config.isDisableKeycloakTrustManager()) {
                SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy())
                        .build();
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,
                        NoopHostnameVerifier.INSTANCE);
                httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
            } else {
                httpClient = HttpClients.createSystem();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @see io.apicurio.hub.api.security.ILinkedAccountsProvider#initiateLinkedAccount(io.apicurio.hub.api.beans.LinkedAccountType, java.lang.String, java.lang.String)
     */
    @Override
    public InitiatedLinkedAccount initiateLinkedAccount(LinkedAccountType accountType, String redirectUri,
            String nonce) {
        String authServerRootUrl = config.getKeycloakAuthUrl();
        String realm = config.getKeycloakRealm();
        String provider = accountType.alias();

        KeycloakSecurityContext session = (KeycloakSecurityContext) request
                .getAttribute(KeycloakSecurityContext.class.getName());
        AccessToken token = session.getToken();

        String clientId = token.getIssuedFor();
        MessageDigest md = null;
        try {
            md = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        String input = nonce + token.getSessionState() + clientId + provider;
        byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
        String hash = Base64Url.encode(check);
        String accountLinkUrl = KeycloakUriBuilder.fromUri(authServerRootUrl)
                .path("/realms/{realm}/broker/{provider}/link").queryParam("nonce", nonce).queryParam("hash", hash)
                .queryParam("client_id", clientId).queryParam("redirect_uri", redirectUri).build(realm, provider)
                .toString();

        logger.debug("Account Link URL: {}", accountLinkUrl);

        // Return the URL that the browser should use to initiate the account linking
        InitiatedLinkedAccount rval = new InitiatedLinkedAccount();
        rval.setAuthUrl(accountLinkUrl);
        rval.setNonce(nonce);
        return rval;
    }

    /**
     * @see io.apicurio.hub.api.security.ILinkedAccountsProvider#deleteLinkedAccount(io.apicurio.hub.api.beans.LinkedAccountType)
     */
    @Override
    public void deleteLinkedAccount(LinkedAccountType type) throws IOException {
        try {
            KeycloakSecurityContext session = (KeycloakSecurityContext) request
                    .getAttribute(KeycloakSecurityContext.class.getName());

            String authServerRootUrl = config.getKeycloakAuthUrl();
            String realm = config.getKeycloakRealm();
            String provider = type.alias();

            session.getToken().getSessionState();

            String url = KeycloakUriBuilder.fromUri(authServerRootUrl)
                    .path("/auth/realms/{realm}/account/federated-identity-update").queryParam("action", "REMOVE")
                    .queryParam("provider_id", provider).build(realm).toString();
            logger.debug("Deleting identity provider using URL: {}", url);

            HttpGet get = new HttpGet(url);
            get.addHeader("Accept", "application/json");
            get.addHeader("Authorization", "Bearer " + session.getTokenString());

            try (CloseableHttpResponse response = httpClient.execute(get)) {
                if (response.getStatusLine().getStatusCode() != 200) {
                    logger.debug("HTTP Response Status Code when deleting identity provider: {}",
                            response.getStatusLine().getStatusCode());
                }
            }
        } catch (Exception e) {
            throw new IOException("Error deleting linked account.", e);
        }
    }

    /**
     * @see io.apicurio.hub.api.security.ILinkedAccountsProvider#getLinkedAccountToken(io.apicurio.hub.api.beans.LinkedAccountType)
     */
    @Override
    public String getLinkedAccountToken(LinkedAccountType type) throws IOException {
        String authServerRootUrl = config.getKeycloakAuthUrl();
        String realm = config.getKeycloakRealm();
        String provider = type.alias();

        try {
            String externalTokenUrl = KeycloakUriBuilder.fromUri(authServerRootUrl)
                    .path("/realms/{realm}/broker/{provider}/token").build(realm, provider).toString();
            String token = this.security.getToken();

            HttpGet get = new HttpGet(externalTokenUrl);
            get.addHeader("Accept", "application/json");
            get.addHeader("Authorization", "Bearer " + token);

            try (CloseableHttpResponse response = httpClient.execute(get)) {
                if (response.getStatusLine().getStatusCode() != 200) {
                    logger.error("Failed to access External IDP Access Token from Keycloak: {} - {}",
                            response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
                    throw new IOException(
                            "Unexpected response from Keycloak: " + response.getStatusLine().getStatusCode() + "::"
                                    + response.getStatusLine().getReasonPhrase());
                }

                try (InputStream contentStream = response.getEntity().getContent()) {
                    String content = IOUtils.toString(contentStream);
                    return content;
                }
            }
        } catch (IllegalArgumentException e) {
            throw new IOException("Error getting linked account token.", e);
        }
    }

}