org.eclipse.smarthome.auth.oauth2client.internal.OAuthConnector.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.smarthome.auth.oauth2client.internal.OAuthConnector.java

Source

/**
 * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.smarthome.auth.oauth2client.internal;

import static org.eclipse.smarthome.auth.oauth2client.internal.Keyword.*;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.FormContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.Fields;
import org.eclipse.smarthome.core.auth.client.oauth2.AccessTokenResponse;
import org.eclipse.smarthome.core.auth.client.oauth2.OAuthException;
import org.eclipse.smarthome.core.auth.client.oauth2.OAuthResponseException;
import org.eclipse.smarthome.io.net.http.HttpClientFactory;
import org.eclipse.smarthome.io.net.http.TrustManagerProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;

/**
 * Implementation of the OAuthConnector. It directly deals with the underlying http connections (using Jetty).
 * This is meant for internal use. OAuth2client's clients should look into {@code OAuthClientService} or
 * {@code OAuthFactory}
 *
 * @author Michael Bock - Initial contribution
 * @author Gary Tse - ESH adaptation
 *
 */
@NonNullByDefault
public class OAuthConnector {

    private static final String HTTP_CLIENT_CONSUMER_NAME = "OAuthConnector";

    private final HttpClientFactory httpClientFactory;

    private final Logger logger = LoggerFactory.getLogger(OAuthConnector.class);
    private final Gson gson;

    public OAuthConnector(HttpClientFactory httpClientFactory) {
        this.httpClientFactory = httpClientFactory;
        gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
    }

    /**
     * Authorization Code Grant
     *
     * @param authorizationEndpoint The end point of the authorization provider that performs authorization of the
     *            resource owner
     * @param clientId Client identifier (will be URL-encoded)
     * @param redirectURI RFC 6749 section 3.1.2 (will be URL-encoded)
     * @param state Recommended to enhance security (will be URL-encoded)
     * @param scope Optional space separated list of scope (will be URL-encoded)
     *
     * @return A URL based on the authorizationEndpoint, with query parameters added.
     * @see https://tools.ietf.org/html/rfc6749#section-4.1.1
     */
    public String getAuthorizationUrl(String authorizationEndpoint, String clientId, @Nullable String redirectURI,
            @Nullable String state, @Nullable String scope) {
        StringBuilder authorizationUrl = new StringBuilder(authorizationEndpoint);

        if (authorizationUrl.indexOf("?") == -1) {
            authorizationUrl.append('?');
        } else {
            authorizationUrl.append('&');
        }

        try {
            authorizationUrl.append("response_type=code");
            authorizationUrl.append("&client_id=")
                    .append(URLEncoder.encode(clientId, StandardCharsets.UTF_8.name()));
            if (state != null) {
                authorizationUrl.append("&state=").append(URLEncoder.encode(state, StandardCharsets.UTF_8.name()));
            }
            if (redirectURI != null) {
                authorizationUrl.append("&redirect_uri=")
                        .append(URLEncoder.encode(redirectURI, StandardCharsets.UTF_8.name()));
            }
            if (scope != null) {
                authorizationUrl.append("&scope=").append(URLEncoder.encode(scope, StandardCharsets.UTF_8.name()));
            }
        } catch (UnsupportedEncodingException e) {
            // never happens
            logger.error("Unknown encoding {}", e.getMessage(), e);
        }
        return authorizationUrl.toString();
    }

    /**
     * Resource Owner Password Credentials Grant
     *
     * @see https://tools.ietf.org/html/rfc6749#section-4.3
     *
     * @param tokenUrl URL of the oauth provider that accepts access token requests.
     * @param username The resource owner username.
     * @param password The resource owner password.
     * @param clientId The client identifier issued to the client during the registration process
     * @param clientSecret The client secret. The client MAY omit the parameter if the client secret is an empty string.
     * @param scope Access Token Scope.
     * @param supportsBasicAuth Determines whether the oauth client should use HTTP Authorization header to the oauth
     *            provider.
     * @return Access Token
     * @throws IOException IO/ network exceptions
     * @throws OAuthException Other exceptions
     * @throws OAuthErrorException Error codes given by authorization provider, as in RFC 6749 section 5.2 Error
     *             Response
     */
    public AccessTokenResponse grantTypePassword(String tokenUrl, String username, String password,
            @Nullable String clientId, @Nullable String clientSecret, @Nullable String scope,
            boolean supportsBasicAuth) throws OAuthResponseException, OAuthException, IOException {

        HttpClient httpClient = null;
        try {
            httpClient = createHttpClient(tokenUrl);
            Request request = getMethod(httpClient, tokenUrl);
            Fields fields = initFields(GRANT_TYPE, PASSWORD, USERNAME, username, PASSWORD, password, SCOPE, scope);

            setAuthentication(clientId, clientSecret, request, fields, supportsBasicAuth);
            return doRequest(PASSWORD, httpClient, request, fields);
        } finally {
            shutdownQuietly(httpClient);
        }
    }

