Showing posts with label authentication. Show all posts
Showing posts with label authentication. Show all posts

Saturday, July 5, 2014

Spring Security Run-As example using annotations and namespace configuration

Spring Security offers an authentication replacement feature, often referred to as Run-As, that can replace the current user's authentication (and thus permissions) during a single secured object invocation. Using this feature makes sense when a backend system invoked during request processing requires different privileges than the current application.
For example, an application might want to expose a financial transaction log to the currently logged in user, but the backend system that provides it only permits this action to the members of a special "auditor" role. The application can not simply assign this role to the user as that would potentially permit them to execute other restricted actions. Instead, the user can be given this right exclusively for viewing their transaction log.

Only two classes are used to implement this feature. Instances of RunAsManager are tasked with producing the actual replacement authentication tokens. A sensible default implementation is already provided by Spring Security. As with other types of authentication, it is also necessary to register an instance of an appropriate AuthenticationProvider.
<bean id="runAsManager"
    class="org.springframework.security.access.intercept.RunAsManagerImpl">
  <property name="key" value="my_run_as_key"/>
</bean>

<bean id="runAsAuthenticationProvider"
    class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
  <property name="key" value="my_run_as_key"/>
</bean>
Tokens produced by runAsManager are signed with the provided key (my_run_as_key in the example above) and are later checked against the same key by runAsAuthenticationProvider, in order to mitigate the risk of fake tokens being provided. These keys can have any value, but need to be the same in both objects. Otherwise, runAsAuthenticationProvider will reject the produced tokens as invalid.

If an instance is registered, RunAsManager will be invoked by AbstractSecurityInterceptor for every intercepted object invocation for which the user has already been given access. If RunAsManager returns a token, this token will be used be used instead of the original one for the duration of the invocation, thus granting the user different privileges. There are two key points here. In order for the authentication replacement feature to do anything, the call has to actually be secured (and thus intercepted), and the user has to already have been granted access.

To register a RunAsManager instance with the method security interceptor, something similar to the following is needed:
<global-method-security secured-annotations="enabled" run-as-manager-ref="runAsManager"/>
Now, all methods secured by the @Secured annotation will be able to trigger RunAsManager. One important point here is that global-method-security will only work in the Spring context in which it is defined. In Spring MVC applications, there usually are two Spring contexts: the parent context, attached to ContextLoaderListener, and the child context, attached to DispatcherServlet. To secure Controller methods in this way, global-method-security must be added to DispatcherServlet's context. To secure methods in beans not in this context, global-method-security should also be added to ContextLoaderListener's context. Otherwise, security annotations will be ignored.

The default implementation of RunAsManager (RunAsManagerImpl) will inspect the secured object's configuration and if it finds any attributes prefixed with RUN_AS_, it will create a token identical to the original, with the addition of one new GrantedAuthorty per RUN_AS_ attribute found. The new GrantedAuthority will be a role (prefixed by ROLE_ by default) named like the found attribute without the RUN_AS_ prefix.

So, if a user with a role ROLE_REGISTERED_USER invokes a method annotated with @Secured({"ROLE_REGISTERED_USER","RUN_AS_AUDITOR"}), e.g.

@Controller
public class TransactionLogController {

