Reputation: 605
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
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:
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