Sarang
Sarang

Reputation: 605

Role access in Custom Spring authentication

I am trying to use Spring Security to secure my Rest API's. So my requirement is that user should pass an apiKey in header with api call, and it will get validated w.r.t to predefined credentials.

So let us say i have apikey : 'ABCdEfG' with Role: 'ROLE_ADMIN'

So i wrote cutom implimentation of security filter and authentication provider. The authentication with respect to apiKey is working fine but it is not validating role required for the perticular api.

i.e. i am not able to access my api without apiKey, but the required role it is not able to validate.

My current Implementation looks as follows:

please let me know if i am doing wrong somewhere.

Application context:

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

<security:http entry-point-ref="authenticationEntryPoint"
    create-session="stateless">
    <security:intercept-url pattern="/api/*"
                access="ROLE_ADMIN" />
    <security:custom-filter before="FORM_LOGIN_FILTER"
        ref="restAuthenticationFilter" />
</security:http>

<bean id="restAuthenticationFilter"
    class="com.myapp.authentication.RestAuthenticationFilter2">
    <property name="authenticationManager" ref="authenticationManager" />
    <property name="authenticationSuccessHandler" ref="authenticationSuccessHandler" />
</bean>

<bean class="com.myapp.authentication.RestAuthenticationEntryPoint"
    id="authenticationEntryPoint"></bean>
<bean
    class="com.myapp.authentication.RestAuthenticationSuccessHandler"
    id="authenticationSuccessHandler"></bean>
<bean class="com.myapp.authentication.CustomAuthenticationProvider"
    id="customAuthenticationProvider"></bean>


<bean class="com.myapp.authentication.util.UserAuthenticationDAO"
    factory-method="getInstance" id="userAuthenticationDAO"></bean>
<security:authentication-manager alias="authenticationManager">
    <security:authentication-provider
        ref="customAuthenticationProvider" />
</security:authentication-manager>

Role.java

import org.springframework.security.core.GrantedAuthority;

@SuppressWarnings("serial")
public class Role implements GrantedAuthority {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthority() {
        return this.name;
    }

}

User.java

import java.util.List;

import org.springframework.security.core.userdetails.UserDetails;

@SuppressWarnings("serial")
public class User implements UserDetails {

    private String apiKey;

    /* Spring Security related fields */
    private List<Role> authorities;
    private boolean accountNonExpired = true;
    private boolean accountNonLocked = true;
    private boolean credentialsNonExpired = true;
    private boolean enabled = true;

    public String getApiKey() {
        return apiKey;
    }

    public void setApiKey(String apiKey) {
        this.apiKey = apiKey;
    }

    public List<Role> getAuthorities() {
        return authorities;
    }

    public void setAuthorities(List<Role> authorities) {
        this.authorities = authorities;
    }

    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    public void setAccountNonExpired(boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    public void setAccountNonLocked(boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    public void setCredentialsNonExpired(boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    @Override
    public String getPassword() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public String getUsername() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean equals(Object obj) {
        return this.apiKey.equals(((User) obj).getApiKey());
    }
}

CustomAuthentiCationToken.java

    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

public class CustomAuthenticationToken extends UsernamePasswordAuthenticationToken {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private String token;

    public CustomAuthenticationToken(String token) {
        super(null, null);
        this.token = token;
    }

    public String getToken() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return null;
    } }

AuthenticationFilter

    import java.io.IOException;

    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;

    import com.myapp.authentication.bean.CustomAuthenticationToken;

    public class RestAuthenticationFilter2 extends AbstractAuthenticationProcessingFilter {
        protected RestAuthenticationFilter2() {
            super("/**");
        }

        @Override
        protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
            return true;
        }

        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {

            String header = request.getHeader("Authorization");

            if (header == null) {
                throw new BadCredentialsException("No token found in request headers");
            }

            //String authToken = header.substring(7);
            String authToken = header.trim();

            CustomAuthenticationToken authRequest = new CustomAuthenticationToken(authToken);
            return getAuthenticationManager().authenticate(authRequest);
        }

        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                FilterChain chain, Authentication authResult) throws IOException, ServletException {
            super.successfulAuthentication(request, response, chain, authResult);

            // As this authentication is in HTTP header, after success we need to
            // continue the request normally
            // and return the response as if the resource was not secured at all
            chain.doFilter(request, response);
        }
    }

AuthenticationProvider

public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Autowired
    RetinaAuthenticationService retinaAuthenticationService;

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // TODO Auto-generated method stub

    }

    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        CustomAuthenticationToken customAuthenticationToken = (CustomAuthenticationToken) authentication;
        String token = customAuthenticationToken.getToken();

        User user = retinaAuthenticationService.loadUserByApiKey(token);

        if (null != user) {
            return user;
        } else {
            throw new BadCredentialsException("API token is not valid");
        }
    }

}

Upvotes: 2

Views: 3639

Answers (1)

jlumietu
jlumietu

Reputation: 6444

According to the security configuration you wrote

<security:http entry-point-ref="authenticationEntryPoint"
    create-session="stateless">
    <security:intercept-url pattern="/api/*"
                access="ROLE_ADMIN" />
    <security:custom-filter before="FORM_LOGIN_FILTER"
        ref="restAuthenticationFilter" />
</security:http>

