Spring Security with SSO Headers - integrating with OAM WebGate

转载自: http://blog.sbeynon.net/2011/12/spring-security-with-sso-headers.html

How to integrate java web applications with SSO? Usually a simple J2EE filter looking for the correct header name can suffice, however, if you want to take advantage of Spring Security you need to set it up with a "PreAuthentication" filter. This states that spring is not performing authentication, it's already done.

This solution may be overkill, but it provides more control/log info, and the ability to test outside of the SSO environment without implementing alternate authentication providers (although it would also be possible to have a form/database authentication fallback as well).

My environment: J2ee (any app server) fronted by a Oracle Access WebGate protected web server.

Spring Security 2.5+ (tested with 3.0.5)

What I need: Convert Request Headers OAM_REMOTE_USER and ROLES to map to an authenticated principal.

Making it usable: Allow a fallback security configuration for local testing.

Step 1. PreAuthentication Config

There are two custom beans in this file "HeaderAuthenticationFilter" and "HeaderAuthenticationDetails" and three configuration properties:

security.principal.header.name=OAM_REMOTE_USER
security.roles.header.name=ROLES
security.test.principal=no_header_in_test_mode

applicationContext-security.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
  xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"
  default-lazy-init="true">

  <bean
    class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />

  <!-- this bean name must match the filter name defined in security.xml below -->
  <bean id="ssoHeaderFilter"
    class="com.sharnibey.sample.security.HeaderAuthenticationFilter">
    <property name="principalRequestHeader" value="${security.principal.header.name}" />

    <!-- fall back to other authentication providers is OAM SSO is not there -->
    <property name="exceptionIfHeaderMissing" value="false" />
    <!-- hard code a testUserId for local tests -->
    <property name="testUserId" value="${security.test.principal}" />

    <property name="authenticationManager" ref="authenticationManager" />

    <property name="authenticationDetailsSource">
      <bean class="com.sharnibey.sample.security.HeaderAuthenticationDetails">
        <!-- look for the request header set by the webgate and map to local
          roles -->
        <property name="roleHeaderName" value="${security.roles.header.name}" />
        <property name="userRoles2GrantedAuthoritiesMapper">
          <bean
            class="org.springframework.security.core.authority.mapping.SimpleAttributes2GrantedAuthoritiesMapper">
            <property name="convertAttributeToUpperCase" value="true" />
          </bean>
        </property>
        <!-- setup a testing role if not deployed with a webgate - this only
          applies if ENV_NAME != uat/prod -->
        <property name="testingRoles">
          <set>
            <value>USER</value>
          </set>
        </property>
        <!-- all available roles for this application -->
        <property name="allRoles">
          <set>
            <value>USER</value>
            <value>ADMIN</value>
          </set>
        </property>
      </bean>
    </property>
  </bean>
  <bean id="preauthAuthProvider"
    class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
    <property name="preAuthenticatedUserDetailsService" ref="preAuthenticatedUserDetailsService" />
  </bean>
  <!-- magically map the user header to a valid user object -->
  <bean id="preAuthenticatedUserDetailsService"
    class="org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesUserDetailsService" />

  <bean id="securityContextHolderAwareRequestFilter"
    class="org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter" />
</beans>

PreAuthentication Code

