Java tutorial
/* * 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 2014-2015 ForgeRock AS. */ package org.forgerock.openidm.jaspi.modules; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.forgerock.jaspi.exceptions.JaspiAuthException; import org.forgerock.jaspi.runtime.JaspiRuntime; import org.forgerock.json.fluent.JsonValue; import org.forgerock.json.resource.BadRequestException; import org.forgerock.json.resource.QueryFilter; import org.forgerock.json.resource.QueryRequest; import org.forgerock.json.resource.Requests; import org.forgerock.json.resource.Resource; import org.forgerock.json.resource.ResourceException; import org.forgerock.openidm.jaspi.config.OSGiAuthnFilterBuilder; import org.forgerock.openidm.jaspi.config.OSGiAuthnFilterHelper; import org.forgerock.script.ScriptEntry; import org.forgerock.util.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.script.ScriptException; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.message.AuthException; import javax.security.auth.message.AuthStatus; import javax.security.auth.message.MessageInfo; import javax.security.auth.message.MessagePolicy; import javax.security.auth.message.module.ServerAuthModule; import javax.servlet.http.HttpServletRequest; import java.security.Principal; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import static org.forgerock.json.fluent.JsonValue.json; import static org.forgerock.json.fluent.JsonValue.object; import static org.forgerock.json.resource.Resource.FIELD_CONTENT; import static org.forgerock.json.resource.Resource.FIELD_CONTENT_ID; import static org.forgerock.json.resource.Resource.FIELD_CONTENT_REVISION; import static org.forgerock.openidm.jaspi.modules.MappingRoleCalculator.GroupComparison; import static org.forgerock.openidm.servletregistration.ServletRegistration.SERVLET_FILTER_AUGMENT_SECURITY_CONTEXT; /** * A Jaspi ServerAuthModule that is designed to wrap any other Jaspi ServerAuthModule. This module provides * IDM specific authentication processing to the authentication mechanism of underlying auth module. * <br/> * This allows IDM to use any common auth module and still benefit from automatic role calculation * and augment security context scripts (providing the authentication.json contains the required configuration). * * @since 3.0.0 */ public class IDMJaspiModuleWrapper implements ServerAuthModule { private static final Logger logger = LoggerFactory.getLogger(IDMJaspiModuleWrapper.class); public static final String QUERY_ID = "queryId"; public static final String QUERY_ON_RESOURCE = "queryOnResource"; public static final String PROPERTY_MAPPING = "propertyMapping"; public static final String AUTHENTICATION_ID = "authenticationId"; public static final String USER_CREDENTIAL = "userCredential"; static final String USER_ROLES = "userRoles"; static final String DEFAULT_USER_ROLES = "defaultUserRoles"; private static final String GROUP_ROLE_MAPPING = "groupRoleMapping"; private static final String GROUP_MEMBERSHIP = "groupMembership"; private static final String GROUP_COMPARISON_METHOD = "groupComparisonMethod"; /** Authentication without a session header. */ public static final String NO_SESSION = "X-OpenIDM-NoSession"; /** Key in Messages Map for the cached resource detail */ public static final String AUTHENTICATED_RESOURCE = "org.forgerock.openidm.authentication.resource"; private final OSGiAuthnFilterHelper authnFilterHelper; private final AuthModuleConstructor authModuleConstructor; private final AugmentationScriptExecutor augmentationScriptExecutor; /** an security context augmentation script, if configured */ private ScriptEntry augmentScript = null; private final RoleCalculatorFactory roleCalculatorFactory; private JsonValue properties = json(object()); private ServerAuthModule authModule; private String logClientIPHeader = null; private String queryOnResource; private Function<QueryRequest, Resource, ResourceException> queryExecutor; private UserDetailQueryBuilder queryBuilder; private RoleCalculator roleCalculator; /** * Constructs a new instance of the IDMJaspiModuleWrapper. */ public IDMJaspiModuleWrapper() throws AuthException { this.authnFilterHelper = OSGiAuthnFilterBuilder.getInstance(); if (authnFilterHelper == null) { throw new AuthException("OSGiAuthnFilterHelper is not ready."); } this.authModuleConstructor = new AuthModuleConstructor(); this.roleCalculatorFactory = new RoleCalculatorFactory(); this.augmentationScriptExecutor = new AugmentationScriptExecutor(authnFilterHelper); } /** * Constructs a new instance of the IDMJaspiModuleWrapper with the provided parameters, for test use. * * @param authnFilterHelper An instance of the OSGiAuthnFilterHelper. * @param authModuleConstructor An instance of the AuthModuleConstructor. * @param roleCalculatorFactory An instance of the RoleCalculatorFactory. * @param augmentationScriptExecutor An instance of the AugmentationScriptExecutor. */ IDMJaspiModuleWrapper(OSGiAuthnFilterHelper authnFilterHelper, AuthModuleConstructor authModuleConstructor, RoleCalculatorFactory roleCalculatorFactory, AugmentationScriptExecutor augmentationScriptExecutor) { this.authnFilterHelper = authnFilterHelper; this.authModuleConstructor = authModuleConstructor; this.roleCalculatorFactory = roleCalculatorFactory; this.augmentationScriptExecutor = augmentationScriptExecutor; } /** * Calls the underlying auth module's getSupportedMessageTypes method. * * @return {@inheritDoc} */ @Override public Class[] getSupportedMessageTypes() { return authModule.getSupportedMessageTypes(); } /** * Initialises the underlying auth module with the provided parameters and constructs an instance * of the RoleCalculator from the authentication configuration. * <br/> * Required configuration: * <ul> * <li>connectionFactory - the ConnectionFactory for making an authenticate request on the router</li> * <li>context - the ServerContext to use when making requests on the router</li> * <li>queryOnResource - the resource to perform the role calculation query on</li> * <li>authenticationId - the object attribute that represents the authentication id</li> * <li>groupMembership - the object attribute representing the group membership</li> * <li>defaultRoles - the list of default roles</li> * <li>roleMapping - the mapping between OpenIDM roles and pass-through auth groups</li> * <li>groupComparison - the method of {@link GroupComparison} to use</li> * </ul> * * @param requestMessagePolicy {@inheritDoc} * @param responseMessagePolicy {@inheritDoc} * @param handler {@inheritDoc} * @param options {@inheritDoc} * @throws AuthException {@inheritDoc} */ @Override public void initialize(MessagePolicy requestMessagePolicy, MessagePolicy responseMessagePolicy, CallbackHandler handler, Map options) throws AuthException { properties = new JsonValue(options); authModule = authModuleConstructor.construct(properties.get("authModuleClassName").asString()); authModule.initialize(requestMessagePolicy, responseMessagePolicy, handler, options); logClientIPHeader = properties.get("clientIPHeader").asString(); queryOnResource = properties.get(QUERY_ON_RESOURCE).asString(); String queryId = properties.get(QUERY_ID).asString(); String authenticationId = properties.get(PROPERTY_MAPPING).get(AUTHENTICATION_ID).asString(); String userRoles = properties.get(PROPERTY_MAPPING).get(USER_ROLES).asString(); String groupMembership = properties.get(PROPERTY_MAPPING).get(GROUP_MEMBERSHIP).asString(); List<String> defaultRoles = properties.get(DEFAULT_USER_ROLES).defaultTo(Collections.emptyList()) .asList(String.class); Map<String, List<String>> roleMapping = properties.get(GROUP_ROLE_MAPPING).defaultTo(Collections.emptyMap()) .asMapOfList(String.class); MappingRoleCalculator.GroupComparison groupComparison = properties.get(GROUP_COMPARISON_METHOD) .defaultTo(MappingRoleCalculator.GroupComparison.equals.name()) .asEnum(MappingRoleCalculator.GroupComparison.class); // a function to perform the user detail query on the router queryExecutor = new Function<QueryRequest, Resource, ResourceException>() { @Override public Resource apply(QueryRequest request) throws ResourceException { if (request == null) { return null; } final List<Resource> resources = new ArrayList<Resource>(); authnFilterHelper.getConnectionFactory().getConnection() .query(authnFilterHelper.getRouter().createServerContext(), request, resources); if (resources.isEmpty()) { throw ResourceException.getException(401, "Access denied, no user detail could be retrieved."); } else if (resources.size() > 1) { throw ResourceException.getException(401, "Access denied, user detail retrieved was ambiguous."); } return resources.get(0); } }; queryBuilder = new UserDetailQueryBuilder(queryOnResource).useQueryId(queryId) .withAuthenticationIdProperty(authenticationId); roleCalculator = roleCalculatorFactory.create(defaultRoles, userRoles, groupMembership, roleMapping, groupComparison); JsonValue scriptConfig = properties.get(SERVLET_FILTER_AUGMENT_SECURITY_CONTEXT); if (!scriptConfig.isNull()) { augmentScript = getAugmentScript(scriptConfig); logger.debug("Registered script {}", augmentScript); } } /** * Gets the ScriptEntry for the specified script config. * * @param scriptConfig The script config. * @return The ScriptEntry. * @throws JaspiAuthException If there is a problem retrieving the ScriptEntry. */ ScriptEntry getAugmentScript(JsonValue scriptConfig) throws JaspiAuthException { try { return authnFilterHelper.getScriptRegistry().takeScript(scriptConfig); } catch (ScriptException e) { logger.error("{} when attempting to register script {}", e.toString(), scriptConfig, e); throw new JaspiAuthException(e.toString(), e); } } /** * Provides IDM specific authentication process handling, by setting whether to log the client's IP address, * and then calls the underlying auth module's validateRequest method. If the auth module returns * SUCCESS, based on the authentication configuration will perform role calculation and, if present, will run the * augment security context script. * * @param messageInfo {@inheritDoc} * @param clientSubject {@inheritDoc} * @param serviceSubject {@inheritDoc} * @return {@inheritDoc} * @throws AuthException {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException { // Add this properties so the AuditLogger knows whether to log the client IP in the header. setClientIPAddress(messageInfo); final AuthStatus authStatus = authModule.validateRequest(messageInfo, clientSubject, serviceSubject); if (!AuthStatus.SUCCESS.equals(authStatus)) { return authStatus; } String principalName = null; for (Principal principal : clientSubject.getPrincipals()) { if (principal.getName() != null) { principalName = principal.getName(); break; } } if (principalName == null) { // As per Jaspi spec, the module developer MUST ensure that the client // subject's principal is set when the module returns SUCCESS. throw new JaspiAuthException( "Underlying Server Auth Module has not set the client subject's principal!"); } // user is authenticated; populate security context try { final Resource resource = getAuthenticatedResource(principalName, messageInfo); final SecurityContextMapper securityContextMapper = SecurityContextMapper.fromMessageInfo(messageInfo) .setAuthenticationId(principalName); // Calculate (and set) roles if not already set if (securityContextMapper.getRoles() == null || securityContextMapper.getRoles().isEmpty()) { roleCalculator.calculateRoles(principalName, securityContextMapper, resource); } // set "resource" (component) if not already set if (securityContextMapper.getResource() == null) { securityContextMapper.setResource(queryOnResource); } // set "user id" (authorization.id) if not already set if (securityContextMapper.getUserId() == null) { if (resource != null) { // assign authorization id from resource if present securityContextMapper.setUserId(resource.getId() != null ? resource.getId() : resource.getContent().get(FIELD_CONTENT_ID).asString()); } else { // set to principal otherwise securityContextMapper.setUserId(principalName); } } // run the augmentation script, if configured (will no-op if none specified) augmentationScriptExecutor.executeAugmentationScript(augmentScript, properties, securityContextMapper); } catch (ResourceException e) { if (logger.isDebugEnabled()) { logger.debug("Failed role calculation for {} on {}.", principalName, queryOnResource, e); } if (e.isServerError()) { // HTTP server-side error; AuthException sadly does not accept cause throw new JaspiAuthException("Failed pass-through authentication of " + principalName + " on " + queryOnResource + ":" + e.getMessage(), e); } // role calculation failed return AuthStatus.SEND_FAILURE; } return authStatus; } private Resource getAuthenticatedResource(String principalName, MessageInfo messageInfo) throws ResourceException { // see if the resource was stored in the MessageInfo by the Authenticator if (messageInfo.getMap().containsKey(AUTHENTICATED_RESOURCE)) { JsonValue resourceDetail = new JsonValue(messageInfo.getMap().get(AUTHENTICATED_RESOURCE)); if (resourceDetail.isMap()) { return new Resource(resourceDetail.get(FIELD_CONTENT_ID).asString(), resourceDetail.get(FIELD_CONTENT_REVISION).asString(), resourceDetail.get(FIELD_CONTENT)); } } // attempt to read the user object; will return null if any of the pieces are null return queryExecutor.apply(queryBuilder.forPrincipal(principalName).build()); } private void setClientIPAddress(MessageInfo messageInfo) { HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage(); String ipAddress; if (logClientIPHeader == null) { ipAddress = request.getRemoteAddr(); } else { ipAddress = request.getHeader(logClientIPHeader); if (ipAddress == null) { ipAddress = request.getRemoteAddr(); } } getContextMap(messageInfo).put("ipAddress", ipAddress); } private Map<String, Object> getContextMap(MessageInfo messageInfo) { return (Map<String, Object>) messageInfo.getMap().get(JaspiRuntime.ATTRIBUTE_AUTH_CONTEXT); } /** * If the request contains the X-OpenIDM-NoSession header, sets the skipSession property on the MessageInfo, * and then calls the underlying auth module's secureResponse method. * * @param messageInfo {@inheritDoc} * @param serviceSubject {@inheritDoc} * @return {@inheritDoc} * @throws AuthException {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException { final HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage(); final String noSession = request.getHeader(NO_SESSION); if (Boolean.parseBoolean(noSession)) { messageInfo.getMap().put("skipSession", true); } return authModule.secureResponse(messageInfo, serviceSubject); } /** * Calls the underlying auth module's cleanSubject method. * * @param messageInfo {@inheritDoc} * @param clientSubject {@inheritDoc} * @throws AuthException {@inheritDoc} */ @Override public void cleanSubject(MessageInfo messageInfo, Subject clientSubject) throws AuthException { authModule.cleanSubject(messageInfo, clientSubject); } /** * Constructs Server Auth Modules from a given class name, using the mandatory no-arg constructor. * * @since 3.0.0 */ static class AuthModuleConstructor { /** * Creates an instance of a Server Auth Module for the specified class name. * <br/> * Uses the spec mandated no-arg constructor. * * @param authModuleClassName The ServerAuthModule class name. * @return The ServerAuthModule instance. * @throws JaspiAuthException If there is any problem creating the ServerAuthModule instance. */ ServerAuthModule construct(String authModuleClassName) throws JaspiAuthException { try { return Class.forName(authModuleClassName).asSubclass(ServerAuthModule.class).newInstance(); } catch (ClassNotFoundException e) { logger.error("Failed to construct Auth Module instance", e); throw new JaspiAuthException("Failed to construct Auth Module instance", e); } catch (InstantiationException e) { logger.error("Failed to construct Auth Module instance", e); throw new JaspiAuthException("Failed to construct Auth Module instance", e); } catch (IllegalAccessException e) { logger.error("Failed to construct Auth Module instance", e); throw new JaspiAuthException("Failed to construct Auth Module instance", e); } } } /** * QueryRequest Builder class for querying the user object detail. * <p> * If queryId is provided, build() will set additional parameters for the authenticationId * property and principal name. Otherwise a QueryFilter where "authenticationId property = principal name" * is used. * * @since 3.0.0 */ private static class UserDetailQueryBuilder { private final String queryOnResource; private String queryId = null; private String authenticationId = null; private String principal = null; private UserDetailQueryBuilder(final String queryOnResource) { this.queryOnResource = queryOnResource; } UserDetailQueryBuilder useQueryId(String queryId) { this.queryId = queryId; return this; } UserDetailQueryBuilder withAuthenticationIdProperty(final String authenticationId) { this.authenticationId = authenticationId; return this; } UserDetailQueryBuilder forPrincipal(final String principal) { this.principal = principal; return this; } QueryRequest build() throws BadRequestException { if (queryOnResource == null || authenticationId == null || principal == null) { return null; } QueryRequest request = Requests.newQueryRequest(queryOnResource); if (queryId != null) { // if we're using a queryId, provide the additional parameter mapping the authenticationId property // and its value (the principal) request.setQueryId(queryId); request.setAdditionalParameter(authenticationId, principal); } else { // otherwise, use a query filter mapping the authenticationId property to the principal request.setQueryFilter(QueryFilter.equalTo(authenticationId, principal)); } return request; } } /** * Internal credential bean to hold username/password pair. * * @since 3.0.0 */ static class Credential { final String username; final String password; Credential(final String username, final String password) { this.username = username; this.password = password; } boolean isComplete() { return (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)); } } /** * Interface for a helper that returns user credentials from an HttpServletRequest * * @since 3.0.0 */ static interface CredentialHelper { public Credential getCredential(HttpServletRequest request); } /** CredentialHelper to get auth header creds from request */ static final CredentialHelper HEADER_AUTH_CRED_HELPER = new CredentialHelper() { /** Authentication username header. */ private static final String HEADER_USERNAME = "X-OpenIDM-Username"; /** Authentication password header. */ private static final String HEADER_PASSWORD = "X-OpenIDM-Password"; @Override public Credential getCredential(HttpServletRequest request) { return new Credential(request.getHeader(HEADER_USERNAME), request.getHeader(HEADER_PASSWORD)); } }; /** CredentialHelper to get Basic-Auth creds from request */ static final CredentialHelper BASIC_AUTH_CRED_HELPER = new CredentialHelper() { /** Basic auth header. */ private static final String HEADER_AUTHORIZATION = "Authorization"; private static final String AUTHORIZATION_HEADER_BASIC = "Basic"; @Override public Credential getCredential(HttpServletRequest request) { final String authHeader = request.getHeader(HEADER_AUTHORIZATION); if (authHeader != null) { final String[] authValue = authHeader.split("\\s", 2); if (AUTHORIZATION_HEADER_BASIC.equalsIgnoreCase(authValue[0]) && authValue[1] != null) { final String[] creds = new String(Base64.decodeBase64(authValue[1].getBytes())).split(":"); if (creds.length == 2) { return new Credential(creds[0], creds[1]); } } } return new Credential(null, null); } }; }