/* * 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; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import javax.inject.Provider; import org.apache.commons.lang3.StringUtils; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.ConfigurationPolicy; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.ReferencePolicy; import org.apache.felix.scr.annotations.Service; import org.forgerock.caf.authentication.api.AsyncServerAuthModule; import org.forgerock.caf.authentication.api.AuthenticationException; import org.forgerock.caf.authentication.framework.AuthenticationFilter; import org.forgerock.caf.authentication.framework.AuthenticationFilter.AuthenticationModuleBuilder; import org.forgerock.guava.common.base.Function; import org.forgerock.guava.common.base.Predicate; import org.forgerock.guava.common.collect.FluentIterable; import org.forgerock.http.Filter; import org.forgerock.json.JsonValue; import org.forgerock.json.resource.ActionRequest; import org.forgerock.json.resource.ActionResponse; import org.forgerock.json.resource.BadRequestException; import org.forgerock.json.resource.ConnectionFactory; import org.forgerock.json.resource.ForbiddenException; import org.forgerock.json.resource.InternalServerErrorException; import org.forgerock.json.resource.NotSupportedException; import org.forgerock.json.resource.PatchRequest; import org.forgerock.json.resource.ReadRequest; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.ResourceException; import org.forgerock.openidm.crypto.util.JettyPropertyUtil; import org.forgerock.openidm.auth.modules.IDMAuthModule; import org.forgerock.openidm.auth.modules.IDMAuthModuleWrapper; import org.forgerock.openidm.router.IDMConnectionFactory; import org.forgerock.script.ScriptRegistry; import org.forgerock.services.context.SecurityContext; import org.forgerock.services.context.Context; import org.forgerock.json.resource.SingletonResourceProvider; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.json.resource.http.HttpContext; import org.forgerock.openidm.config.enhanced.EnhancedConfig; import org.forgerock.openidm.core.ServerConstants; import org.forgerock.openidm.crypto.CryptoService; import org.forgerock.util.promise.Promise; import org.osgi.framework.Constants; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.forgerock.json.resource.Responses.newActionResponse; import static org.forgerock.openidm.auth.modules.IDMAuthModuleWrapper.AUTHENTICATION_ID; import static org.forgerock.openidm.auth.modules.IDMAuthModuleWrapper.PROPERTY_MAPPING; import static org.forgerock.openidm.auth.modules.IDMAuthModuleWrapper.QUERY_ID; import static org.forgerock.openidm.auth.modules.IDMAuthModuleWrapper.QUERY_ON_RESOURCE; import static org.forgerock.openidm.auth.modules.IDMAuthModuleWrapper.USER_CREDENTIAL; import static org.forgerock.caf.authentication.framework.AuthenticationFilter.AuthenticationModuleBuilder.configureModule; /** * Configures the authentication chains based on authentication.json. * * Example: * * <pre> * <code> * { * "serverAuthConfig" : { * "sessionModule" : { * "name" : "JWT_SESSION", * "properties" : { * "someSetting" : "some-value" * } * }, * "authModules" : [ * { * "name" : "IWA", * "properties" : { * "someSetting" : "some-value" * } * }, * { * "name" : "PASSTHROUGH", * "properties" : { * "someSetting" : "some-value" * } * } * ] * } * } * </code> * </pre> */ @Component(name = AuthenticationService.PID, immediate = true, policy = ConfigurationPolicy.REQUIRE) @Service @Properties({ @Property(name = Constants.SERVICE_VENDOR, value = ServerConstants.SERVER_VENDOR_NAME), @Property(name = Constants.SERVICE_DESCRIPTION, value = "OpenIDM Authentication Service"), @Property(name = ServerConstants.ROUTER_PREFIX, value = "/authentication") }) public class AuthenticationService implements SingletonResourceProvider { /** The PID for this Component. */ public static final String PID = "org.forgerock.openidm.authentication"; private static final Logger logger = LoggerFactory.getLogger(AuthenticationService.class); /** Re-authentication password header. */ private static final String HEADER_REAUTH_PASSWORD = "X-OpenIDM-Reauth-Password"; private static final String SERVER_AUTH_CONTEXT_KEY = "serverAuthContext"; private static final String SESSION_MODULE_KEY = "sessionModule"; private static final String AUTH_MODULES_KEY = "authModules"; private static final String AUTH_MODULE_PROPERTIES_KEY = "properties"; private static final String AUTH_MODULE_NAME_KEY = "name"; private static final String AUTH_MODULE_CLASS_NAME_KEY = "className"; private static final String MODULE_CONFIG_ENABLED = "enabled"; private JsonValue config; /** The authenticators to delegate to.*/ private List<Authenticator> authenticators = new ArrayList<>(); // ----- Declarative Service Implementation @Reference(policy = ReferencePolicy.DYNAMIC) CryptoService cryptoService; /** The Connection Factory */ @Reference(policy = ReferencePolicy.STATIC) protected IDMConnectionFactory connectionFactory; /** Script Registry service. */ @Reference(policy = ReferencePolicy.DYNAMIC) protected ScriptRegistry scriptRegistry; /** Enhanced configuration service. */ @Reference(policy = ReferencePolicy.DYNAMIC) private EnhancedConfig enhancedConfig; /** The CHF filter to wrap the CAF filter */ @Reference(policy = ReferencePolicy.DYNAMIC, target="(service.pid=org.forgerock.openidm.auth.config)") private AuthFilterWrapper authFilterWrapper; /** An on-demand Provider for the ConnectionFactory */ private final Provider<ConnectionFactory> connectionFactoryProvider = new Provider<ConnectionFactory>() { @Override public ConnectionFactory get() { return connectionFactory; } }; /** An on-demand Provider for the CryptoService */ private final Provider<CryptoService> cryptoServiceProvider = new Provider<CryptoService>() { @Override public CryptoService get() { return cryptoService; } }; /** a factory Function to build an Authenticator from an auth module config */ private final AuthenticatorFactory toAuthenticatorFromProperties = new AuthenticatorFactory(connectionFactoryProvider, cryptoServiceProvider); /** A {@link Predicate} that returns whether the auth module is enabled */ private static final Predicate<JsonValue> enabledAuthModules = new Predicate<JsonValue>() { @Override public boolean apply(JsonValue jsonValue) { return jsonValue.get(MODULE_CONFIG_ENABLED).defaultTo(true).asBoolean(); } }; /** A {@link Function} that returns the auth modules properties as a JsonValue */ private static final Function<JsonValue, JsonValue> toModuleProperties = new Function<JsonValue, JsonValue>() { @Override public JsonValue apply(JsonValue value) { return value.get(AUTH_MODULE_PROPERTIES_KEY); } }; /** A {link Predicate} that validates an auth module's properties for the purposes of instantiating an Authenticator */ private static final Predicate<JsonValue> authModulesThatHaveValidAuthenticatorProperties = new Predicate<JsonValue>() { @Override public boolean apply(JsonValue jsonValue) { // must have "queryOnResource" return jsonValue.get(QUERY_ON_RESOURCE).isString() // must either not have "queryId" && (jsonValue.get(QUERY_ID).isNull() // or have "queryId" + "authenticationId"/"userCredential" property mapping || (jsonValue.get(QUERY_ID).isString() && jsonValue.get(PROPERTY_MAPPING).get(AUTHENTICATION_ID).isString() && jsonValue.get(PROPERTY_MAPPING).get(USER_CREDENTIAL).isString())); } }; /** * Activates this component. * * @param context The ComponentContext */ @Activate public synchronized void activate(ComponentContext context) throws AuthenticationException { logger.info("Activating Authentication Service with configuration {}", context.getProperties()); config = enhancedConfig.getConfigurationAsJson(context); authFilterWrapper.setFilter(configureAuthenticationFilter(config)); // the auth module list config lives under at /serverAuthConfig/authModule final JsonValue authModuleConfig = config.get(SERVER_AUTH_CONTEXT_KEY).get(AUTH_MODULES_KEY); // filter enabled module configs and get their properties; // then filter those with valid auth properties, and build an authenticator for (final Authenticator authenticator : FluentIterable.from(authModuleConfig) .filter(enabledAuthModules) .transform(toModuleProperties) .filter(authModulesThatHaveValidAuthenticatorProperties) .transform(toAuthenticatorFromProperties)) { authenticators.add(authenticator); } logger.debug("OpenIDM Config for Authentication {} is activated.", config.get(Constants.SERVICE_PID)); } /** * Nulls the stored authentication JsonValue. * * @param context The ComponentContext. */ @Deactivate public void deactivate(ComponentContext context) { logger.debug("OpenIDM Config for Authentication {} is deactivated.", config.get(Constants.SERVICE_PID)); config = null; authenticators.clear(); // remove CAF filter from CHF filter wrapper if (authFilterWrapper != null) { try { authFilterWrapper.reset(); } catch (Exception ex) { logger.warn("Failure reported during unregistering of authentication filter: {}", ex.getMessage(), ex); } } } /** * Configures the commons Authentication Filter with the given configuration. * * @param jsonConfig The authentication configuration. * @return the CAF filter produced from the json config * @throws AuthenticationException on missing or incorrect configuration, or failure to construct an auth module * from the config */ private Filter configureAuthenticationFilter(JsonValue jsonConfig) throws AuthenticationException { if (jsonConfig == null || jsonConfig.size() == 0) { throw new AuthenticationException("No auth modules configured"); } // make copy of config final JsonValue moduleConfig = jsonConfig.copy(); final JsonValue serverAuthContext = moduleConfig.get(SERVER_AUTH_CONTEXT_KEY).required(); final JsonValue sessionConfig = serverAuthContext.get(AuthenticationService.SESSION_MODULE_KEY); final JsonValue authModulesConfig = serverAuthContext.get(AuthenticationService.AUTH_MODULES_KEY); final List<AuthenticationModuleBuilder> authModuleBuilders = new ArrayList<>(); for (final JsonValue authModuleConfig : authModulesConfig) { AuthenticationModuleBuilder moduleBuilder = processModuleConfiguration(authModuleConfig); if (moduleBuilder != null) { authModuleBuilders.add(moduleBuilder); } } return AuthenticationFilter.builder() .logger(logger) .auditApi(new IDMAuditApi(connectionFactory)) .sessionModule(processModuleConfiguration(sessionConfig)) .authModules(authModuleBuilders) .build(); } /** * Process the module configuration for a specific module, checking to see if the module is enabled and * resolving the module class name if an alias is used. * * @param moduleConfig The specific module configuration json. * @return Whether the module is enabled or not. */ private AuthenticationModuleBuilder processModuleConfiguration(JsonValue moduleConfig) throws AuthenticationException { if (moduleConfig.isDefined(MODULE_CONFIG_ENABLED) && !moduleConfig.get(MODULE_CONFIG_ENABLED).asBoolean()) { return null; } moduleConfig.remove(MODULE_CONFIG_ENABLED); AsyncServerAuthModule module; if (moduleConfig.isDefined(AUTH_MODULE_NAME_KEY)) { module = moduleConfig.get(AUTH_MODULE_NAME_KEY).asEnum(IDMAuthModule.class) .newInstance(toAuthenticatorFromProperties); } else if (moduleConfig.isDefined(AUTH_MODULE_CLASS_NAME_KEY)) { module = constructAuthModuleByClassName(moduleConfig.get(AUTH_MODULE_CLASS_NAME_KEY).asString()); } else { logger.warn("Unable to create auth module from config " + moduleConfig.toString()); throw new AuthenticationException("Auth module config lacks 'name' and 'className' attribute"); } JsonValue moduleProperties = moduleConfig.get("properties"); if (moduleProperties.isDefined("privateKeyPassword")) { // decrypt/de-obfuscate privateKey password moduleProperties.put("privateKeyPassword", JettyPropertyUtil.decryptOrDeobfuscate(moduleProperties.get("privateKeyPassword").asString())); } if (moduleProperties.isDefined("keystorePassword")) { // decrypt/de-obfuscate keystore password moduleProperties.put("keystorePassword", JettyPropertyUtil.decryptOrDeobfuscate(moduleProperties.get("keystorePassword").asString())); } // wrap all auth modules in our wrapper to apply the IDM business logic return configureModule(new IDMAuthModuleWrapper(module, connectionFactory, cryptoService, scriptRegistry)) .withSettings(moduleProperties.asMap()); } private AsyncServerAuthModule constructAuthModuleByClassName(String authModuleClassName) throws AuthenticationException { try { return Class.forName(authModuleClassName).asSubclass(AsyncServerAuthModule.class).newInstance(); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { logger.error("Failed to construct Auth Module instance", e); throw new AuthenticationException("Failed to construct Auth Module instance", e); } } // ----- Implementation of SingletonResourceProvider interface private enum Action { reauthenticate } /** * Action support, including reauthenticate action {@inheritDoc} */ @Override public Promise<ActionResponse, ResourceException> actionInstance(Context context, ActionRequest request) { try { if (Action.reauthenticate.equals(request.getActionAsEnum(Action.class))) { if (context.containsContext(HttpContext.class) && context.containsContext(SecurityContext.class)) { String authcid = context.asContext(SecurityContext.class).getAuthenticationId(); HttpContext httpContext = context.asContext(HttpContext.class); String password = httpContext.getHeaderAsString(HEADER_REAUTH_PASSWORD); if (StringUtils.isBlank(authcid) || StringUtils.isBlank(password)) { logger.debug("Reauthentication failed, missing or empty headers"); return new ForbiddenException("Reauthentication failed, missing or empty headers") .asPromise(); } for (Authenticator authenticator : authenticators) { try { if (authenticator.authenticate(authcid, password, context).isAuthenticated()) { JsonValue result = new JsonValue(new HashMap<String, Object>()); result.put("reauthenticated", true); return newActionResponse(result).asPromise(); } } catch (ResourceException e) { // log error and try next authentication mechanism logger.debug("Reauthentication failed: {}", e.getMessage()); } } return new ForbiddenException("Reauthentication failed for " + authcid).asPromise(); } else { return new InternalServerErrorException("Failure to reauthenticate - missing context").asPromise(); } } else { return new BadRequestException("Action " + request.getAction() + " on authentication service not supported") .asPromise(); } } catch (IllegalArgumentException e) { // from getActionAsEnum return new BadRequestException("Action " + request.getAction() + " on authentication service not supported") .asPromise(); } catch (Exception e) { return new InternalServerErrorException("Error processing action", e).asPromise(); } } /** * {@inheritDoc} */ @Override public Promise<ResourceResponse, ResourceException> patchInstance(Context context, PatchRequest request) { return new NotSupportedException("Patch operation not supported").asPromise(); } /** * {@inheritDoc} */ @Override public Promise<ResourceResponse, ResourceException> readInstance(Context context, ReadRequest request) { return new NotSupportedException("Read operation not supported").asPromise(); } /** * {@inheritDoc} */ @Override public Promise<ResourceResponse, ResourceException> updateInstance(Context context, UpdateRequest request) { return new NotSupportedException("Update operation not supported").asPromise(); } }