    /**
     * Refresh Token
     *
     * @see https://tools.ietf.org/html/rfc6749#section-6
     *
     * @param tokenUrl URL of the oauth provider that accepts access token requests.
     * @param refreshToken The refresh token, which can be used to obtain new access tokens using authorization grant
     * @param clientId The client identifier issued to the client during the registration process
     * @param clientSecret The client secret. The client MAY omit the parameter if the client secret is an empty string.
     * @param scope Access Token Scope.
     * @param supportsBasicAuth Determines whether the oauth client should use HTTP Authorization header to the oauth
     *            provider.
     * @return Access Token
     * @throws IOException IO/ network exceptions
     * @throws OAuthException Other exceptions
     * @throws OAuthErrorException Error codes given by authorization provider, as in RFC 6749 section 5.2 Error
     *             Response
     */
    public AccessTokenResponse grantTypeRefreshToken(String tokenUrl, String refreshToken,
            @Nullable String clientId, @Nullable String clientSecret, @Nullable String scope,
            boolean supportsBasicAuth) throws OAuthResponseException, OAuthException, IOException {

        HttpClient httpClient = null;
        try {
            httpClient = createHttpClient(tokenUrl);
            Request request = getMethod(httpClient, tokenUrl);
            Fields fields = initFields(GRANT_TYPE, REFRESH_TOKEN, REFRESH_TOKEN, refreshToken, SCOPE, scope);

            setAuthentication(clientId, clientSecret, request, fields, supportsBasicAuth);
            return doRequest(REFRESH_TOKEN, httpClient, request, fields);
        } finally {
            shutdownQuietly(httpClient);
        }
    }

    /**
     * Authorization Code Grant - part (E)
     *
     * @see https://tools.ietf.org/html/rfc6749#section-4.1.3
     *
     * @param tokenUrl URL of the oauth provider that accepts access token requests.
     * @param authorizationCode to be used to trade with the oauth provider for access token
     * @param clientId The client identifier issued to the client during the registration process
     * @param clientSecret The client secret. The client MAY omit the parameter if the client secret is an empty string.
     * @param redirectUrl is the http request parameter which tells the oauth provider the URI to redirect the
     *            user-agent. This may/ may not be present as per agreement with the oauth provider.
     * @param supportsBasicAuth Determines whether the oauth client should use HTTP Authorization header to the oauth
     *            provider
     * @return Access Token
     * @throws IOException IO/ network exceptions
     * @throws OAuthException Other exceptions
     * @throws OAuthErrorException Error codes given by authorization provider, as in RFC 6749 section 5.2 Error
     *             Response
     */
    public AccessTokenResponse grantTypeAuthorizationCode(String tokenUrl, String authorizationCode,
            String clientId, @Nullable String clientSecret, String redirectUrl, boolean supportsBasicAuth)
            throws OAuthResponseException, OAuthException, IOException {
        HttpClient httpClient = null;
        try {
            httpClient = createHttpClient(tokenUrl);
            Request request = getMethod(httpClient, tokenUrl);
            Fields fields = initFields(GRANT_TYPE, AUTHORIZATION_CODE, CODE, authorizationCode, REDIRECT_URI,
                    redirectUrl);

            setAuthentication(clientId, clientSecret, request, fields, supportsBasicAuth);
            return doRequest(AUTHORIZATION_CODE, httpClient, request, fields);
        } finally {
            shutdownQuietly(httpClient);
        }
    }

    /**
     * Client Credentials Grant
     *
     * @see https://tools.ietf.org/html/rfc6749#section-4.4
     *
     * @param tokenUrl URL of the oauth provider that accepts access token requests.
     * @param clientId The client identifier issued to the client during the registration process
     * @param clientSecret The client secret. The client MAY omit the parameter if the client secret is an empty string.
     * @param scope Access Token Scope.
     * @param supportsBasicAuth Determines whether the oauth client should use HTTP Authorization header to the oauth
     *            provider
     * @return Access Token
     * @throws IOException IO/ network exceptions
     * @throws OAuthException Other exceptions
     * @throws OAuthErrorException Error codes given by authorization provider, as in RFC 6749 section 5.2 Error
     *             Response
     */
    public AccessTokenResponse grantTypeClientCredentials(String tokenUrl, String clientId,
            @Nullable String clientSecret, @Nullable String scope, boolean supportsBasicAuth)
            throws OAuthResponseException, OAuthException, IOException {

        HttpClient httpClient = null;
        try {
            httpClient = createHttpClient(tokenUrl);
            Request request = getMethod(httpClient, tokenUrl);
            Fields fields = initFields(GRANT_TYPE, CLIENT_CREDENTIALS, SCOPE, scope);

            setAuthentication(clientId, clientSecret, request, fields, supportsBasicAuth);
            return doRequest(CLIENT_CREDENTIALS, httpClient, request, fields);
        } finally {
            shutdownQuietly(httpClient);
        }
    }

    private Request getMethod(HttpClient httpClient, String tokenUrl) {
        Request request = httpClient.newRequest(tokenUrl).method(HttpMethod.POST);
        request.header(HttpHeader.ACCEPT, "application/json");
        request.header(HttpHeader.ACCEPT_CHARSET, "UTF-8");
        return request;
    }