public class HeaderAuthenticationFilter
    extends AbstractPreAuthenticatedProcessingFilter {
  protected final Logger log = LoggerFactory.getLogger(HeaderAuthenticationFilter.class);
  private String principalRequestHeader = "OAM_REMOTE_USER";
  /**
   * Configure a value in the applicationContext-security for local tests.
   */
  private String testUserId = null;
  /**
   * Configure whether a missing SSO header is an exception.
   */
  private boolean exceptionIfHeaderMissing = false;

  /**
   * Read and return header named by <tt>principalRequestHeader</tt> from Request
   *
   * @throws PreAuthenticatedCredentialsNotFoundException
   *             if the header is missing and
   *             <tt>exceptionIfHeaderMissing</tt> is set to <tt>true</tt>.
   */
  protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
    String principal = request.getHeader(principalRequestHeader);

    if (principal == null) {
      if (exceptionIfHeaderMissing) {
        throw new PreAuthenticatedCredentialsNotFoundException(principalRequestHeader
            + " header not found in request.");
      } if (StringUtils.isNotBlank(testUserId)) {
          log.warn("spring configuration has a test user id " + testUserId);
          principal = testUserId;
      } else if (request.getSession().getAttribute("session_user") != null) {
// A bit of a hack for testers - allow the principal to be
// obtained by session. Must be set by a page with no security filters enabled.
// should remove for production.
        principal = (String) request.getSession().getAttribute("session_user");
      }
    }
    // also set it into the session, sometimes that's easier for jsp/faces
    // to get at..
    request.getSession().setAttribute("session_user", principal);
    return principal;
  }

  /**
   * Credentials aren't applicable here for OAM WebGate SSO.
   */
  protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
    return "password_not_applicable";
  }

  public void setPrincipalRequestHeader(String principalRequestHeader) {
    Assert.hasText(principalRequestHeader, "principalRequestHeader must not be empty or null");
    this.principalRequestHeader = principalRequestHeader;
  }

  public void setTestUserId(String testId) {
    if (StringUtils.isNotBlank(testId)) {
      this.testUserId = testId;
    }
  }

  /**
   * Exception if the principal header is missing. Default <tt>false</tt>
   * @param exceptionIfHeaderMissing
   */
  public void setExceptionIfHeaderMissing(boolean exceptionIfHeaderMissing) {
    this.exceptionIfHeaderMissing = exceptionIfHeaderMissing;
  }

  public void setAuthenticationDetailsSource(AuthenticationDetailsSource source) {
    log.info("testing authenticationDetailsSource set " + source);
    super.setAuthenticationDetailsSource(source);
  }
}
public class HeaderAuthenticationDetails extends AuthenticationDetailsSourceImpl {
  protected final Logger log = LoggerFactory.getLogger(HeaderAuthenticationDetails.class);

  /**
   * Can be setup in applicationContext-security if the ROLES header value is
   * not found.
   */
  private Set<string> testingRoles = new HashSet<string>();

  /**
   * Security principal will only contain roles from "allRoles" - letting us
   * cut down the irrelevant values setup by the webgate SSO header.
   */
  protected Set<string> allRoles = new HashSet<string>();

  /**
   * setup in applicationContext-security
   */
  private String roleHeaderName = "ROLES";

  protected Attributes2GrantedAuthoritiesMapper grantedAuthoritiesMapper
    = new SimpleAttributes2GrantedAuthoritiesMapper();

  public HeaderAuthenticationDetails() {
    super.setClazz(PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails.class);
  }

  /**
   * Build the authentication details object. If the specified authentication
   * details class implements {@link MutableGrantedAuthoritiesContainer}, a
   * list of pre-authenticated Granted Authorities will be set based on the
   * roles for the current user.
   */
  public Object buildDetails(Object context) {
    Object result = super.buildDetails(context);
    List<grantedauthority> userGas = new ArrayList<grantedauthority>();
    if (result instanceof MutableGrantedAuthoritiesContainer) {
      Collection<string> userRoles = getUserRoles(context, allRoles);
      userGas = grantedAuthoritiesMapper.getGrantedAuthorities(userRoles);
      ((MutableGrantedAuthoritiesContainer) result).setGrantedAuthorities(userGas);
    }
    return result;
  }

  /**
   * Allows the roles of the current user to be determined from the context
   * object
   *
   * @param context
   *            the context object (HttpRequest, PortletRequest etc)
   * @param mappableRoles
   *            the possible roles determined by the
   *            MappableAttributesRetriever
   * @return Collection<string> subset of mappable roles current user has.
   */
  protected Collection<string> getUserRoles(Object context, Set<string> mappableRoles) {
    ArrayList<string> requestRoles = new ArrayList<string>();
    if (((HttpServletRequest) context).getHeader(roleHeaderName) != null) {
      String[] roles = ((HttpServletRequest) context).getHeader(roleHeaderName).split(",");
      for (int i = 0; i < roles.length; i++) {
        if (mappableRoles.contains(roles[i])) {
          requestRoles.add(roles[i]);
        }
      }
    } else if ( testingRoles != null) {
      log.warn("Failed to retrieve Roles from Header, for debug purposes set to testingRole");
      requestRoles.addAll(testingRoles);
    } else {
      log.warn("Failed to retrieve Roles from Header, setup as 'user' role.");
      requestRoles.add("USER");
    }
    // add them to the session for convenience
    ((HttpServletRequest) context).getSession().setAttribute("ROLES", requestRoles);
    return requestRoles;
  }

