Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Securing REST API using Spring-security @PreAuthorize annotation and OAuth2

I'm struggling with some spring-security OAuth2 configuration.

I'm using:

  • Spring.version: 4.0.5.RELEASE
  • Spring security version: 3.2.5.RELEASE
  • Spring security oauth version: 2.0.2.RELEASE
  • Jersey version: 1.18.1

I want to secure my REST API using the PreAuthorize annotation of Spring security where I define the role that is authorized to access the method:

@Transactional
@POST
@PreAuthorize("hasRole('ROLE_ADMIN')")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response create(User user) throws BusinessException {
    LOG.info("POST Request: Creation of a new user with username [{}]", user.getUsername());
    UserValidator.validateUser(user);
    User createdUser = userDao.create(user);
    return Response.ok(createdUser).build();
}

When I call the API method with a valid bearer token for a user with role "ROLE_ADMIN", I get following exception:

09-Sep-2014 16:13:40.977 SEVERE [http-nio-8080-exec-6] com.sun.jersey.spi.container.ContainerResponse.mapMappableContainerException The RuntimeException     could not be mapped to a response, re-throwing to the HTTP container
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AbstractAccessDecisionManager.checkAllowIfAllAbstainDecisions(AbstractAccessDecisionManager.java:70)
at org.springframework.security.access.vote.UnanimousBased.decide(UnanimousBased.java:107)
at ...

When using postman, I see the following error description:

{
    "error": "unauthorized",
    "error_description": "Full authentication is required to access this resource"
}

This is my security config:

<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:context="http://www.springframework.org/schema/context"
   xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
   xmlns:sec="http://www.springframework.org/schema/security"
   xsi:schemaLocation="http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.2.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-3.2.xsd
    http://www.springframework.org/schema/security/oauth2
    http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd">

<context:property-placeholder location="classpath:main.properties"/>

<sec:global-method-security pre-post-annotations="enabled" />

<oauth:expression-handler id="oauthExpressionHandler" />

<!-- Definition of the Authentication Service -->
<sec:http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="clientAuthenticationManager">
    <sec:intercept-url pattern="/oauth/token" access="IS_AUTHENTICATED_FULLY"/>
    <sec:anonymous enabled="false"/>
    <sec:http-basic entry-point-ref="clientAuthenticationEntryPoint"/>
    <!--     include this only if you need to authenticate clients via request parameters -->
    <sec:custom-filter ref="clientCredentialsTokenEndpointFilter" after="BASIC_AUTH_FILTER"/>
    <sec:access-denied-handler ref="oauthAccessDeniedHandler"/>
</sec:http>

<!-- Protected resources -->
<sec:http auto-config="true"
    entry-point-ref="oauthAuthenticationEntryPoint"/>

<bean id="oauthAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
    <property name="realmName" value="dstest" />
</bean>

<bean id="clientAuthenticationEntryPoint"
      class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
    <property name="realmName" value="dstest/client"/>
    <property name="typeName" value="Basic"/>
</bean>

<bean id="oauthAccessDeniedHandler"
      class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"/>

<bean id="clientCredentialsTokenEndpointFilter"
      class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
    <property name="authenticationManager" ref="clientAuthenticationManager"/>
</bean>

<bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
    <constructor-arg>
        <list>
            <bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter"/>
            <bean class="org.springframework.security.access.vote.RoleVoter"/>
            <bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
        </list>
    </constructor-arg>
</bean>

<!-- Authentication in config file -->
<sec:authentication-manager id="clientAuthenticationManager">
    <sec:authentication-provider user-service-ref="clientDetailsUserService"/>
</sec:authentication-manager>

<sec:authentication-manager alias="authenticationManager">
    <sec:authentication-provider>
        <sec:jdbc-user-service data-source-ref="securityDataSource"/>
    </sec:authentication-provider>
</sec:authentication-manager>

<bean id="clientDetailsUserService"
      class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
    <constructor-arg ref="clientDetails"/>
</bean>

<!-- Token Store  -->
<bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore">
    <constructor-arg ref="securityDataSource" />
</bean>

<bean id="oAuth2RequestFactory" class="org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory">
    <constructor-arg ref="clientDetails"/>
</bean>

<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
    <property name="tokenStore" ref="tokenStore"/>
    <property name="supportRefreshToken" value="true"/>
    <property name="clientDetailsService" ref="clientDetails"/>
    <!-- VIV -->
    <property name="accessTokenValiditySeconds" value="10"/>
</bean>

<bean id="userApprovalHandler"
      class="org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler">
    <property name="tokenStore" ref="tokenStore"/>
    <property name="requestFactory" ref="oAuth2RequestFactory"/>
</bean>

<!-- Token management -->
<oauth:authorization-server client-details-service-ref="clientDetails" token-services-ref="tokenServices"
                            user-approval-handler-ref="userApprovalHandler" token-endpoint-url="/oauth/token">
    <oauth:authorization-code/>
    <oauth:implicit/>
    <oauth:refresh-token/>
    <oauth:client-credentials/>
    <oauth:password/>
</oauth:authorization-server>

<oauth:resource-server id="resourceServerFilter"
                       resource-id="dstest"
                       token-services-ref="tokenServices"/>

<!-- Client Definition -->
<oauth:client-details-service id="clientDetails">
    <oauth:client client-id="healthdata-client"
                  authorized-grant-types="password,authorization_code,refresh_token,implicit,redirect"
                  authorities="ROLE_CLIENT, ROLE_TRUSTED_CLIENT"
                  redirect-uri="/web"
                  scope="read,write,trust"
                  access-token-validity="300"
                  refresh-token-validity="6000"/>
</oauth:client-details-service>
</beans>

And this is my web.xml configuration:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">

    <display-name>Spring MVC Application BASIC AUTH</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:applicationContext.xml
            /WEB-INF/rest-dispatcher-servlet.xml
            /WEB-INF/security.xml
        </param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>

    <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>

    <servlet>
        <servlet-name>rest-dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>rest-dispatcher</servlet-name>
        <url-pattern>/oauth/token</url-pattern>
    </servlet-mapping>
    <servlet>

        <servlet-name>Spring servlets</servlet-name>
        <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>be.spring.security.api</param-value>
        </init-param>
        <init-param>
            <param-name>com.sun.jersey.config.property.packages</param-name>
            <param-value>be.spring;org.codehaus.jackson.jaxrs</param-value>
        </init-param>
        <!-- optional: JSON support apart from jaxb -->
        <init-param>
            <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Spring servlets</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>
like image 888
Mathias G. Avatar asked Oct 20 '22 02:10

Mathias G.


1 Answers

I found the solution to my problem. Web security services are configured using the element and I had to alter my protected resource definition from:

<sec:http auto-config="true"
    entry-point-ref="oauthAuthenticationEntryPoint"/>

to:

<sec:http auto-config="true">
    <sec:custom-filter ref="resourceServerFilter"
                       before="PRE_AUTH_FILTER"/>
    <sec:access-denied-handler
            ref="oauthAccessDeniedHandler"/>
</sec:http>

The resourceServerFilter must be referenced in order to be picked up by the security mechanism and the before attribute had to be set to "PRE_AUTH_FILTER".

Also the access-denied-handler is needed if you want proper error handling in a JSON format in the case you can't access the service.

like image 104
Mathias G. Avatar answered Oct 23 '22 02:10

Mathias G.