/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2011-2015 ForgeRock AS.
*/
package org.forgerock.openidm.auth;
import static org.forgerock.json.resource.ResourceException.newResourceException;
import org.eclipse.jetty.jaas.spi.UserInfo;
import org.eclipse.jetty.util.security.Password;
import org.forgerock.json.crypto.JsonCryptoException;
import org.forgerock.json.resource.ConnectionFactory;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.services.context.Context;
import org.forgerock.openidm.crypto.CryptoService;
import org.forgerock.util.Reject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Provider;
/**
* Authenticator class which performs authentication against managed/internal user tables using a queryId to fetch
* the complete local user data and validates the password locally.
*/
class ResourceQueryAuthenticator implements Authenticator {
private static final Logger logger = LoggerFactory.getLogger(ResourceQueryAuthenticator.class);
private final Provider<CryptoService> cryptoServiceProvider;
private final Provider<ConnectionFactory> connectionFactoryProvider;
private final String queryOnResource;
private final String queryId;
private final String userRolesProperty;
private final String authenticationIdProperty;
private final String userCredentialProperty;
/**
* Constructs an instance of the ResourceQueryAuthenticator.
* @param cryptoService The CryptoService.
* @param connectionFactory The ConnectionFactory.
* @param queryOnResource The query resource.
* @param queryId The query id.
* @param authenticationIdProperty The user id property.
* @param userCredentialProperty The user credential property.
* @param userRolesProperty The property for reading authorization roles
*/
public ResourceQueryAuthenticator(Provider<CryptoService> cryptoService, Provider<ConnectionFactory> connectionFactory,
String queryOnResource, String queryId, String authenticationIdProperty, String userCredentialProperty, String userRolesProperty) {
Reject.ifNull(cryptoService, "CryptoService is null");
Reject.ifNull(connectionFactory, "ConnectionFactory is null");
Reject.ifNull(queryOnResource, "User query resource was null");
Reject.ifNull(queryId, "Credential query was null");
Reject.ifNull(authenticationIdProperty, "authenticationId property is not defined");
Reject.ifNull(userCredentialProperty, "userCredential property is not defined");
this.cryptoServiceProvider = cryptoService;
this.connectionFactoryProvider = connectionFactory;
this.queryOnResource = queryOnResource;
this.queryId = queryId;
this.authenticationIdProperty = authenticationIdProperty;
this.userCredentialProperty = userCredentialProperty;
this.userRolesProperty = userRolesProperty;
}
/**
* Performs the authentication using the given query id, resource, username and password.
*
* @param username The username.
* @param password The password.
* @param context the Context to use
* @return True if authentication is successful, otherwise false.
*/
public AuthenticatorResult authenticate(String username, String password, Context context) throws ResourceException {
Reject.ifNull(username, "Provided username was null");
Reject.ifNull(context, "Router context was null");
final CryptoService cryptoService = cryptoServiceProvider.get();
if (cryptoService == null) {
throw new InternalServerErrorException("No CryptoService available");
}
final ResourceResponse resource = getResource(username, context);
if (resource != null) {
if (cryptoService.isHashed(resource.getContent().get(userCredentialProperty))) {
try {
if (cryptoService.matches(password, resource.getContent().get(userCredentialProperty))) {
return AuthenticatorResult.authenticationSuccess(resource);
}
} catch (JsonCryptoException jce) {
throw new InternalServerErrorException(jce.getMessage(), jce);
}
} else {
final UserInfo userInfo = getRepoUserInfo(username, resource);
if (userInfo == null) {
// getResource already logged why
return AuthenticatorResult.FAILED;
} else if (userInfo.checkCredential(password)) {
logger.debug("Authentication succeeded for {}", username);
return AuthenticatorResult.authenticationSuccess(resource);
}
}
}
logger.debug("Authentication failed for {} due to invalid credentials", username);
return AuthenticatorResult.FAILED;
}
private ResourceResponse getResource(String username, Context context) throws ResourceException {
QueryRequest request = Requests.newQueryRequest(queryOnResource)
.setQueryId(queryId)
.addField("") // all default fields
.setAdditionalParameter(authenticationIdProperty, username);
if (this.userRolesProperty != null) {
request = request.addField(this.userRolesProperty);
}
final ConnectionFactory connectionFactory = connectionFactoryProvider.get();
if (connectionFactory == null) {
throw new InternalServerErrorException("No ConnectionFactory available");
}
final Set<ResourceResponse> result = new HashSet<>();
connectionFactory.getConnection().query(context, request, result);
if (result.size() == 0) {
logger.debug("Query to match user credentials found no user matching {}", username);
return null;
}
if (result.size() > 1) {
logger.debug("Query to match user credentials found more than one matching user for {}", username);
for (ResourceResponse entry : result) {
logger.debug("Ambiguous matching username for {} found id: {}", username, entry.getId());
}
throw newResourceException(401, "Access denied, user detail retrieved was ambiguous.");
}
return result.iterator().next(); // the retrieved resource
}
private UserInfo getRepoUserInfo(String username, ResourceResponse resource) throws ResourceException {
final CryptoService cryptoService = cryptoServiceProvider.get();
if (cryptoService == null) {
throw new InternalServerErrorException("No CryptoService available");
}
if (username == null || resource == null) {
return null;
}
final String retrievedCred =
cryptoService.decryptIfNecessary(resource.getContent().get(userCredentialProperty)).asString();
return new UserInfo(username, new Password(retrievedCred), null);
}
}