/*
* 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 2013-2015 ForgeRock AS.
*/
package org.forgerock.openidm.auth.modules;
import static javax.security.auth.message.AuthStatus.*;
import static org.forgerock.json.JsonValue.*;
import static org.forgerock.util.promise.Promises.newExceptionPromise;
import static org.forgerock.util.promise.Promises.newResultPromise;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.message.AuthStatus;
import javax.security.auth.message.MessagePolicy;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import org.forgerock.caf.authentication.api.AsyncServerAuthModule;
import org.forgerock.caf.authentication.api.AuthenticationException;
import org.forgerock.caf.authentication.api.MessageInfoContext;
import org.forgerock.caf.authentication.framework.AuditTrail;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.RootContext;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.openidm.auth.Authenticator;
import org.forgerock.openidm.auth.AuthenticatorFactory;
import org.forgerock.util.Reject;
import org.forgerock.util.promise.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Authentication Filter module for the Common Authentication Filter. Validates client requests by passing though
* to a OpenICF Connector or other delegated endpoint.
*/
public class DelegatedAuthModule implements AsyncServerAuthModule {
private final static Logger logger = LoggerFactory.getLogger(DelegatedAuthModule.class);
/**
* A NullObject {@link Authenticator} implementation.
*/
private static final Authenticator NULL_AUTHENTICATOR = new Authenticator() {
@Override
public AuthenticatorResult authenticate(String username, String password, Context context)
throws ResourceException {
return AuthenticatorResult.FAILED;
}
};
private final AuthenticatorFactory authenticatorFactory;
private final IDMAuthModule moduleId;
private Authenticator authenticator = NULL_AUTHENTICATOR;
private String queryOnResource = "";
private JsonValue options = new JsonValue(null);
/**
* Constructor used by the commons Authentication Filter framework to create an instance of this authentication
* module.
*
* @param authenticatorFactory
*/
public DelegatedAuthModule(AuthenticatorFactory authenticatorFactory, IDMAuthModule moduleId) {
Reject.ifNull(authenticatorFactory, "AuthenticationFactory cannot be null");
this.authenticatorFactory = authenticatorFactory;
this.moduleId = moduleId;
}
/**
* {@inheritDoc}
*/
@Override
public final Collection<Class<?>> getSupportedMessageTypes() {
return Arrays.asList(new Class<?>[]{Request.class, Response.class});
}
@Override
public String getModuleId() {
return moduleId.name();
}
/**
* Initialises the Passthrough authentication module with the OSGi json configuration.
*
* @param requestPolicy {@inheritDoc}
* @param responsePolicy {@inheritDoc}
* @param handler {@inheritDoc}
* @param options {@inheritDoc}
*/
@Override
public Promise<Void, AuthenticationException> initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy,
CallbackHandler handler, Map<String, Object> options) {
this.options = new JsonValue(options);
queryOnResource = new JsonValue(options).get(IDMAuthModuleWrapper.QUERY_ON_RESOURCE).required().asString();
authenticator = authenticatorFactory.apply(this.options);
return newResultPromise(null);
}
/**
* Validates the client's request by passing through the request to be authenticated against a OpenICF Connector.
*
* @param messageInfo {@inheritDoc}
* @param clientSubject {@inheritDoc}
* @param serviceSubject {@inheritDoc}
* @return {@inheritDoc}
*/
@Override
public Promise<AuthStatus, AuthenticationException> validateRequest(MessageInfoContext messageInfo,
Subject clientSubject, Subject serviceSubject) {
logger.debug("DelegatedAuthModule: validateRequest START");
SecurityContextMapper securityContextMapper = SecurityContextMapper.fromMessageInfo(messageInfo);
Request request = messageInfo.getRequest();
try {
logger.debug("DelegatedAuthModule: Delegating call to remote authentication");
if (authenticate(IDMAuthModuleWrapper.HEADER_AUTH_CRED_HELPER.getCredential(request), messageInfo, securityContextMapper)
|| authenticate(IDMAuthModuleWrapper.BASIC_AUTH_CRED_HELPER.getCredential(request), messageInfo, securityContextMapper)) {
logger.debug("DelegatedAuthModule: Authentication successful");
final String authcid = securityContextMapper.getAuthenticationId();
clientSubject.getPrincipals().add(new Principal() {
public String getName() {
return authcid;
}
});
// Auth success will be logged in IDMAuthModuleWrapper
return newResultPromise(SUCCESS);
} else {
logger.debug("DelegatedAuthModule: Authentication failed");
return newResultPromise(SEND_FAILURE);
}
} catch (AuthenticationException e) {
return newExceptionPromise(e);
} finally {
logger.debug("DelegatedAuthModule: validateRequest END");
}
}
private boolean authenticate(IDMAuthModuleWrapper.Credential credential, MessageInfoContext messageInfo,
SecurityContextMapper securityContextMapper) throws AuthenticationException {
if (!credential.isComplete()) {
logger.debug("Failed authentication, missing or empty headers");
return false;
}
// set the authenticationId of the user that is trying to authenticate
securityContextMapper.setAuthenticationId(credential.username);
try {
// construct a rudimentary context chain to validate the auth credentials
Context context = new SecurityContext(
new RootContext(),
securityContextMapper.getAuthenticationId(),
securityContextMapper.getAuthorizationId());
Authenticator.AuthenticatorResult result = authenticator.authenticate(
credential.username, credential.password, context);
final ResourceResponse resource = result.getResource();
if (resource != null) {
final JsonValue messageMap = new JsonValue(messageInfo.getRequestContextMap());
messageMap.put(IDMAuthModuleWrapper.AUTHENTICATED_RESOURCE,
json(object(
field(ResourceResponse.FIELD_CONTENT_ID, resource.getId()),
field(ResourceResponse.FIELD_CONTENT_REVISION, resource.getRevision()),
field(ResourceResponse.FIELD_CONTENT, resource.getContent().asMap())))
.asMap());
}
return result.isAuthenticated();
} catch (ResourceException e) {
logger.debug("Failed delegated authentication of {} on {}.", credential.username, queryOnResource, e);
messageInfo.getRequestContextMap().put(AuditTrail.AUDIT_FAILURE_REASON_KEY, e.toJsonValue().asMap());
if (e.isServerError()) { // HTTP server-side error
throw new AuthenticationException(
"Failed delegated authentication of " + credential.username + " on " + queryOnResource, e);
}
// authentication failed
return false;
}
}
/**
* No work to do here so always returns AuthStatus.SEND_SUCCESS.
*
* @param messageInfo {@inheritDoc}
* @param serviceSubject {@inheritDoc}
* @return {@inheritDoc}
*/
@Override
public Promise<AuthStatus, AuthenticationException> secureResponse(MessageInfoContext messageInfo,
Subject serviceSubject) {
return newResultPromise(SEND_SUCCESS);
}
/**
* Nothing to clean up.
*
* @param messageInfo {@inheritDoc}
* @param subject {@inheritDoc}
* @return {@inheritDoc}
*/
@Override
public Promise<Void, AuthenticationException> cleanSubject(MessageInfoContext messageInfo, Subject subject) {
return newResultPromise(null);
}
}