com.muk.services.security.DefaultUaaLoginService.java Source code

Java tutorial

Introduction

Here is the source code for com.muk.services.security.DefaultUaaLoginService.java

Source

/*******************************************************************************
 * Copyright (C)  2018  mizuuenikaze inc
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *******************************************************************************/
package com.muk.services.security;

import java.net.HttpCookie;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import com.fasterxml.jackson.databind.JsonNode;
import com.muk.services.api.SecurityConfigurationService;
import com.muk.services.api.UaaLoginService;

/**
 *
 * Implements the login sequence with UAA to provide a custom login flow without spring mvc.
 * <p>
 * The UAA login server requirements are based on a manual browser login. However with an SPA, it is not unreasonable to
 * want a highly customized login flow. Since this would be via xhr or fetch, there are header and cookie restrictions
 * that make it impossible to fulfill the spring mvc requirements to successfully login. This simplifies the login
 * process for an SPA to satisfy the auth code oauth flow.
 * <p>
 * Supports multiple clients as long as an entry is available in the internal keystore.
 *
 */
public class DefaultUaaLoginService implements UaaLoginService {
    private static final String CSRF = "X-Uaa-Csrf";
    private static final Pattern csrfPat = Pattern.compile("X-Uaa-Csrf=(?<csrf>[^;]+)");

    @Inject
    @Qualifier("genericRestTemplate")
    private RestTemplate restTemplate;

    @Inject
    @Qualifier("securityConfigurationService")
    private SecurityConfigurationService cfgService;

