org.forgerock.openidm.selfservice.impl.SelfService.java Source code

Java tutorial

Introduction

Here is the source code for org.forgerock.openidm.selfservice.impl.SelfService.java

Source

/*
 * 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.");
        }
    }

}