 @Secured({"ROLE_REGISTERED_USER","RUN_AS_AUDITOR"}) //Authorities needed for method access and authorities added by RunAsManager prefixed with RUN_AS_
 @RequestMapping(value = "/transactions",  method = RequestMethod.GET) //Spring MVC configuration. Not related to security
 @ResponseBody //Spring MVC configuration. Not related to security
 public List<Transaction> getTransactionLog(...) {
  ... //Invoke something in the backend requiring ROLE_AUDITOR
 {

 ...  //User does not have ROLE_AUDITOR here
}
the resulting token created by RunAsManagerImpl with be granted ROLE_REGISTERED_USER and ROLE_AUDITOR. Thus, the user will also be allowed actions, normally reserved for ROLE_AUDITOR members, during the current invocation, permitting them, in this case, to access the transaction log.

To enable runAsAuthenticationProvider, register it as usual:
<authentication-manager alias="authenticationManager">
    <authentication-provider ref="runAsAuthenticationProvider"/>
    ... other authentication-providers used by the application ...
</authentication-manager>
This is all that is necessary to have the default implementation activated.

Still, this setting will not work for methods secured by @PreAuthorize and @PostAuthorize annotations as their configuration attributes are differently evaluated (they are SpEL expressions and not a simple list or required authorities like with @Secured) and will not be recognized by RunAsManagerImpl. For this scenario to work, a custom RunAsManager implementation is required, as, at least at the time of writing, no applicable implementation is provided by Spring.

A custom RunAsManager implementation for use with @PreAuthorize/@PostAuthorize

A convenient implementation relying on a custom annotation is provided below:

public class AnnotationDrivenRunAsManager extends RunAsManagerImpl {

    @Override
    public Authentication buildRunAs(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        if(!(object instanceof ReflectiveMethodInvocation) || ((ReflectiveMethodInvocation)object).getMethod().getAnnotation(RunAsRole.class) == null) {
            return super.buildRunAs(authentication, object, attributes);
        }

        String roleName = ((ReflectiveMethodInvocation)object).getMethod().getAnnotation(RunAsRole.class).value();
       
        if (roleName == null || roleName.isEmpty()) {
            return null;
        }

        GrantedAuthority runAsAuthority = new SimpleGrantedAuthority(roleName);
        List<GrantedAuthority> newAuthorities = new ArrayList<GrantedAuthority>();
        // Add existing authorities
        newAuthorities.addAll(authentication.getAuthorities());
        // Add the new run-as authority
        newAuthorities.add(runAsAuthority);

        return new RunAsUserToken(getKey(), authentication.getPrincipal(), authentication.getCredentials(),
                newAuthorities, authentication.getClass());
    }
}

This implementation will look for a custom @RunAsRole annotation on a protected method (e.g. @RunAsRole("ROLE_AUDITOR")) and, if found, will add the given authority (ROLE_AUDITOR in this case) to the list of granted authorities. RunAsRole itself is just a simple custom annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RunAsRole {
    String value();
}
This new implementation would be instantiated in the same way as before:
<bean id="runAsManager"
    class="org.springframework.security.access.intercept.RunAsManagerImpl">
  <property name="key" value="my_run_as_key"/>
</bean>
And registered in a similar fashion:
<global-method-security pre-post-annotations="enabled" run-as-manager-ref="runAsManager">
    <expression-handler ref="expressionHandler"/>
</global-method-security>
The expression-handler is always required for pre-post-annotations to work. It is a part of the standard Spring Security configuration, and not related to the topic described here. Both pre-post-annotations and secured-annotations can be enabled at the same time, but should never be used in the same class. The protected controller method from above could now look like this:
@Controller
public class TransactionLogController {

 @PreAuthorize("hasRole('ROLE_REGISTERED_USER')") //Authority needed to access the method
 @RunAsRole("ROLE_AUDITOR") //Authority added by RunAsManager
 @RequestMapping(value = "/transactions",  method = RequestMethod.GET) //Spring MVC configuration. Not related to security
 @ResponseBody //Spring MVC configuration. Not related to security
 public List<Transaction> getTransactionLog(...) {
  ... //Invoke something in the backend requiring ROLE_AUDITOR
 {

 ... //User does not have ROLE_AUDITOR here
}

Saturday, May 30, 2009

Authentication and authorization with JBoss security domains

Authentication and authorization comprise the fundamentals of security, and as such, they are important aspects of almost any application. Having in mind that security is such a frequent issue, it comes as no surprise that many people have devised many different ways of carrying it out, and so we now have literally countless options to choose from. If you are going to deploy your application to JBoss AS, you can get the basic authentication and authorization set up in virtually no time. To carry out these tasks, JBoss uses the notion of a security domain overlaying the standard JAAS (Java Authentication and Authorization Service).

Configuring the data source (*-ds.xml)



In order to set up a security domain for your app to use, you first need to set up a data source against which you will check the user's credentials. More often than not, your source of data will be a relational database. JBoss conveniently provides templates for many populars databases, so setting up a data source should be a breeze. These templates can be found under
<JBoss home>/docs/examples/jca1. Notice that all the filenames in this directory end with "-ds". This is the convention for naming the data source configuration files.

Let's assume we want to use MySQL. All we need to do, is make a copy of mysql-ds.xml, edit it to reflect our situation, and put it into the deployment directory. Editing this file is pretty straight forward, you decide on a JNDI name you want your data source bound to (I'll use MySqlDS), input your connection URL, username and password. There's really no need to change the driver class.

When this is done, you place this file under <JBoss home>/server/<profile name>/deploy2. That's it for this step.

Configuring a security domain (login-config.xml)



Next, you need to edit JBoss' login-config.xml file (found under<JBoss home>/server/<profile name>/conf). There are other ways to set up the security domain, but is by far the easiest. All you need to do here is nest a snippet similar to the following inside the policy element.
<!-- My custom security domain -->
<application-policy name="custom-domain">
<authentication>
<login-module code="org.jboss.security.auth.spi.DatabaseServerLoginModule"
flag = "required">
<module-option name="dsJndiName">java:/MySqlDS</module-option>
<module-option name="principalsQuery">SELECT password FROM USERS WHERE username=?</module-option>
<module-option name="rolesQuery">SELECT role, 'Roles' FROM ROLES WHERE username=?</module-option>
</login-module>
</authentication>
</application-policy>

Pay attention that dsJndiName attribute value must be identical to the jndi-name you chose in your mysql-ds.xml, preceded by java:/.
principalsQuery is the SQL query used to retrieve the user's password which, of course, has to match the one entered, and rolesQuery is the SQL query used to retrieve the roles associated with the user (leave the 'Roles' part as it is, it's not clear from the JBoss' documentation what purpose it has).

Our security domain is now ready. The next thing to do would be to instruct our application to use it.

Declaring page-level security (web.xml)



Presume we have a web.xml with the following security constraints configured:
<security-constraint>
<web-resource-collection>
<web-resource-name>Secure Resource</web-resource-name>
<url-pattern>/secure/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>registereduser</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/error.html</form-error-page>
</form-login-config>
</login-config>
<security-role>
<role-name>registereduser</role-name>
</security-role>
<resource-ref>
<res-ref-name>jdbc/mysql</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>

This tells our app server that only the users with the registereduser role can access the resources under /secure/ (this is the job of the url-pattern element). It also tells we are going to use a form to perform authentication (notice the auth-method element), and that this form is found on the page named login.html (the form-login-page element). The request gets redirected to the page specified under form-error-page in case the login failed. You can also use BASIC authentication method (just place BASIC instead of FORM inside auth-method element) . This will cause the browser to display a simple pop-up dialog that asks for username and password when a secured resource is requested. You don't need form-login-config for this approach.

Creating a login form (j_security_check)



The login page is obviously required to contain a login form. This form must specify j_security_check as it's action, and it also must have a username input field named j_username and a password input field named j_password.

This is how it might look like in it's simplest form:
<form name="loginForm" method="post" action="j_security_check">
<table>
<tr>
<td>User Name:</td>
<td><input type="text" name="j_username"></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" name="j_password"></td>
</tr>
<tr colspan="2">
<td><input type="submit" value="login"></td>
</tr>
</table>
</form>

Binding the application to a security domain (jboss-web.xml)


So, web.xml dictates the security constraints, but there is no mention of how these constraints will be carried out. This is the job of the jboss-web.xml config file (which is, of course, JBoss specific). It is in this file where we specify what security domain we want to use for authentication. Here's how it should look like in this case:
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
<security-domain>java:/jaas/custom-domain</security-domain>
<resource-ref>
<res-ref-name>jdbc/mysql</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<jndi-name>java:/MySqlDS</jndi-name>
</resource-ref>
</jboss-web>

Pay attention to the security-domain element. It is necessary to prefix the domain name with java:/jaas/, and the name its self must be exactly the same as the one you typed into the login-config.xml file.

That's it! Now every unauthenticated request to the secure resource (under /secure/, as specified in web.xml) will first be redirected to the login page (or, in case of BASIC auth-method, a login pop-up will displayed). If the user logs in successfully and is authorized i.e. associated with any of the required roles (only registereduser in our case), the requested resource will be displayed. Otherwise, if the login attempt fails, or the user doesn't have any of the required roles, the error page will be displayed instead. Users stay logged in for the entire duration of the session.

Method-level security (isUserInRole)



Declarative security in web.xml is enough if you only need page-level security (based on the URL pattern), but if you need to dynamically check user's role on a method level, you can do so by calling isUserInRole("registereduser") on a request object (acquired from the ExtrnalContext). Here's a short example of a method doing this:

public Boolean isRegisteredUser() {
HttpServletRequest httpServletRequest =
(HttpServletRequest) FacesContext.getCurrentInstance() //first get the current FacesContext instance
.getExternalContext() //than get the ExternalContext
.getRequest(); //get the underlying request
// return True if this request was issued by a user with a "registereduser" role
return httpServletRequest.isUserInRole("registereduser");
}

You can get the username in pretty much the same way, just call getRemoteUser() instead of isUserInRole("...").

Notes:
1 <JBoss home> refers to your JBoss installation directory (the same as JBOSS_HOME environmental variable).
2 <profile name> refers to your deployment profile name (you will probably be using default).