    @SuppressWarnings("unchecked")
    @Override
    public Map<String, Object> loginForClient(String username, String password, String clientId,
            UriComponents inUrlComponents) {
        final Map<String, Object> responsePayload = new HashMap<String, Object>();

        final HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));

        final UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(cfgService.getOauthServer());

        // login for csrf
        final UriComponents loginUri = uriBuilder.cloneBuilder().pathSegment("login").build();

        ResponseEntity<String> response = exchangeForType(loginUri.toUriString(), HttpMethod.GET, null, headers,
                String.class);

        final List<String> cookies = new ArrayList<String>();
        cookies.addAll(response.getHeaders().get(HttpHeaders.SET_COOKIE));

        final MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("username", username);
        formData.add("password", password);
        formData.add(CSRF, getCsrf(cookies));

        headers.put(HttpHeaders.COOKIE, translateInToOutCookies(cookies));
        headers.add(HttpHeaders.REFERER, loginUri.toUriString());

        // login.do
        response = exchangeForType(uriBuilder.cloneBuilder().pathSegment("login.do").build().toUriString(),
                HttpMethod.POST, formData, headers, String.class);

        if (response.getStatusCode() != HttpStatus.FOUND
                || response.getHeaders().getFirst(HttpHeaders.LOCATION).contains("login")) {
            responsePayload.put("error", "bad credentials");
            return responsePayload;
        }

        removeCookie(cookies, "X-Uaa-Csrf");
        cookies.addAll(response.getHeaders().get(HttpHeaders.SET_COOKIE));
        removeExpiredCookies(cookies);
        headers.remove(HttpHeaders.REFERER);
        headers.put(HttpHeaders.COOKIE, translateInToOutCookies(cookies));

        // authorize
        final ResponseEntity<JsonNode> authResponse = exchangeForType(
                uriBuilder.cloneBuilder().pathSegment("oauth").pathSegment("authorize")
                        .queryParam("response_type", "code").queryParam("client_id", clientId)
                        .queryParam("redirect_uri", inUrlComponents.toUriString()).build().toUriString(),
                HttpMethod.GET, null, headers, JsonNode.class);

        if (authResponse.getStatusCode() == HttpStatus.OK) {
            removeCookie(cookies, "X-Uaa-Csrf");
            cookies.addAll(authResponse.getHeaders().get(HttpHeaders.SET_COOKIE));
            // return approval data
            final List<HttpCookie> parsedCookies = new ArrayList<HttpCookie>();

            for (final String cookie : cookies) {
                parsedCookies.add(HttpCookie.parse(cookie).get(0));
            }

            responsePayload.put(HttpHeaders.SET_COOKIE, new ArrayList<String>());

            for (final HttpCookie parsedCookie : parsedCookies) {
                if (!parsedCookie.getName().startsWith("Saved-Account")) {
                    parsedCookie.setPath(inUrlComponents.getPath());
                    ((List<String>) responsePayload.get(HttpHeaders.SET_COOKIE))
                            .add(httpCookieToString(parsedCookie));
                }
            }

            responsePayload.put("json", authResponse.getBody());
        } else {
            // get auth_code from Location Header
            responsePayload.put("code", authResponse.getHeaders().getLocation().getQuery().split("=")[1]);
        }

        return responsePayload;
    }

    @Override
    public String approveClient(String approvalQuery, String cookie) {
        final UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(cfgService.getOauthServer());
        final HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));

        final StringTokenizer cookieTokenizer = new StringTokenizer(cookie, "; ");
        while (cookieTokenizer.hasMoreTokens()) {
            headers.add(HttpHeaders.COOKIE, cookieTokenizer.nextToken());
        }

        final MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
        for (final String pair : approvalQuery.split("&")) {
            final String[] nv = pair.split("=");
            formData.add(nv[0], nv[1]);
        }
        formData.add("X-Uaa-Csrf", getCsrf(headers.get(HttpHeaders.COOKIE)));

        final UriComponents loginUri = uriBuilder.cloneBuilder().pathSegment("oauth").pathSegment("authorize")
                .build();

        final ResponseEntity<String> response = exchangeForType(loginUri.toUriString(), HttpMethod.POST, formData,
                headers, String.class);

        if (approvalQuery.contains("false")) {
            return null; // approval declined.
        }

        // accepted, but location contains error
        if (response.getHeaders().getLocation().getQuery().startsWith("error")) {
            throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED,
                    response.getHeaders().getLocation().getQuery());
        }

        // accepted with related auth code
        return response.getHeaders().getLocation().getQuery().split("=")[1];
    }

    private <T> ResponseEntity<T> exchangeForType(String url, HttpMethod method,
            MultiValueMap<String, String> formData, HttpHeaders headers, Class<T> returnType) {
        if (headers.getContentType() == null) {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        }

        final ResponseEntity<T> response = restTemplate.exchange(url, method,
                new HttpEntity<MultiValueMap<String, String>>(formData, headers), returnType);

        return response;
    }

    private String getCsrf(List<String> cookies) {
        for (final String cookie : cookies) {
            if (cookie.contains(CSRF)) {
                final Matcher m = csrfPat.matcher(cookie);
                if (m.find()) {
                    return m.group("csrf");
                }
            }
        }

        return StringUtils.EMPTY;
    }

    private void removeCookie(List<String> cookies, String name) {
        final Iterator<String> iterator = cookies.iterator();

        while (iterator.hasNext()) {
            final String current = iterator.next();
            if (current.startsWith(name)) {
                iterator.remove();
            }
        }
    }

    private void removeExpiredCookies(List<String> cookies) {
        final Iterator<String> iterator = cookies.iterator();

        while (iterator.hasNext()) {
            final String current = iterator.next();
            if (current.toLowerCase().indexOf("max-age=0") >= 0) {
                iterator.remove();
            }
        }
    }

    private String httpCookieToString(HttpCookie cookie) {
        final OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC).plusSeconds(cookie.getMaxAge());
        final String cookieExpires = DateTimeFormatter.RFC_1123_DATE_TIME.format(now);
        final StringBuilder cookieBuilder = new StringBuilder();
        cookieBuilder.append(cookie.getName()).append("=").append(cookie.getValue()).append(";path=")
                .append(cookie.getPath()).append(";max-age=").append(cookie.getMaxAge()).append(";expires=")
                .append(cookieExpires);

        if (cookie.isHttpOnly()) {
            cookieBuilder.append(";HttpOnly");
        }

        return cookieBuilder.toString();
    }

    private List<String> translateInToOutCookies(List<String> inCookies) {
        final List<String> outCookies = new ArrayList<String>();

        for (final String inCookie : inCookies) {
            final HttpCookie httpCookie = HttpCookie.parse(inCookie).get(0);
            final StringBuilder cookieBuilder = new StringBuilder();
            cookieBuilder.append(httpCookie.getName()).append("=").append(httpCookie.getValue());
            outCookies.add(cookieBuilder.toString());
        }

        return outCookies;
    }
}