com.yfiton.oauth.OAuthNotifier.java Source code

Java tutorial

Introduction

Here is the source code for com.yfiton.oauth.OAuthNotifier.java

Source

/*
 * Copyright 2015 Laurent Pellegrino
 *
 * 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 com.yfiton.oauth;

import com.yfiton.api.NotificationResult;
import com.yfiton.api.Notifier;
import com.yfiton.api.exceptions.NotificationException;
import com.yfiton.api.parameter.Parameters;
import com.yfiton.oauth.receiver.GraphicalReceiver;
import com.yfiton.oauth.receiver.PromptReceiver;
import com.yfiton.oauth.receiver.graphical.WebEngineListener;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalINIConfiguration;
import org.slf4j.Logger;

import java.awt.*;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

/**
 * Provides basic abstractions for retrieving access token from third-party
 * service using OAuth 2.0 before sending a notification.
 * <p/>
 * The standard workflow involves two steps. The first step is used to get an
 * authorization code with {@link #requestAuthorizationData(String, String...)}.
 * This last relies on {@link #getAuthorizationUrl(String)}
 * to retrieve the URL to open for getting an authorization code. If the
 * environment is headless, the user will see a message that asks for opening
 * the URL and to copy and paste the code manually. Otherwise, if a graphical
 * environment is available, a windows is opened to forward the user to the
 * required Web page.
 * <p/>
 * The second step usually consists in trading the authorization code for an
 * access token. You need to define your own behaviour for this operation by
 * overriding {@link #requestAccessTokenData(AuthorizationData)}.
 *
 * @author lpellegr
 */
public abstract class OAuthNotifier extends Notifier {

    public static final String YFITON_OAUTH_CALLBACK_URL = "http://oauth.yfiton.com/callback";

    protected static final String KEY_ACCESS_TOKEN = "accessToken";

    protected final String clientId;

    protected final String clientSecret;

    protected final Class<? extends PromptReceiver> promptReceiverClazz;

    protected final Class<? extends WebEngineListener> webEngineListenerClazz;

    /**
     * Creates a new notifier instance supporting OAuth authentication.
     *
     * @param clientId               third-party service client identifier.
     * @param clientSecret           third-party service client secret.
     * @param promptReceiverClazz    the class associated to the receiver used to get authorization data on headless environments
     * @param webEngineListenerClazz
     */
    public OAuthNotifier(String clientId, String clientSecret, Class<? extends PromptReceiver> promptReceiverClazz,
            Class<? extends WebEngineListener> webEngineListenerClazz) {
        super();

        this.clientId = clientId;
        this.clientSecret = clientSecret;

        this.promptReceiverClazz = promptReceiverClazz;
        this.webEngineListenerClazz = webEngineListenerClazz;
    }

    /**
     * Returns the URL to use for retrieving an authorization code.
     *
     * @param stateParameterValue the value to pass for the state parameter value
     *                            if supported by the third-party service. This one
     *                            is used for security purposes in order to prevent
     *                            Cross Site Request Forgery (XRSF).
     * @return
     */
    protected abstract String getAuthorizationUrl(String stateParameterValue) throws NotificationException;

    /**
     * Returns the name of the request parameter used to get authorization code.
     *
     * @return the name of the request parameter used to get authorization code.
     */
    protected String getCodeRequestParameterName() {
        return "code";
    }

    /**
     * Optionally returns the name of the request parameter used for checking OAuth
     * state in order to prevent Cross Site Request Forgery (XRSF).
     *
     * @return the name of the request parameter used for checking OAuth
     * state in order to prevent Cross Site Request Forgery (XRSF).
     */
    protected abstract Optional<String> getStateRequestParameterName();

    /**
     * Returns a boolean indicating whether it is required or not to execute
     * the flow to get an access token.
     *
     * @return {@code true} if authentication is required, {@code false otherwise},
     */
    protected boolean isAuthenticationRequired() {
        return !getConfiguration().containsKey(KEY_ACCESS_TOKEN);
    }

