Spring Security - separate configuration for REST API and other URLs

advertisements

I have two sets of URLs - one set is REST API, the second one - is the pretty ordinary website. I want to apply different security rules for REST API so that the user/script that occasionally invoked REST API will be answered either with 401 code (basic auth would be fine) - or just 403.

So I want to allow access to REST API for:

  • the user that has been logged in (for javascript on the site page that invokes REST API thus shares the same session).
  • some script that invokes REST API with basic auth credentials in the WWW-Authenticate header.

At the moment I'm trying to figure out what configuration will make spring "understand" what I want. I came up with the following config:

<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="
                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
                http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
    <http pattern="/" security="none" />
    <http pattern="/static/**" security="none" />

    <!-- REST API -->
    <http pattern="/rest/*" use-expressions="true">
        <http-basic />
        <intercept-url pattern="/*" access="isAuthenticated()" />
    </http>

    <!-- Site -->
    <http access-denied-page="/WEB-INF/views/errors/403.jsp" use-expressions="true">
        <intercept-url pattern="/index.html" access="hasRole('ROLE_USER') or hasRole('ROLE_ANONYMOUS')" />
        <intercept-url pattern="/login.html" access="hasRole('ROLE_USER') or hasRole('ROLE_ANONYMOUS')" />
        <intercept-url pattern="/admin/*" access="hasRole('ROLE_ADMIN')" />
        <intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
        <form-login login-page="/login.html"
                    default-target-url="/index.html"
                    authentication-failure-url="/login.html?error=1" />

        <logout logout-url="/logout.do" logout-success-url="/index.html" />

        <anonymous username="guest" granted-authority="ROLE_ANONYMOUS" />
        <remember-me />
    </http>

    <authentication-manager>
        <authentication-provider>
            <user-service>
                <user name="admin" password="2" authorities="ROLE_ADMIN,ROLE_USER" />
                <user name="alex" password="1" authorities="ROLE_USER" />
            </user-service>
        </authentication-provider>
    </authentication-manager>
</beans:beans>

Unfortunately not only basic authentication does not work with this config but there is no 403 responses for requests made by anonymous user - application answers with redirect 302 Found which I'd like to disallow for REST API urls.

I tried to add custom entry point for REST API:

<beans:bean id="ep403" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint"/>

<!-- REST API -->
<http pattern="/rest/*" entry-point-ref="ep403" use-expressions="true">
    <intercept-url pattern="/*" access="hasRole('ROLE_USER')" />
    <http-basic />
</http>

but this does not work either.

To sum what I do want to get:

  • Allow authorized user to access REST API (authorization facility should take into an account the user session in cookies).
  • Allow script to access REST API if it specifies correct authentication token and does not specifies session in cookies.

UPDATE

After digging into Spring Security internals I found the code that decides whether to deny certain request or not. This is a so called "Access Decision Voters", basically all of them applied to certain request and if one access decision voter in a chain votes to deny access to certain resource the request is ultimately denied.

Hence the original problem might be solved by introducing special access decision voter that behaves in the following way: it tries to extract associated role from the session (if any present in the request) and proceeds to authorization step with this role; if no session present it tries to authenticate the user against the credentials in WWW-Authenticate header and then proceeds to authorization step with the roles associated with the given user.


Reposting as a solution as [email protected] suggested :) Hope this will help someone.

I found a simple workaround for that. I just mapped one rest controller to two distinct sets of URLs and assigned each set to the distinct auth handler in the spring security config:

<!-- Defines custom security policy for Stateful REST API -->
<beans:bean id="nonRedirectingAccessDeniedHandler" class="org.springframework.security.web.access.AccessDeniedHandlerImpl"/>
<beans:bean id="forbiddenEntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint"/>

<!-- Stateful REST API -->
<http pattern="/rest/stateful/**" use-expressions="true" entry-point-ref="forbiddenEntryPoint">
    <access-denied-handler ref="nonRedirectingAccessDeniedHandler"/>
    <intercept-url pattern="/rest/stateful/**" access="isAuthenticated()" />
</http>

<!-- Stateless REST API -->
<http pattern="/rest/stateless/**" use-expressions="true" create-session="stateless">
    <http-basic/>
    <intercept-url pattern="/rest/stateless/**" access="isAuthenticated()" />
</http>

It looks like a good approach to that problem because in future you may want to extend either "stateful"-user or "stateless"-script REST API with the unique URLs specific to either user or script use cases.

In my case this may happen when UX changes will require REST API to provide some new methods designed to implement a particular UI scenario and some script scenario may require some URLs added solely to support certain client script-to-server interaction.