You are stating that any incoming request to /api/* (what means that http://localhost:8080/myapp/api/test will be secured, but neither http://localhost:8080/myapp/api/ nor http://localhost:8080/myapp/api/more/test, these are not secured) must have ROLE_ADMIN as granted authority.

As you set create-session as stateless, any request must be validated, so you must include authentication credentials (in this case, the APIKEY) in every request.

Once the APIKEY is validated (so the request gets authenticated), then it would be checked if the Authentication instance returned by your CustomAuthenticationProvider has ROLE_ADMIN as granted autority. But you don't have to check it by your own, the spring-security filter chain (org.springframework.web.filter.DelegatingFilterProxy) would do it by itself.

So, there is no need to access by yourself to the authority you configured in access attribute of security:intercept-url element.

That finally means that if the User object returned by the provider has ROLE_ADMIN as authority in the List authorities, it will be allowed to hit the endpoint /api/test, otherwise not.

EDIT: I was quite annoyed, so I have tested your configuration, by copying the classes you posted and building the other stuff.

I build a fixed implementation of RetinaAuthenticationService like this, as was the piece left, based on an interface with method loadUserByApikey():

public interface RetinaAuthenticationService {

    public abstract User loadUserByApiKey(String token);

}

The implementation:

public class RetinaAuthenticationServiceImpl implements RetinaAuthenticationService {

    private Map<String, List<String>> apiKeyRoleMappings;

    @Override
    public User loadUserByApiKey(String token) {
        User user = null;
        if(this.apiKeyRoleMappings.containsKey(token)){
            user = new User();
            user.setApiKey(token);
            List<Role> authorities = new ArrayList<Role>();
            for(String roleStr : this.apiKeyRoleMappings.get(token)){
                Role role = new Role();
                role.setName(roleStr);
                authorities.add(role);
            }
            user.setAuthorities(authorities );
            user.setAccountNonExpired(true);
            user.setAccountNonLocked(true);
            user.setCredentialsNonExpired(true);
            user.setEnabled(true);
        }else{
            throw new BadCredentialsException("ApiKey " + token + " not found");
        }
        return user;
    }

    public Map<String, List<String>> getApiKeyRoleMappings() {
        return apiKeyRoleMappings;
    }

    public void setApiKeyRoleMappings(Map<String, List<String>> apiKeyRoleMappings) {
        this.apiKeyRoleMappings = apiKeyRoleMappings;
    }


}

Then I configured all in securiy-context.xml in a running project for testing purposes:

<security:http auto-config='false' pattern="/api/**" entry-point-ref="serviceAccessDeniedHandler" create-session="stateless" use-expressions="false">
    <security:intercept-url pattern="/api/*" access="ROLE_ADMIN" />
    <security:intercept-url pattern="/api/user/*" access="ROLE_USER,ROLE_ADMIN" />
    <security:custom-filter before="FORM_LOGIN_FILTER" ref="restAuthenticationFilter" />
    <security:csrf disabled="true"/>
</security:http>

<beans:bean id="restAuthenticationFilter"
    class="com.eej.test.security.filter.RestAuthenticationFilter2">
    <beans:property name="authenticationManager" ref="apiAuthenticationManager" />
    <beans:property name="authenticationSuccessHandler" ref="authenticationSuccessHandler" />
</beans:bean>

<beans:bean id="retinaAuthenticationServiceImpl" class="com.eej.test.security.services.RetinaAuthenticationServiceImpl">
    <beans:property name="apiKeyRoleMappings">
        <beans:map>
            <beans:entry key="aaaaa">
                <beans:list>
                    <beans:value>ROLE_USER</beans:value>
                </beans:list>
            </beans:entry>
            <beans:entry key="bbbbb">
                <beans:list>
                    <beans:value>ROLE_ADMIN</beans:value>
                </beans:list>
            </beans:entry>
            <beans:entry key="ccccc">
                <beans:list>
                    <beans:value>ROLE_USER</beans:value>
                    <beans:value>ROLE_ADMIN</beans:value>
                </beans:list>
            </beans:entry>
        </beans:map>
    </beans:property>
</beans:bean>

<!-- bean class="com.myapp.authentication.RestAuthenticationEntryPoint"  id="authenticationEntryPoint"></bean-->
<beans:bean
    class="com.eej.test.security.handler.RestAuthenticationSuccessHandler" id="authenticationSuccessHandler" />
<beans:bean class="com.eej.test.security.CustomAuthenticationProvider" id="customAuthenticationProvider" />

<!-- beans:bean class="com.myapp.authentication.util.UserAuthenticationDAO" factory-method="getInstance" id="userAuthenticationDAO" /-->
<security:authentication-manager alias="apiAuthenticationManager">
    <security:authentication-provider ref="customAuthenticationProvider" />
</security:authentication-manager>  

I made minor changes to yours (use a pre-existing entry point ref, apply a pattern to security:http section, as I already have an universal one in this project, set use-expressions to false, disable auto-config and disable csrf), changed the package name and comment unnecessary elements

I had to configure a bean for my class RetinaAuthenticationServiceImpl , where I set a map with this apikey-role mappings:

  • aaaaa > ROLE_USER
  • bbbbb > ROLE_ADMIN
  • ccccc > ROLE_USER,ROLE_ADMIN

And all works as it should. An access to a http://host:port/context/api/test return 200 where using token bbbbb and ccccc and 403 while using aaaaa.

Upvotes: 1

Related Questions