    @Override
    public NotificationResult handle(Parameters parameters) throws NotificationException {
        if (isAuthenticationRequired()) {
            String[] requestParameterNames = new String[0];

            Optional<String> stateRequestParameterName = getStateRequestParameterName();
            if (stateRequestParameterName.isPresent()) {
                requestParameterNames = new String[] { stateRequestParameterName.get() };
            }

            executeOAuthLogin(getCodeRequestParameterName(), requestParameterNames);
        }

        return super.handle(parameters);
    }

    protected String executeOAuthLogin(String authorizationCodeParameterName, String... requestParameterNames)
            throws NotificationException {
        AuthorizationData authorizationData = requestAuthorizationData(authorizationCodeParameterName,
                requestParameterNames);

        AccessTokenData accessTokenData = requestAccessTokenData(authorizationData);

        storeAccessTokenData(accessTokenData, getConfiguration());

        return accessTokenData.getAccessToken();
    }

    protected AuthorizationData requestAuthorizationData(String authorizationCodeParameterName,
            String... requestParameterNames) throws NotificationException {
        log.trace("First call requires to get authorization");

        String stateParameterValue = UUID.randomUUID().toString();
        String authorizationUrl = getAuthorizationUrl(stateParameterValue);

        log.debug("Opening {} to get authorization", authorizationUrl);

        AuthorizationData authorizationData;

        if (GraphicsEnvironment.isHeadless() || isHeadlessEnforced()) {
            log.debug("Headless mode used");
            authorizationData = getAuthorizationDataHeadless(authorizationUrl, authorizationCodeParameterName,
                    requestParameterNames);
        } else {
            log.debug("Graphical mode used");
            authorizationData = getAuthorizationDataUsingScreen(authorizationUrl, authorizationCodeParameterName);
        }

        checkState(stateParameterValue, authorizationData);

        return authorizationData;
    }

    private void checkState(String stateParameterValue, AuthorizationData authorizationData)
            throws NotificationException {
        if (getStateRequestParameterName().isPresent()
                && !stateParameterValue.equals(authorizationData.get(getStateRequestParameterName().get()))) {
            throw new NotificationException("Invalid state value",
                    "Unauthorized access detected: Cross Site Request Forgery (XRSF)",
                    "https://en.wikipedia.org/wiki/Cross-site_request_forgery");
        }
    }

    protected abstract AccessTokenData requestAccessTokenData(AuthorizationData authorizationCode)
            throws NotificationException;

    protected void storeAccessTokenData(AccessTokenData accessTokenData, HierarchicalINIConfiguration configuration)
            throws NotificationException {
        configuration.setProperty(KEY_ACCESS_TOKEN, accessTokenData.getAccessToken());

        for (Map.Entry<String, String> entry : accessTokenData.getData()) {
            configuration.setProperty(entry.getKey(), entry.getValue());
        }

        try {
            configuration.save();
        } catch (ConfigurationException e) {
            throw new NotificationException(e);
        }
    }

    protected abstract Optional<String> getAccessTokenUrl(String authorizationCode);

    private boolean isHeadlessEnforced() {
        String headlessEnforced = System.getProperty("yfiton.headless.enforced");
        return headlessEnforced != null && headlessEnforced.equalsIgnoreCase("true");
    }

    private AuthorizationData getAuthorizationDataHeadless(String authorizationUrl,
            String authorizationCodeParameterName, String[] requestParameterNames) throws NotificationException {
        log.info("Please open the following URL in a web browser:");
        log.info(authorizationUrl);

        try {
            return promptReceiverClazz.getConstructor(Logger.class).newInstance(log).requestAuthorizationData(
                    authorizationUrl, authorizationCodeParameterName, requestParameterNames);
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException
                | InvocationTargetException e) {
            throw new NotificationException(e.getMessage());
        }
    }

    protected AuthorizationData getAuthorizationDataUsingScreen(String authorizationUrl,
            String authorizationCodeParameterName) throws NotificationException {
        GraphicalReceiver graphicalReceiver = new GraphicalReceiver(webEngineListenerClazz,
                log.isDebugEnabled() || log.isTraceEnabled());

        return graphicalReceiver.requestAuthorizationData(authorizationUrl, authorizationCodeParameterName);
    }

    public String getClientId() {
        return clientId;
    }

    public String getClientSecret() {
        return clientSecret;
    }

}