BasicVerifier.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.auth;

import org.entrystore.Entry;
import org.entrystore.GraphType;
import org.entrystore.PrincipalManager;
import org.entrystore.User;
import org.entrystore.config.Config;
import org.entrystore.repository.config.Settings;
import org.entrystore.repository.security.Password;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.Status;
import org.restlet.security.Verifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


/**
 * Does a simple lookup for the secret of a principal.
 *
 * @author Hannes Ebner
 */
public class BasicVerifier implements Verifier {

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

	private final PrincipalManager pm;
	private final Map<String, Long> loginCache = new ConcurrentHashMap<>();
	private final List<String> passwordLoginWhitelist;

	public BasicVerifier(PrincipalManager pm, Config config) {
		this.pm = pm;
		if ("whitelist".equalsIgnoreCase(config.getString(Settings.AUTH_PASSWORD))) {
			this.passwordLoginWhitelist = config.getStringList(Settings.AUTH_PASSWORD_WHITELIST, new ArrayList<>());
		} else {
			passwordLoginWhitelist = null;
		}
	}

	public static String getSaltedHashedSecret(PrincipalManager pm , String identifier) {
		URI authUser = pm.getAuthenticatedUserURI();
		try {
			pm.setAuthenticatedUserURI(pm.getAdminUser().getURI());
			Entry userEntry = pm.getPrincipalEntry(identifier);
			if (userEntry != null && GraphType.User.equals(userEntry.getGraphType())) {
				User user = ((User) userEntry.getResource());
				if (user.getSaltedHashedSecret() != null) {
					return user.getSaltedHashedSecret();
				} else {
					log.error("No secret found for principal: " + identifier);
				}
			}
		} finally {
			pm.setAuthenticatedUserURI(authUser);
		}

		return null;
	}

	public static boolean userExists(PrincipalManager pm, String userName) {
		if (userName == null) {
			return false;
		}

		URI currentUser = pm.getAuthenticatedUserURI();
		try {
			pm.setAuthenticatedUserURI(pm.getAdminUser().getURI());
			Entry userEntry = pm.getPrincipalEntry(userName);
			if (userEntry != null) {
				return true;
			}
		} finally {
			pm.setAuthenticatedUserURI(currentUser);
		}
		return false;
	}

	public static boolean isUserDisabled(PrincipalManager pm, String userName) {
		URI currentUser = pm.getAuthenticatedUserURI();
		try {
			pm.setAuthenticatedUserURI(pm.getAdminUser().getURI());
			Entry userEntry = pm.getPrincipalEntry(userName);
			if (userEntry != null) {
				return ((User) userEntry.getResource()).isDisabled();
			}
		} finally {
			pm.setAuthenticatedUserURI(currentUser);
		}
		return false;
	}

	@Override
	public int verify(Request request, Response response) {
		// to avoid an override of an already existing authentication, e.g. from CookieVerifier
		URI authUser = pm.getAuthenticatedUserURI();
		if (authUser != null && !pm.getGuestUser().getURI().equals(authUser)) {
			return RESULT_VALID;
		}

		URI userURI = null;
		boolean challenge = !"false".equalsIgnoreCase(response.getRequest().getResourceRef().getQueryAsForm().getFirstValue("auth_challenge"));

		try {
			if (request.getChallengeResponse() == null && "basic".equals(request.getResourceRef().getLastSegment())) {
				if (challenge) {
					return RESULT_MISSING;
				} else {
					// workaround to avoid challenge response window in browsers
					userURI = pm.getGuestUser().getURI();
					response.setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
					return RESULT_VALID;
				}
			}

			String identifier = null;
			String secret = null;
			ChallengeResponse cr = request.getChallengeResponse();
			if (cr == null) {
				identifier = "_guest";
			} else {
				identifier = request.getChallengeResponse().getIdentifier();
				secret = String.valueOf(request.getChallengeResponse().getSecret());
			}

			pm.setAuthenticatedUserURI(pm.getAdminUser().getURI());

			if (identifier == null) {
				return RESULT_MISSING;
			}

			if ("_guest".equals(identifier)) {
				userURI = pm.getGuestUser().getURI();
				return RESULT_VALID;
			}

			if (secret == null) {
				return RESULT_MISSING;
			}

			if (secret.length() > Password.PASSWORD_MAX_LENGTH) {
				return RESULT_UNSUPPORTED;
			}

			identifier = identifier.toLowerCase();
			if (passwordLoginWhitelist != null && !passwordLoginWhitelist.contains(identifier)) {
				response.setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
				return RESULT_INVALID;
			}

			Entry userEntry = pm.getPrincipalEntry(identifier);
			if (userEntry == null) {
				return RESULT_UNKNOWN;
			}

			// check whether login is cached, setting max age to 1 hour (3600 seconds)
			if (isLoginCached(userEntry.getEntryURI().toString(), secret, 3600)) {
				userURI = userEntry.getResourceURI();
				return RESULT_VALID;
			}

			if (!isUserDisabled(pm, identifier) && Password.check(secret, getSaltedHashedSecret(pm, identifier))) {
				userURI = userEntry.getResourceURI();
				addLoginToCache(userEntry.getEntryURI().toString(), secret);
				return RESULT_VALID;
			} else {
				// workaround to avoid challenge response window in browsers
				if (!challenge) {
					userURI = pm.getGuestUser().getURI();
					response.setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
					return RESULT_VALID;
				}
			}
			return RESULT_INVALID;
		} finally {
			pm.setAuthenticatedUserURI(userURI);
		}
	}

	private boolean addLoginToCache(String user, String password) {
		String hash = Password.sha256(user + password);
		if (hash != null) {
			loginCache.put(hash, new Date().getTime());
			return true;
		}
		return false;
	}

	private boolean isLoginCached(String user, String password, long seconds) {
		String hash = Password.sha256(user + password);
		if (hash == null || !loginCache.containsKey(hash)) {
			return false;
		}
		long loginTime = loginCache.get(hash);
		long expirationTime = new Date().getTime() - (seconds * 1000);
		if (loginTime < expirationTime) {
			log.info("Login has expired for user " + user);
			loginCache.remove(hash);
			return false;
		}
		return true;
	}
}