    private void setAuthentication(@Nullable String clientId, @Nullable String clientSecret, Request request,
            Fields fields, boolean supportsBasicAuth) {
        logger.debug("Setting authentication for clientId {}. Using basic auth {}", clientId, supportsBasicAuth);
        if (supportsBasicAuth && clientSecret != null) {
            String authString = clientId + ":" + clientSecret;
            request.header(HttpHeader.AUTHORIZATION,
                    "Basic " + Base64.getEncoder().encodeToString(authString.getBytes(StandardCharsets.UTF_8)));
        } else {
            if (clientId != null) {
                fields.add(CLIENT_ID, clientId);
            }
            if (clientSecret != null) {
                fields.add(CLIENT_SECRET, clientSecret);
            }
        }
    }

    private Fields initFields(String... parameters) {
        Fields fields = new Fields();

        for (int i = 0; i < parameters.length; i += 2) {
            if (i + 1 < parameters.length && parameters[i] != null && parameters[i + 1] != null) {
                logger.debug("Oauth request parameter {}, value {}", parameters[i], parameters[i + 1]);
                fields.add(parameters[i], parameters[i + 1]);
            }
        }
        return fields;
    }

    private AccessTokenResponse doRequest(final String grantType, HttpClient httpClient, final Request request,
            Fields fields) throws OAuthResponseException, OAuthException, IOException {

        int statusCode = 0;
        String content = "";
        try {
            final FormContentProvider entity = new FormContentProvider(fields);
            final ContentResponse response = AccessController
                    .doPrivileged((PrivilegedExceptionAction<ContentResponse>) () -> {
                        Request requestWithContent = request.content(entity);
                        return requestWithContent.send();
                    });

            statusCode = response.getStatus();
            content = response.getContentAsString();

            if (statusCode == HttpStatus.OK_200) {
                AccessTokenResponse jsonResponse = gson.fromJson(content, AccessTokenResponse.class);
                jsonResponse.setCreatedOn(LocalDateTime.now()); // this is not supplied by the response
                logger.info("grant type {} to URL {} success", grantType, request.getURI());
                return jsonResponse;
            } else if (statusCode == HttpStatus.BAD_REQUEST_400) {
                OAuthResponseException errorResponse = gson.fromJson(content, OAuthResponseException.class);
                logger.error("grant type {} to URL {} failed with error code {}, description {}", grantType,
                        request.getURI(), errorResponse.getError(), errorResponse.getErrorDescription());

                throw errorResponse;
            } else {
                logger.error("grant type {} to URL {} failed with HTTP response code {}", grantType,
                        request.getURI(), statusCode);
                throw new OAuthException("Bad http response, http code " + statusCode);
            }
        } catch (PrivilegedActionException pae) {
            Exception underlyingException = pae.getException();
            if (underlyingException instanceof InterruptedException
                    || underlyingException instanceof TimeoutException
                    || underlyingException instanceof ExecutionException) {
                throw new IOException("Exception in oauth communication, grant type " + grantType,
                        underlyingException);
            }
            // Dont know what exception it is, wrap it up and throw it out
            throw new OAuthException("Exception in oauth communication, grant type " + grantType,
                    underlyingException);
        } catch (JsonSyntaxException e) {
            throw new OAuthException(String.format(
                    "Unable to deserialize json into AccessTokenResponse/ OAuthResponseException. httpCode: %i json: %s",
                    statusCode, content), e);
        }
    }

    /**
     * This is a special case where the httpClient (jetty) is created due to the need for certificate pinning.
     * If ceritificate pinning is needed, please refer to {@code TrustManagerProvider}. The http client is
     * created, used and then shutdown immediately after use. There is little reason to cache the client/ connections
     * because oauth requests are short; and it may take hours/ days before the next request is needed.
     *
     * @param tokenUrl access token url
     * @return http client. This http client
     * @throws OAuthException If any exception is thrown while starting the http client.
     * @see TrustManagerProvider
     */
    private HttpClient createHttpClient(String tokenUrl) throws OAuthException {
        HttpClient httpClient = httpClientFactory.createHttpClient(HTTP_CLIENT_CONSUMER_NAME, tokenUrl);
        if (!httpClient.isStarted()) {
            try {
                AccessController.doPrivileged((PrivilegedExceptionAction<@Nullable Void>) () -> {
                    httpClient.start();
                    return null;
                });
            } catch (Exception e) {
                throw new OAuthException("Exception while starting httpClient, tokenUrl: " + tokenUrl, e);
            }
        }
        return httpClient;
    }

    private void shutdownQuietly(@Nullable HttpClient httpClient) {
        try {
            if (httpClient != null) {
                AccessController.doPrivileged((PrivilegedExceptionAction<@Nullable Void>) () -> {
                    httpClient.stop();
                    return null;
                });
            }
        } catch (Exception e) {
            // there is nothing we can do here
            logger.error("Exception while shutting down httpClient, {}", e.getMessage(), e);
        }
    }

}