CookieLoginResource.java

/*
 * Copyright (c) 2007-2017 MetaSolutions AB
 *
 * 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 org.entrystore.rest.resources;

import org.entrystore.config.Config;
import org.entrystore.repository.config.Settings;
import org.entrystore.repository.security.Password;
import org.entrystore.rest.EntryStoreApplication;
import org.entrystore.rest.auth.BasicVerifier;
import org.entrystore.rest.auth.CookieVerifier;
import org.entrystore.rest.util.HttpUtil;
import org.entrystore.rest.util.SimpleHTML;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.Form;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.resource.Post;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;

import static org.restlet.data.MediaType.TEXT_HTML;
import static org.restlet.data.Status.CLIENT_ERROR_REQUEST_ENTITY_TOO_LARGE;

/**
 * This resource checks credentials and sets a cookie.
 * <p>
 * It only allows POST requests to avoid user/password in URL and therefore
 * logging in clear-text.
 *
 * @author Hannes Ebner
 */
public class CookieLoginResource extends BaseResource {

	private static final Logger log = LoggerFactory.getLogger(CookieLoginResource.class);

	private static List<String> passwordLoginWhitelist;

	private static List<String> passwordLoginBlacklist;

	@Override
	public void init(Context c, Request request, Response response) {
		super.init(c, request, response);
		Config config = getRM().getConfiguration();
		if ("whitelist".equalsIgnoreCase(config.getString(Settings.AUTH_PASSWORD))) {
			passwordLoginWhitelist = config.getStringList(Settings.AUTH_PASSWORD_WHITELIST);
		}
		passwordLoginBlacklist = config.getStringList(Settings.AUTH_PASSWORD_BLACKLIST);
	}

	@Post
	public void acceptRepresentation(Representation r) {
		if (HttpUtil.isLargerThan(r, 32768)) {
			log.warn("The size of the representation is larger than 32KB or unknown, request blocked");
			getResponse().setStatus(CLIENT_ERROR_REQUEST_ENTITY_TOO_LARGE);
			return;
		}

		boolean html = TEXT_HTML.equals(getRequest().getClientInfo().getPreferredMediaType(Arrays.asList(TEXT_HTML, MediaType.APPLICATION_ALL)));
		Form query;
		try {
			query = new Form(r);
		} catch (IllegalArgumentException iae) {
			log.warn(iae.getMessage());
			getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
			return;
		}

		String userName = query.getFirstValue("auth_username");
		String password = query.getFirstValue("auth_password");
		String maxAgeStr = query.getFirstValue("auth_maxage");

		if (userName == null || password == null) {
			getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
			return;
		}

		if (password.length() > Password.PASSWORD_MAX_LENGTH) {
			getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
			return;
		}

		String lowerCaseUserName = userName.toLowerCase();

		// Use case for whitelisting: enforced SSO with some users that should be able to log in
		// with their local credentials, see https://entrystore.org/#!KB/Authentication.md
		if ((passwordLoginBlacklist != null && passwordLoginBlacklist.stream().anyMatch(s -> s.equalsIgnoreCase(lowerCaseUserName))) ||
				(passwordLoginWhitelist != null && passwordLoginWhitelist.stream().noneMatch(s -> s.equalsIgnoreCase(lowerCaseUserName)))) {
			getResponse().setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
			if (html) {
				getResponse().setEntity(new SimpleHTML("Login").representation("Login failed."));
			}
			return;
		}

		if (getUserTempLockoutCache().userIsLockedOut(lowerCaseUserName)) {
			getResponse().setStatus(Status.CLIENT_ERROR_TOO_MANY_REQUESTS);
			if (html) {
				getResponse().setEntity(new SimpleHTML("Login").representation("User account is temporarily disabled. Too many failed logins."));
			}
			return;
		}

		String saltedHashedSecret = BasicVerifier.getSaltedHashedSecret(getPM(), lowerCaseUserName);
		boolean userIsEnabled = !BasicVerifier.isUserDisabled(getPM(), lowerCaseUserName);
		try {
			if (saltedHashedSecret != null && Password.check(password, saltedHashedSecret)) {
				if (userIsEnabled) {
					EntryStoreApplication app = (EntryStoreApplication) getApplication();
					new CookieVerifier(app, getRM()).createAuthToken(lowerCaseUserName, maxAgeStr, getRequest(), getResponse());
					getResponse().setStatus(Status.SUCCESS_OK);
					getUserTempLockoutCache().succeedLogin(lowerCaseUserName);
					if (html) {
						getResponse().setEntity(new SimpleHTML("Login").representation("Login successful."));
					}
					return;
				} else {
					getResponse().setStatus(Status.CLIENT_ERROR_FORBIDDEN);
					if (html) {
						getResponse().setEntity(new SimpleHTML("Login").representation("Login failed. The account is disabled."));
					}
					return;
				}
			}
		} catch (IllegalArgumentException iae) {
			log.warn(iae.getMessage());
			getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
			if (html) {
				getResponse().setEntity(new SimpleHTML("Login").representation(iae.getMessage()));
			}
			return;
		}

		getResponse().setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
		getUserTempLockoutCache().failLogin(lowerCaseUserName);
		if (html) {
			getResponse().setEntity(new SimpleHTML("Login").representation("Login failed."));
		}
	}
}