All Downloads are FREE. Search and download functionalities are using the official Maven repository.

controllers.impl.StandardAuthentication Maven / Gradle / Ivy

The newest version!
/**
 * Copyright 2015 Groupon.com
 *
 * 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 controllers.impl;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Lists;
import com.typesafe.config.Config;
import controllers.Authentication;
import org.apache.http.client.utils.URIBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.libs.ws.WSClient;
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Result;
import utils.AuthN;

import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.SecureRandom;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * Holds methods for authentication.
 *
 * @author Brandon Arp (barp at groupon dot com)
 */
@Singleton
public class StandardAuthentication extends Controller implements Authentication {

    /**
     * Public constructor.
     *
     * @param client ws client to use
     * @param config Play configuration
     */
    @Inject
    public StandardAuthentication(final WSClient client, final Config config) {
        _client = client;
        _config = config;
    }

    @Override
    public CompletionStage auth(final String redirectUrl) {
        final String baseURL = _config.getString("auth.ghe.baseURL");
        final String clientId = _config.getString("auth.ghe.clientId");

        final String state = new BigInteger(160, RANDOM).toString(32);
        // TODO(barp): This should go in the database or memcached
        URL_CACHE.put(state, redirectUrl);

        final URI uri;
        try {
            uri = apiUriBuilder(baseURL, "/login/oauth/authorize")
                    .addParameter("client_id", clientId)
                    .addParameter("scope", "user:email,read:org")
                    .addParameter("state", state)
                    .build();
        } catch (final URISyntaxException e) {
            LOGGER.error("Unable to build URI for GHE authentication", e);
            return CompletableFuture.completedFuture(internalServerError());
        }

        LOGGER.info("Redirecting login to " + uri.toString());

        return CompletableFuture.completedFuture(redirect(uri.toString()));
    }

    @Override
    public CompletionStage finishAuth(final String code, final String state) {
        final String baseURL = _config.getString("auth.ghe.baseURLApi");
        final String clientId = _config.getString("auth.ghe.clientId");
        final String clientSecret = _config.getString("auth.ghe.clientSecret");
        final String redirect = URL_CACHE.getIfPresent(state);
        LOGGER.info(String.format("Got an auth callback; code=%s, state=%s", code, state));

        if (redirect != null) {
            URL_CACHE.invalidate(state);

            //First, we need to get the access token to make the organizations API call
            final String relativePath = "/login/oauth/access_token";

            final URI tokenPostUri;
            try {
                tokenPostUri = apiUriBuilder(baseURL, relativePath)
                        .addParameter("client_id", clientId)
                        .addParameter("client_secret", clientSecret)
                        .addParameter("code", code)
                        .build();
            } catch (final URISyntaxException e) {
                LOGGER.error("Unable to build URI for GHE authentication", e);
                return CompletableFuture.completedFuture(internalServerError());
            }
            return _client
                    .url(tokenPostUri.toString())
                    .addHeader(Http.HeaderNames.ACCEPT, "application/json")
                    .post("")
                    .thenCompose(wsResponse -> {
                        final String response = wsResponse.getBody();
                        LOGGER.info(response);
                        final JsonNode tokenJson = wsResponse.asJson();
                        final String accessToken = tokenJson.get("access_token").asText();
                        LOGGER.info(String.format("Got access token; token=%s", accessToken));
                        return lookupUsername(baseURL, accessToken).thenCompose(userName -> {
                                LOGGER.info(String.format("Found user name; name=%s", userName));
                                return lookupUserOrgs(baseURL, accessToken).thenApply(orgs -> {
                                        LOGGER.info(String.format("Found orgs for user; user=%s, orgs=%s", userName, orgs));
                                        AuthN.initializeAuthenticatedSession(ctx(), userName, orgs);
                                        AuthN.storeToken(userName, accessToken);
                                        return redirect(redirect);
                                    }
                                );
                            }
                        );
                    });
        } else {
            LOGGER.info("URL_CACHE: " + URL_CACHE);
            for (final Map.Entry entry : URL_CACHE.asMap().entrySet()) {
                LOGGER.info(String.format("entry:   %s :: %s", entry.getKey(), entry.getValue()));
            }
            return CompletableFuture.completedFuture(unauthorized("Something went wrong. Please try logging in again."));
        }
    }

    private CompletionStage lookupUsername(final String baseURL, final String token) {
        return _client
                .url(apiUri(baseURL, "user").toString())
                .addHeader(Http.HeaderNames.ACCEPT, "application/json")
                .addHeader(Http.HeaderNames.AUTHORIZATION, String.format("token %s", token))
                .get()
                .thenApply(wsResponse -> {
                        //We have the user details, but still need to fetch the organizations
                        final JsonNode userJson = wsResponse.asJson();
                        return userJson.get("login").asText();
                    }
                );
    }

    private CompletionStage> lookupUserOrgs(final String baseURL, final String token) {
        return _client
                .url(apiUri(baseURL, "user/orgs").toString())
                .addHeader(Http.HeaderNames.ACCEPT, "application/json")
                .addHeader(Http.HeaderNames.AUTHORIZATION, String.format("token %s", token))
                .get()
                .thenApply(wsResponse -> {
                        final ArrayNode orgs = (ArrayNode) wsResponse.asJson();
                        final List orgList = Lists.newArrayList();
                        for (final JsonNode org : orgs) {
                            final String orgName = org.get("login").asText();
                            orgList.add(orgName);
                        }

                        return orgList;
                    }
                );
    }

    private static URI apiUri(final String baseURL, final String relativePath) {
        try {
            return apiUriBuilder(baseURL, relativePath).build();
        } catch (final URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

    private static URIBuilder apiUriBuilder(final String baseURL, final String relativePath) {
        final URI tokenUri;
        try {
            tokenUri = new URI(baseURL).resolve(relativePath);
        } catch (final URISyntaxException e) {
            LOGGER.error(String.format("Unable to parse baseURL for GHE authentication; baseURL=%s", baseURL), e);
            throw new RuntimeException(e);
        }
        return new URIBuilder(tokenUri);
    }

    @Override
    public CompletionStage logout() {
        AuthN.logout(ctx());
        return CompletableFuture.completedFuture(redirect("/loggedout"));
    }

    private final WSClient _client;
    private final Config _config;

    private static final Cache URL_CACHE = CacheBuilder.newBuilder().expireAfterWrite(90, TimeUnit.SECONDS).build();
    private static final Logger LOGGER = LoggerFactory.getLogger(StandardAuthentication.class);
    private static final SecureRandom RANDOM = new SecureRandom();
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy