org.eclipse.che.multiuser.keycloak.server.KeycloakServiceClient.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.che.multiuser.keycloak.server.KeycloakServiceClient.java

Source

/*
 * Copyright (c) 2012-2018 Red Hat, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Red Hat, Inc. - initial API and implementation
 */
package org.eclipse.che.multiuser.keycloak.server;

import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;

import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.impl.DefaultClaims;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.UnauthorizedException;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.lang.Pair;
import org.eclipse.che.dto.server.DtoFactory;
import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakErrorResponse;
import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakTokenResponse;

/**
 * Helps to perform keycloak operations and provide correct errors handling.
 *
 * @author Max Shaposhnik (mshaposh@redhat.com)
 */
@Singleton
public class KeycloakServiceClient {

    private KeycloakSettings keycloakSettings;

    private static final Pattern assotiateUserPattern = Pattern
            .compile("User (.+) is not associated with identity provider (.+)");

    private static final Gson gson = new Gson();

    @Inject
    public KeycloakServiceClient(KeycloakSettings keycloakSettings) {
        this.keycloakSettings = keycloakSettings;
    }

    /**
     * Generates URL for account linking redirect
     *
     * @param token client jwt token
     * @param oauthProvider provider name
     * @param redirectAfterLogin URL to return after login
     * @return URL to redirect client to perform account linking
     */
    public String getAccountLinkingURL(Jwt token, String oauthProvider, String redirectAfterLogin) {

        DefaultClaims claims = (DefaultClaims) token.getBody();
        final String clientId = claims.getAudience();

        final String sessionState = claims.get("session_state", String.class);
        MessageDigest md;
        try {
            md = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }

        final String nonce = UUID.randomUUID().toString();
        final String input = nonce + sessionState + clientId + oauthProvider;
        byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
        final String hash = Base64.getUrlEncoder().encodeToString(check);

        return UriBuilder.fromUri(keycloakSettings.get().get(AUTH_SERVER_URL_SETTING))
                .path("/realms/{realm}/broker/{provider}/link").queryParam("nonce", nonce).queryParam("hash", hash)
                .queryParam("client_id", clientId).queryParam("redirect_uri", redirectAfterLogin)
                .build(keycloakSettings.get().get(REALM_SETTING), oauthProvider).toString();
    }

    /**
     * Gets auth token from given identity provider.
     *
     * @param oauthProvider provider name
     * @return KeycloakTokenResponse token response
     * @throws ForbiddenException when HTTP request was forbidden
     * @throws BadRequestException when HTTP request considered as bad
     * @throws IOException when unable to parse error response
     * @throws NotFoundException when requested URL not found
     * @throws ServerException when other error occurs
     * @throws UnauthorizedException when no token present for user or user not linked to provider
     */
    public KeycloakTokenResponse getIdentityProviderToken(String oauthProvider) throws ForbiddenException,
            BadRequestException, IOException, NotFoundException, ServerException, UnauthorizedException {
        String url = UriBuilder.fromUri(keycloakSettings.get().get(AUTH_SERVER_URL_SETTING))
                .path("/realms/{realm}/broker/{provider}/token")
                .build(keycloakSettings.get().get(REALM_SETTING), oauthProvider).toString();
        try {
            String response = doRequest(url, HttpMethod.GET, null);
            // Successful answer is not a json, but key=value&foo=bar format pairs
            return DtoFactory.getInstance().createDtoFromJson(toJson(response), KeycloakTokenResponse.class);
        } catch (BadRequestException e) {
            if (assotiateUserPattern.matcher(e.getMessage()).matches()) {
                // If user has no link with identity provider yet,
                // we should threat this as unauthorized and send to oAuth login page.
                throw new UnauthorizedException(e.getMessage());
            }
            throw e;
        }
    }

    private String doRequest(String url, String method, List<Pair<String, ?>> parameters) throws IOException,
            ServerException, ForbiddenException, NotFoundException, UnauthorizedException, BadRequestException {
        final String authToken = EnvironmentContext.getCurrent().getSubject().getToken();
        final boolean hasQueryParams = parameters != null && !parameters.isEmpty();
        if (hasQueryParams) {
            final UriBuilder ub = UriBuilder.fromUri(url);
            for (Pair<String, ?> parameter : parameters) {
                ub.queryParam(parameter.first, parameter.second);
            }
            url = ub.build().toString();
        }
        final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
        conn.setConnectTimeout(60000);
        conn.setReadTimeout(60000);

        try {
            conn.setRequestMethod(method);
            // drop a hint for server side that we want to receive application/json
            conn.addRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON);
            if (authToken != null) {
                conn.setRequestProperty(HttpHeaders.AUTHORIZATION, "bearer " + authToken);
            }
            final int responseCode = conn.getResponseCode();
            if ((responseCode / 100) != 2) {
                InputStream in = conn.getErrorStream();
                if (in == null) {
                    in = conn.getInputStream();
                }
                final String str;
                try (Reader reader = new InputStreamReader(in)) {
                    str = CharStreams.toString(reader);
                }
                final String contentType = conn.getContentType();
                if (contentType != null && (contentType.startsWith(MediaType.APPLICATION_JSON)
                        || contentType.startsWith("application/vnd.api+json"))) {
                    final KeycloakErrorResponse serviceError = DtoFactory.getInstance().createDtoFromJson(str,
                            KeycloakErrorResponse.class);
                    if (responseCode == Response.Status.FORBIDDEN.getStatusCode()) {
                        throw new ForbiddenException(serviceError.getErrorMessage());
                    } else if (responseCode == Response.Status.NOT_FOUND.getStatusCode()) {
                        throw new NotFoundException(serviceError.getErrorMessage());
                    } else if (responseCode == Response.Status.UNAUTHORIZED.getStatusCode()) {
                        throw new UnauthorizedException(serviceError.getErrorMessage());
                    } else if (responseCode == Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) {
                        throw new ServerException(serviceError.getErrorMessage());
                    } else if (responseCode == Response.Status.BAD_REQUEST.getStatusCode()) {
                        throw new BadRequestException(serviceError.getErrorMessage());
                    }
                    throw new ServerException(serviceError.getErrorMessage());
                }
                // Can't parse content as json or content has format other we expect for error.
                throw new IOException(String.format("Failed access: %s, method: %s, response code: %d, message: %s",
                        UriBuilder.fromUri(url).replaceQuery("token").build(), method, responseCode, str));
            }
            try (Reader reader = new InputStreamReader(conn.getInputStream())) {
                return CharStreams.toString(reader);
            }
        } finally {
            conn.disconnect();
        }
    }

    /** Converts key=value&foo=bar string into json */
    private static String toJson(String source) {
        Map<String, String> queryPairs = new HashMap<>();
        Arrays.stream(source.split("&")).forEach(p -> {
            int delimiterIndex = p.indexOf("=");
            queryPairs.put(p.substring(0, delimiterIndex), p.substring(delimiterIndex + 1));
        });
        return gson.toJson(queryPairs);
    }
}