com.microsoftopentechnologies.auth.AuthenticationContext.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoftopentechnologies.auth.AuthenticationContext.java

Source

/**
 * Copyright 2014 Microsoft Open Technologies Inc.
 * <p/>
 * 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
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * 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.microsoftopentechnologies.auth;

import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.io.CharStreams;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.microsoftopentechnologies.auth.browser.BrowserLauncher;
import com.microsoftopentechnologies.auth.browser.BrowserLauncherDefault;
import com.microsoftopentechnologies.auth.utils.EncodingHelper;
import com.microsoftopentechnologies.auth.utils.JsonUtils;
import com.microsoftopentechnologies.auth.webserver.AADWebServer;
import com.microsoftopentechnologies.auth.webserver.ClosedCallback;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;

public class AuthenticationContext {
    private String authority;

    private final String AUTHORIZE_ENDPOINT_TEMPLATE = "https://{host}/{tenant}/oauth2/authorize";
    private final String TOKEN_ENDPOINT_TEMPLATE = "https://{host}/{tenant}/oauth2/token";

    private AADWebServer webServer = null;

    private ReentrantLock authCodeLock = new ReentrantLock();
    private boolean gotAuthCode = false;

    private BrowserLauncher browserLauncher = new BrowserLauncherDefault();

    public AuthenticationContext(final String authority) throws IOException {
        this.authority = authority;
    }

    public void dispose() {
        if (webServer != null) {
            webServer.stop();
            webServer = null;
        }
    }

    public void setBrowserLauncher(BrowserLauncher browserLauncher) {
        this.browserLauncher = browserLauncher;
    }

    public ListenableFuture<AuthenticationResult> acquireTokenInteractiveAsync(final String tenantName,
            final String resource, final String clientId, final String redirectUri, final String promptValue)
            throws IOException {
        return acquireTokenInteractiveAsync(tenantName, resource, clientId, redirectUri,
                "Sign in to your Microsoft account", promptValue);
    }

    public ListenableFuture<AuthenticationResult> acquireTokenInteractiveAsync(final String tenantName,
            final String resource, final String clientId, final String redirectUri, final String windowTitle,
            final String promptValue) throws IOException {

        final SettableFuture<AuthenticationResult> future = SettableFuture.create();

        // get the auth code
        ListenableFuture<String> authCodeFuture = acquireAuthCodeInteractiveAsync(tenantName, resource, clientId,
                redirectUri, windowTitle, promptValue);
        Futures.addCallback(authCodeFuture, new FutureCallback<String>() {
            @Override
            public void onSuccess(String code) {
                OutputStream output = null;
                BufferedReader reader = null;

                try {
                    // if code is null then the user cancelled the auth
                    if (code == null) {
                        future.set(null);
                        return;
                    }

                    URL adAuthEndpointUrl = new URL(
                            TOKEN_ENDPOINT_TEMPLATE.replace("{host}", authority).replace("{tenant}", tenantName));

                    // build the a/d auth params
                    Map<String, String> params = new HashMap<String, String>();
                    params.put(OAuthParameter.clientId, clientId);
                    params.put(OAuthParameter.code, code);
                    params.put(OAuthParameter.grantType, OAuthGrantType.AuthorizationCode);
                    params.put(OAuthParameter.redirectUri, redirectUri);
                    params.put(OAuthParameter.resource, resource);
                    byte[] requestData = EncodingHelper.toQueryString(params).getBytes(Charsets.UTF_8);

                    // make a POST request to the endpoint with this data
                    HttpURLConnection connection = (HttpURLConnection) adAuthEndpointUrl.openConnection();
                    connection.setRequestMethod("POST");
                    connection.setDoOutput(true);
                    connection.setDoInput(true);
                    connection.setUseCaches(false);
                    connection.setRequestProperty("Content-Type",
                            "application/x-www-form-urlencoded; charset=" + Charsets.UTF_8.name());
                    connection.setRequestProperty("Content-Length", Integer.toString(requestData.length));
                    output = connection.getOutputStream();
                    output.write(requestData);
                    output.close();
                    output = null;

                    // read the response
                    int statusCode = connection.getResponseCode();
                    if (statusCode != HttpURLConnection.HTTP_OK) {
                        // TODO: Is IOException the right exception type to raise?
                        String err = CharStreams.toString(new InputStreamReader(connection.getErrorStream()));
                        future.setException(new IOException("AD Auth token endpoint returned HTTP status code "
                                + Integer.toString(statusCode) + ". Error info: " + err));
                        return;
                    }

                    reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                    StringBuilder sb = new StringBuilder();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        sb.append(line);
                    }
                    reader.close();
                    reader = null;

                    // parse the JSON
                    String response = sb.toString();
                    JsonParser parser = new JsonParser();
                    JsonObject root = (JsonObject) parser.parse(response);

                    // construct the authentication result object
                    AuthenticationResult result = new AuthenticationResult(
                            JsonUtils.getJsonStringProp(root, OAuthReservedClaim.TokenType),
                            JsonUtils.getJsonStringProp(root, OAuthReservedClaim.AccessToken),
                            JsonUtils.getJsonStringProp(root, OAuthReservedClaim.RefreshToken),
                            JsonUtils.getJsonLongProp(root, OAuthReservedClaim.ExpiresOn),
                            JsonUtils.getJsonStringProp(root, OAuthReservedClaim.Resource),
                            UserInfo.parse(JsonUtils.getJsonStringProp(root, OAuthReservedClaim.IdToken)));
                    future.set(result);
                } catch (Exception e) {
                    future.setException(e);
                } finally {
                    try {
                        if (output != null) {
                            output.close();
                        }
                        if (reader != null) {
                            reader.close();
                        }
                    } catch (IOException ignored) {
                    }
                }
            }

            @Override
            public void onFailure(Throwable throwable) {
                future.setException(throwable);
            }
        });

        return future;
    }

    public AuthenticationResult acquireTokenByRefreshToken(AuthenticationResult authenticationResult,
            String tenantName, String resource, String clientId) throws IOException {

        URL adAuthEndpointUrl = new URL(
                TOKEN_ENDPOINT_TEMPLATE.replace("{host}", authority).replace("{tenant}", tenantName));

        // build the a/d auth params
        Map<String, String> params = new HashMap<String, String>();
        params.put(OAuthParameter.clientId, clientId);
        params.put(OAuthParameter.grantType, OAuthGrantType.RefreshToken);
        params.put(OAuthParameter.refreshToken, authenticationResult.getRefreshToken());
        if (resource != null) {
            params.put(OAuthParameter.resource, resource);
        }
        byte[] requestData = EncodingHelper.toQueryString(params).getBytes(Charsets.UTF_8);

        // make a POST request to the endpoint with this data
        HttpURLConnection connection = (HttpURLConnection) adAuthEndpointUrl.openConnection();
        connection.setRequestMethod("POST");
        connection.setDoOutput(true);
        connection.setDoInput(true);
        connection.setUseCaches(false);
        connection.setRequestProperty("Content-Type",
                "application/x-www-form-urlencoded; charset=" + Charsets.UTF_8.name());
        connection.setRequestProperty("Content-Length", Integer.toString(requestData.length));
        OutputStream output = connection.getOutputStream();
        output.write(requestData);
        output.close();

        // read the response
        int statusCode = connection.getResponseCode();
        if (statusCode != HttpURLConnection.HTTP_OK) {
            // TODO: Is IOException the right exception type to raise?
            InputStream errorStream = null;
            InputStreamReader errorReader = null;
            String err = "";
            try {
                errorStream = connection.getErrorStream();
                errorReader = new InputStreamReader(errorStream);
                err = CharStreams.toString(errorReader);
            } finally {
                if (errorStream != null) {
                    errorStream.close();
                }
                if (errorReader != null) {
                    errorReader.close();
                }
            }

            throw new IOException("AD Auth token endpoint returned HTTP status code " + Integer.toString(statusCode)
                    + ". Error info: " + err);
        }

        BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        reader.close();

        // parse the JSON
        String response = sb.toString();
        JsonParser parser = new JsonParser();
        JsonObject root = (JsonObject) parser.parse(response);

        // update the authentication result object
        return new AuthenticationResult(JsonUtils.getJsonStringProp(root, OAuthReservedClaim.TokenType),
                JsonUtils.getJsonStringProp(root, OAuthReservedClaim.AccessToken),
                JsonUtils.getJsonStringProp(root, OAuthReservedClaim.RefreshToken),
                JsonUtils.getJsonLongProp(root, OAuthReservedClaim.ExpiresOn),
                JsonUtils.getJsonStringProp(root, OAuthReservedClaim.Resource), authenticationResult.getUserInfo());
    }

    private ListenableFuture<String> acquireAuthCodeInteractiveAsync(final String tenantName, final String resource,
            final String clientId, final String redirectUri, final String windowTitle, final String promptValue)
            throws IOException {

        final SettableFuture<String> future = SettableFuture.create();

        try {
            String correlationId = UUID.randomUUID().toString();

            boolean noShell = PromptValue.attemptNone.equals(promptValue);

            // build the a/d auth URI params
            Map<String, String> params = new HashMap<String, String>();
            params.put(OAuthParameter.resource, resource);
            params.put(OAuthParameter.clientId, clientId);
            params.put(OAuthParameter.responseType, OAuthResponseType.code);
            params.put(OAuthParameter.redirectUri, redirectUri);
            params.put(OAuthParameter.correlationId, correlationId);
            params.put(OAuthParameter.prompt, promptValue);
            params.put("site_id", "500879");
            params.put("display", "popup");
            String query = EncodingHelper.toQueryString(params);

            // build the actual URI
            String adUri = AUTHORIZE_ENDPOINT_TEMPLATE.replace("{host}", authority).replace("{tenant}", tenantName);
            adUri = adUri + "?" + query;

            // initialize and start up web server
            if (webServer == null) {
                webServer = new AADWebServer();
                webServer.start();
            }

            webServer.setAuthCodeCallback(new AuthCodeCallback() {
                @Override
                public void onAuthCodeReceived(String code, Map<String, String> params) {
                    authCodeLock.lock();
                    try {
                        gotAuthCode = true;

                        if (Strings.isNullOrEmpty(code)) {
                            String msg = "An error occurred during authentication. 'code' is null/empty.";
                            if (params.containsKey("error")) {
                                msg += "\nError code: " + params.get("error");
                            }
                            if (params.containsKey("error_description")) {
                                msg += "\nDescription: " + params.get("error_description");
                            }

                            future.setException(new IllegalArgumentException(msg));
                        } else {
                            future.set(code);
                        }
                    } finally {
                        authCodeLock.unlock();
                    }
                }
            });

            webServer.setClosedCallback(new ClosedCallback() {
                @Override
                public void onClosed() {
                    authCodeLock.lock();
                    try {
                        if (!gotAuthCode) {
                            future.set(null);
                        }
                    } finally {
                        authCodeLock.unlock();
                    }
                }
            });

            // start the browser
            try {
                ListenableFuture<Void> launchFuture = browserLauncher.browseAsync(adUri, redirectUri,
                        webServer.getBaseURL().toString(), windowTitle, noShell);

                Futures.addCallback(launchFuture, new FutureCallback<Void>() {
                    @Override
                    public void onSuccess(Void result) {
                        // do nothing in case browser launch is successful
                    }

                    @Override
                    public void onFailure(Throwable t) {
                        future.setException(t);
                    }
                });

            } catch (MalformedURLException ignored) {
            }
        } catch (UnsupportedEncodingException e) {
            future.setException(e);
        } catch (MalformedURLException e) {
            future.setException(e);
        }

        return future;
    }
}