/*
* 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 2015 ForgeRock AS.
*/
package org.forgerock.openidm.selfservice.impl;
import static org.forgerock.http.handler.HttpClientHandler.*;
import static org.forgerock.json.resource.Requests.newReadRequest;
import static org.forgerock.json.resource.ResourcePath.*;
import static org.forgerock.openidm.util.ContextUtil.createInternalContext;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
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.forgerock.http.Client;
import org.forgerock.http.HttpApplicationException;
import org.forgerock.http.apache.sync.SyncHttpClientProvider;
import org.forgerock.http.handler.HttpClientHandler;
import org.forgerock.http.spi.Loader;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.jose.jws.SigningManager;
import org.forgerock.json.jose.jws.handlers.SigningHandler;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.openidm.core.IdentityServer;
import org.forgerock.openidm.core.ServerConstants;
import org.forgerock.openidm.crypto.CryptoService;
import org.forgerock.selfservice.core.ProcessStore;
import org.forgerock.selfservice.core.ProgressStage;
import org.forgerock.selfservice.core.ProgressStageProvider;
import org.forgerock.selfservice.core.config.StageConfig;
import org.forgerock.selfservice.core.config.StageConfigException;
import org.forgerock.selfservice.core.snapshot.SnapshotTokenConfig;
import org.forgerock.selfservice.core.snapshot.SnapshotTokenHandler;
import org.forgerock.selfservice.core.snapshot.SnapshotTokenHandlerFactory;
import org.forgerock.selfservice.json.JsonAnonymousProcessServiceBuilder;
import org.forgerock.selfservice.stages.tokenhandlers.JwtTokenHandler;
import org.forgerock.selfservice.stages.tokenhandlers.JwtTokenHandlerConfig;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ConnectionFactory;
import org.forgerock.openidm.config.enhanced.EnhancedConfig;
import org.forgerock.util.Options;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This service supports self-registration, password reset, and forgotten username
* per the Commons Self-Service stage configuration.
*/
@Component(name = SelfService.PID, immediate = true, configurationFactory = true, policy = ConfigurationPolicy.REQUIRE)
@Properties({
@Property(name = "service.description", value = "OpenIDM SelfService Service"),
@Property(name = "service.vendor", value = "ForgeRock AS"),
})
public class SelfService {
static final String PID = "org.forgerock.openidm.selfservice";
private static final Logger LOGGER = LoggerFactory.getLogger(SelfService.class);
/** the boot.properties property for the shared key alias */
private static final String SHARED_KEY_PROPERTY = "openidm.config.crypto.selfservice.sharedkey.alias";
/** the registered parent router-path for self-service flows */
static final String ROUTER_PREFIX = "selfservice";
/** this config key is present if the config represents a self-service process */
private static final String STAGE_CONFIGS = "stageConfigs";
/** config key present if config requires KBA questions */
private static final String KBA_CONFIG = "kbaConfig";
// ----- Declarative Service Implementation
/**
* Use the external servlet connection factory so that self-service requests are subject to authz rules
* as "external" requests.
*/
@Reference(policy = ReferencePolicy.STATIC, target="(service.pid=org.forgerock.openidm.router)")
private ConnectionFactory connectionFactory;
/** Enhanced configuration service. */
@Reference(policy = ReferencePolicy.DYNAMIC)
private EnhancedConfig enhancedConfig;
/** The KBA Configuration. */
@Reference(policy = ReferencePolicy.STATIC)
private KbaConfiguration kbaConfiguration;
/** CryptoService - not used directly, but added to make sure shared key gets created before use */
@Reference(policy = ReferencePolicy.DYNAMIC)
private CryptoService cryptoService;
private Dictionary<String, Object> properties = null;
private JsonValue config;
private RequestHandler processService;
private ServiceRegistration<RequestHandler> serviceRegistration = null;
@Activate
void activate(ComponentContext context) throws Exception {
LOGGER.debug("Activating Service with configuration {}", context.getProperties());
try {
config = enhancedConfig.getConfigurationAsJson(context);
amendConfig();
// create self-service request handler
processService = JsonAnonymousProcessServiceBuilder.newBuilder()
.withClassLoader(this.getClass().getClassLoader())
.withJsonConfig(config)
.withProgressStageProvider(newProgressStageProvider(newHttpClient()))
.withTokenHandlerFactory(newTokenHandlerFactory())
.withProcessStore(newProcessStore())
.build();
// begin service registration prep
properties = context.getProperties();
if (null == properties) {
properties = new Hashtable<>();
}
String factoryPid = enhancedConfig.getConfigurationFactoryPid(context);
if (StringUtils.isBlank(factoryPid)) {
throw new IllegalArgumentException("Configuration must have property: "
+ ServerConstants.CONFIG_FACTORY_PID);
}
properties.put(ServerConstants.ROUTER_PREFIX,
resourcePath(ROUTER_PREFIX).concat(resourcePath(factoryPid)).toString());
// service registration - register the AnonymousProcessService directly as a RequestHandler
serviceRegistration = context.getBundleContext().registerService(
RequestHandler.class, processService, properties);
} catch (Exception ex) {
LOGGER.warn("Configuration invalid, can not start self-service.", ex);
throw ex;
}
LOGGER.info("Self-service started.");
}
private void amendConfig() throws ResourceException {
for (JsonValue stageConfig : config.get(STAGE_CONFIGS)) {
if (stageConfig.isDefined(KBA_CONFIG)) {
// overwrite kbaConfig with config from KBA config service
stageConfig.put(KBA_CONFIG, kbaConfiguration.getConfig().getObject());
}
}
// pull the shared key in from the keystore
ResourceResponse result = connectionFactory.getConnection().read(createInternalContext(),
newReadRequest("security/keystore/privatekey/"
+ IdentityServer.getInstance().getProperty(SHARED_KEY_PROPERTY)));
config.put(new JsonPointer("/snapshotToken/sharedKey"),
result.getContent().get(new JsonPointer("/secret/encoded")).asString());
// force storage type to stateless
config.put("storage", "stateless");
}
private Client newHttpClient() throws HttpApplicationException {
return new Client(
new HttpClientHandler(
Options.defaultOptions()
.set(OPTION_LOADER, new Loader() {
@Override
public <S> S load(Class<S> service, Options options) {
return service.cast(new SyncHttpClientProvider());
}
})));
}
private ProgressStageProvider newProgressStageProvider(final Client httpClient) {
return new ProgressStageProvider() {
@Override
public ProgressStage<StageConfig> get(Class<? extends ProgressStage<StageConfig>> progressStageClass) {
Constructor<?>[] constructors = progressStageClass.getConstructors();
if (constructors.length > 1) {
throw new StageConfigException("Only expected one constructor for the configured progress stage "
+ progressStageClass);
}
try {
Constructor<? extends ProgressStage<StageConfig>> constructor =
progressStageClass.getConstructor(constructors[0].getParameterTypes());
Object[] parameters = getParameters(constructor);
return constructor.newInstance(parameters);
} catch (NoSuchMethodException | InvocationTargetException |
IllegalAccessException | InstantiationException e) {
throw new StageConfigException("Unable to instantiate the configured progress stage", e);
}
}
private Object[] getParameters(Constructor<?> constructor) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
Object[] parameters = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
if (parameterTypes[i].equals(ConnectionFactory.class)) {
parameters[i] = connectionFactory;
} else if (parameterTypes[i].equals(Client.class)) {
parameters[i] = httpClient;
} else {
throw new StageConfigException("Unexpected parameter type for configured progress stage "
+ parameters[i]);
}
}
return parameters;
}
};
}
private SnapshotTokenHandlerFactory newTokenHandlerFactory() {
return new SnapshotTokenHandlerFactory() {
@Override
public SnapshotTokenHandler get(SnapshotTokenConfig snapshotTokenConfig) {
switch (snapshotTokenConfig.getType()) {
case JwtTokenHandlerConfig.TYPE:
return createJwtTokenHandler((JwtTokenHandlerConfig) snapshotTokenConfig);
default:
throw new IllegalArgumentException("Unknown type " + snapshotTokenConfig.getType());
}
}
private SnapshotTokenHandler createJwtTokenHandler(JwtTokenHandlerConfig config) {
try {
SigningManager signingManager = new SigningManager();
SigningHandler signingHandler = signingManager.newHmacSigningHandler(config.getSharedKey());
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(config.getKeyPairAlgorithm());
keyPairGen.initialize(config.getKeyPairSize());
return new JwtTokenHandler(
config.getJweAlgorithm(),
config.getEncryptionMethod(),
keyPairGen.generateKeyPair(),
config.getJwsAlgorithm(),
signingHandler,
config.getTokenLifeTimeInSeconds());
} catch (NoSuchAlgorithmException nsaE) {
throw new RuntimeException("Unable to create key pair for encryption", nsaE);
}
}
};
}
private ProcessStore newProcessStore() {
return new ProcessStore() {
final Map<String, JsonValue> store = new HashMap<>();
@Override
public void add(String s, JsonValue jsonValue) {
store.put(s, jsonValue);
}
@Override
public JsonValue remove(String s) {
return store.remove(s);
}
};
}
@Deactivate
void deactivate(ComponentContext compContext) {
LOGGER.debug("Deactivating Service {}", compContext.getProperties());
try {
if (null != serviceRegistration) {
serviceRegistration.unregister();
serviceRegistration = null;
}
} catch (IllegalStateException e) {
/* Catch if the service was already removed */
serviceRegistration = null;
} finally {
processService = null;
config = null;
LOGGER.info("Self-service stopped.");
}
}
}