  /**
   * @param mapper
   *            The Attributes2GrantedAuthoritiesMapper to use
   */
  public void setUserRoles2GrantedAuthoritiesMapper(Attributes2GrantedAuthoritiesMapper mapper) {
    grantedAuthoritiesMapper = mapper;
  }

  /**
   * All available roles for this application
   *
   * @param allRoles
   */
  public void setAllRoles(Set<string> allRoles) {
    this.allRoles = allRoles;
  }
  /**
   * @param roleHeaderName
   */
  public void setRoleHeaderName(String roleHeaderName) {
    this.roleHeaderName = roleHeaderName;
  }
  /**
   * @param testingRole
   */
  public void setTestingRoles(Set<string> testingRole) {
    this.testingRoles = testingRole;
  }
}

web.xml updates

These snippets will look familiar if you've ever used spring security; define the filter, map to all resources. Spring contextConfiguration locations should have your resource properties loaded first, then security config, and everything else.

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
 classpath:/ctx/applicationContext-resources.xml
 classpath:/ctx/applicationContext-security.xml
 /WEB-INF/security.xml
 /WEB-INF/applicationContext*.xml
    </param-value>
  </context-param>

  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

Final Step - Setup Spring Security

security.xml is application specific, but my sample application is based on appfuse - JSF version - so it should cover most example uses.

The intercept-url element defines the roles or authentication states that are required to access a URL path. Roles should be comma-delimited.

To restrict pages by user type instead of user role the following values can be used: IS_AUTHENTICATED_ANONYMOUSLY - Allow access to any user. IS_AUTHENTICATED_REMEMBERED - Allow access to logged-in users or users with a "remember me" cookie. IS_AUTHENTICATED_FULLY - Allow access to logged-in users.

To remove all Spring Security processing from a page use the filters="none" attribute.

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
              http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">

 <http auto-config="true" lowercase-comparisons="false">
  <intercept-url pattern="/images/**" filters="none" />
  <intercept-url pattern="/styles/**" filters="none" />
  <intercept-url pattern="/scripts/**" filters="none" />
  <intercept-url pattern="/javax.faces.resource/**"
   filters="none" />

  <!-- direct xhtml access disallowed -->
  <intercept-url pattern="/**/*.xhtml" access="ROLE_NOBODY" />

  <!-- local authentication is unused, but this is how it's configured -->
  <intercept-url pattern="/j_security*" access="IS_AUTHENTICATED_ANONYMOUSLY" />
  <intercept-url pattern="/login*" access="IS_AUTHENTICATED_ANONYMOUSLY" />

  <intercept-url pattern="/a4j.res/**"
   access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER" />
  <intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
  <intercept-url pattern="/user/**" access="ROLE_ADMIN,ROLE_USER" />

  <!-- show request headers and session variables for any user -->
  <intercept-url pattern="/env.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />

  <intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />

  <!-- matches the bean name for HeaderAuthenticationFilter class above -->
  <custom-filter position="PRE_AUTH_FILTER" ref="ssoHeaderFilter" />

  <form-login login-page="/login" authentication-failure-url="/login?error=true"
   login-processing-url="/j_security_check" always-use-default-target="true"
   default-target-url="/" />
 </http>

 <authentication-manager alias="authenticationManager">
  <authentication-provider ref="preauthAuthProvider" />

  <!-- this is an example of alternate user authentication providers, although
   we only have the PRE_AUTH_FILTER defined above, so it isn't used. -->
  <authentication-provider>
   <user-service>
    <user authorities="ROLE_USER" name="guest" password="guest" />
   </user-service>
  </authentication-provider>
 </authentication-manager>
</beans:beans>

If you read this far, and you want a Ready-To-Go example of how all this fits together, leave a comment and I will upload a full-source war to a temporary share site. Please don't put your email in the comments.

Comments

comments powered by Disqus