I'm working on a new REST Server that I develop with spring. The Server must have only server side logic, nothing js or views.
At the moment I started with Spring-boot version 1.2.7, but this only to give a server to the front-end developers until the setup for the server is done.
After I will change to the spring-core version 4.1 or something similar.
At the moment I have difficulty in configure the security part.
At the beginning I started with a Java Config configuration but after I changed to xml, because I already had a similar configuration.
My end result must be these:
hostname **/api/auth**** :is the entry point where a frontend developer make a **POST request with username and password of a customer. This call returns a token. This token permits me to identify the user the next time.
hostname /api/secure/resource/1 : is a resource that is protected, and can be accessible only with a valid token
hostname /api/other/1 : is an other type of resource that is not protected and can be accessible for everyone
hostname /api/secure/bambi/: is a resource that can be accessed from everyone but if it has a token then, more object-parameters are shown.
This seams for me a relative simple configuration, but I'm not able to configure it.
I know that this is not very his work, but for handle the token, and the resource access I would use OAUTH2 infrastructure (I'know, that this can be done better, but this was a requisite)
As follow I write you my configuration:
StartUpApplication.java
#SpringBootApplication(exclude = DispatcherServletAutoConfiguration.class)
#Import({ InMemoryDBConfigurationImpl.class})
#ImportResource({ "classpath:config/security-context.xml" })
public class SalustroApplication {
#Autowired
#Qualifier("InMemoryConfig")
private SystemConfiguration systemConfiguration;
public static void main(String[] args) {
SpringApplication app = new SpringApplication(SalustroApplication.class);
app.run(args);
}
#Bean
public ServletRegistrationBean foo() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(FooConfig.class);
dispatcherServlet.setApplicationContext(applicationContext);
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/");
servletRegistrationBean.setName("foo");
return servletRegistrationBean;
}
Do I need the foo method for the security part?
security-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<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:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<sec:http pattern="/api/auth" create-session="stateless"
authentication-manager-ref="clientAuthenticationManager" use-expressions="true">
<sec:intercept-url pattern="/api/auth" access="IS_AUTHENTICATED_FULLY" />
<sec:anonymous enabled="false" />
<sec:custom-filter ref="clientCredentialsTokenEndpointFilter"
after="BASIC_AUTH_FILTER" />
<sec:access-denied-handler ref="oauthAccessDeniedHandler" />
<sec:http-basic entry-point-ref="clientAuthenticationEntryPoint" />
<sec:csrf disabled="true" />
</sec:http>
<sec:http pattern="/api/**" create-session="never"
entry-point-ref="oauthAuthenticationEntryPoint" use-expressions="true"
access-decision-manager-ref="accessDecisionManager">
<sec:intercept-url pattern="/api/**"
access="isFullyAuthenticated() AND hasRole('ROLE_USER')" />
<sec:anonymous enabled="false" />
<sec:custom-filter ref="resourceServerFilter"
before="PRE_AUTH_FILTER" />
<sec:access-denied-handler ref="oauthAccessDeniedHandler" />
<sec:csrf disabled="true" />
<sec:headers />
</sec:http>
<sec:authentication-manager id="clientAuthenticationManager">
<sec:authentication-provider
user-service-ref="clientDetailsUserService" />
</sec:authentication-manager>
<bean id="accessDecisionManager"
class="org.springframework.security.access.vote.UnanimousBased"
c:decisionVoters-ref="votersList" />
<bean id="clientAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"
p:realmName="p4me-test/client" p:typeName="Basic" />
<bean id="clientCredentialsTokenEndpointFilter"
class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter"
p:authenticationManager-ref="clientAuthenticationManager"
p:filterProcessesUrl="/api/auth" />
<bean id="clientDetailsService"
class="app.security.ClientDetailsServiceImpl" />
<bean id="clientDetailsUserService"
class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService"
c:clientDetailsService-ref="clientDetailsService" />
<bean id="clientDetailServiceImpl" class="app.security.ClientDetailsServiceImpl" />
<bean id="oauthAccessDeniedHandler"
class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" />
<bean id="oauthAuthenticationEntryPoint"
class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"
p:realmName="p4me-test">
</bean>
<oauth:resource-server id="resourceServerFilter"
resource-id="test" token-services-ref="tokenServices" />
<bean id="tokenEnhancer" class="app.security.CustomTokenEnhancer" />
<bean id="tokenServices" class="app.security.CustomTokenServices"
p:tokenStore-ref="tokenStore" p:clientDetailsService-ref="clientDetailsService"
p:supportRefreshToken="true" p:tokenEnhancer-ref="tokenEnhancer"
p:accessTokenValiditySeconds="1800" />
<bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.InMemory TokenStore" />
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider ref="userAuthenticationProvider" />
</sec:authentication-manager>
<bean id="userAuthenticationProvider"
class="app.config.impl.security.SecureAuthenticationProvider"/>
<oauth:authorization-server
client-details-service-ref="clientDetailsService"
token-services-ref="tokenServices" user-approval-handler-ref="userApprovalHandler"
token-endpoint-url="/api/auth">
<oauth:authorization-code />
<oauth:implicit />
<oauth:refresh-token />
<oauth:client-credentials />
<oauth:password />
</oauth:authorization-server>
<bean id="userApprovalHandler"
class="org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler"
p:tokenStore-ref="tokenStore" p:requestFactory-ref="requestFactory" />
<bean id="requestFactory"
class="org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory"
c:clientDetailsService-ref="clientDetailServiceImpl" />
<util:list id="votersList">
<bean class="app.security.AccessVoter" />
<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" />
<bean class="org.springframework.security.web.access.expression.WebExpressionVoter">
<property name="expressionHandler">
<bean class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />
</property>
</bean>
</util:list>
The test class
#WebAppConfiguration
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = SalustroApplication.class)
public class AuthTest {
#Autowired
private WebApplicationContext context;
#Autowired
private Filter springSecurityFilterChain;
#Test
public void find1() throws Exception {
ResultActions doLogin = doLogin();
String contentAsString = doLogin.andReturn().getResponse().getContentAsString();
JSONObject json = new JSONObject(contentAsString);
DefaultMockMvcBuilder webAppContextSetup = MockMvcBuilders.webAppContextSetup(context)
.addFilter(springSecurityFilterChain);
MockMvc build = webAppContextSetup.build();
final ResultActions userResult = build.perform(post("/api/secure/user/1")
.param("access_token", json.getString("access_token")).accept(MediaType.APPLICATION_JSON))
.andDo(print());
assertEquals(someUser, userResult);
}
protected ResultActions doLogin() throws Exception {
DefaultMockMvcBuilder webAppContextSetup = MockMvcBuilders.webAppContextSetup(context)
.addFilter(springSecurityFilterChain);
MockMvc build = webAppContextSetup.build();
final ResultActions loginResult = build.perform(post("/api/auth").param("grant_type", "password")
.param("client_id", "testId").param("client_secret", "testSecret").param("username", "someUser")
.param("password", "somePassword").param("scope", "read").accept(MediaType.APPLICATION_JSON)).andDo(print());
return loginResult;
}
}
SecureAuthenticationPriver.class
#Component
public class SecureAuthenticationProvider implements AuthenticationProvider {
protected final static Logger logger = LoggerFactory.getLogger(SecureAuthenticationProvider.class);
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
List<GrantedAuthority> grantedAuths = new ArrayList<>();
GrantedAuthority authorithy = new SimpleGrantedAuthority("USER");
grantedAuths.add(authorithy);
UserEntity authenticatedUser = userPersistence.findByUserName(name, password);
if (authenticatedUser != null) {
return new UsernamePasswordAuthenticationToken(name, password, grantedAuths);
} else
return null; }
#Override
public boolean supports(Class<?> authentication) {
return false;
}
}
AccessVoter.class
#Service
public class AccessVoter implements AccessDecisionVoter<Object> {
#Override
public boolean supports(final ConfigAttribute attribute) {
return true;
}
#Override
public boolean supports(final Class<?> clazz) {
return true;
}
#Override
#Transactional
public int vote(final Authentication authentication, final Object object,
final Collection<ConfigAttribute> attributes) {
final Object principal = authentication.getPrincipal();
return 1;
}
private int refreshUserDetails(final Principal principal) {
return 1;
}
}
ClientDetailServiceImpl.class
public class ClientDetailsServiceImpl implements ClientDetailsService {
#Override
public ClientDetails loadClientByClientId(final String clientId) {
if ("invalid".equals(clientId)) {
throw new ClientRegistrationException(clientId + " not found");
}
return createClientDetails(clientId);
}
private ClientDetails createClientDetails(final String clientId) {
final Set<GrantedAuthority> grantAuthorities = new HashSet<GrantedAuthority>();
grantAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
final Set<String> authorizedGrantTypes = new HashSet<String>();
authorizedGrantTypes.add("password");
final BaseClientDetails details = new BaseClientDetails();
details.setClientId("testId");
details.setClientSecret("testSecret");
details.setAuthorizedGrantTypes(authorizedGrantTypes);
details.setAuthorities(grantAuthorities);
return details;
}
}
CustomTokenEnhancer.class
#Component
public class CustomTokenEnhancer implements TokenEnhancer {
private List<TokenEnhancer> delegates = Collections.emptyList();
#Autowired
private UserService userService;
public void setTokenEnhancers(final List<TokenEnhancer> delegates) {
this.delegates = delegates;
}
#Override
public OAuth2AccessToken enhance(final OAuth2AccessToken accessToken, final OAuth2Authentication authentication) {
final DefaultOAuth2AccessToken tempResult = (DefaultOAuth2AccessToken) accessToken;
// tempResult.setAdditionalInformation(getAuthenticationMethod(authentication));
OAuth2AccessToken result = tempResult;
for (final TokenEnhancer enhancer : delegates) {
result = enhancer.enhance(result, authentication);
}
return result;
}
private boolean isAdmin(final Collection<GrantedAuthority> authorities) {
for (final GrantedAuthority grantedAuthority : authorities) {
if (grantedAuthority.getAuthority().compareTo("ROLE_ADMIN") == 0) {
return true;
}
}
return false;
}
}
CustomTokenServices.class
public class CustomTokenServices extends DefaultTokenServices {
private TokenStore tokenStore;
private ClientDetailsService clientDetailsService;
private TokenEnhancer accessTokenEnhancer;
#Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(tokenStore, "tokenStore must be set");
}
#Override
public OAuth2AccessToken createAccessToken(final OAuth2Authentication authentication) {
final OAuth2AccessToken existingAccessToken = tokenStore
.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null && existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
refreshToken = createRefreshToken(authentication);
final ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
final OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
#Override
public OAuth2Authentication loadAuthentication(final String accessTokenValue) {
final DefaultOAuth2AccessToken accessToken = (DefaultOAuth2AccessToken) tokenStore
.readAccessToken(accessTokenValue);
if (accessToken == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
} else if (accessToken.isExpired()) {
tokenStore.removeAccessToken(accessToken);
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
}
final OAuth2Authentication result = tokenStore
.readAuthentication(accessToken);
if (clientDetailsService != null) {
final String clientId = result.getOAuth2Request().getClientId();
try {
clientDetailsService.loadClientByClientId(clientId);
} catch (final ClientRegistrationException e) {
throw new InvalidTokenException("Client not valid: " + clientId, e);
}
}
final int validitySeconds = getAccessTokenValiditySeconds(result
.getOAuth2Request());
accessToken
.setExpiration(new Date(System.currentTimeMillis() + validitySeconds * 1000L));
return result;
}
private ExpiringOAuth2RefreshToken createRefreshToken(final OAuth2Authentication authentication) {
if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
return null;
}
final int validitySeconds = getRefreshTokenValiditySeconds(authentication
.getOAuth2Request());
final ExpiringOAuth2RefreshToken refreshToken = new DefaultExpiringOAuth2RefreshToken(UUID
.randomUUID().toString(), new Date(System.currentTimeMillis() + validitySeconds * 1000L));
return refreshToken;
}
private OAuth2AccessToken createAccessToken(final OAuth2Authentication authentication, final OAuth2RefreshToken refreshToken) {
final DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID
.randomUUID().toString());
final int validitySeconds = getAccessTokenValiditySeconds(authentication
.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + validitySeconds * 1000L));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer
.enhance(token, authentication) : token;
}
#Override
public void setTokenEnhancer(final TokenEnhancer accessTokenEnhancer) {
this.accessTokenEnhancer = accessTokenEnhancer;
}
#Override
public void setTokenStore(final TokenStore tokenStore) {
this.tokenStore = tokenStore;
}
#Override
public void setClientDetailsService(final ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
}
How I said at the beginning: I started from an other working copy of configuration, and transformed to the needs of this application.
It is also possible that I confused a little bit some config.
I repeat the at the end I would utilize the OAUTH2 system to generate a token and utilize this token to authenticate the users. This authentication is made at with /api/auth (or /api/secure/auth?), the resources are available under /api/secure only for user with a valid token and other resources are available under /api/yyy and if they have a token more info is returned
When I run the test to try to make take a resource I receive this error:
Body = {"error":"access_denied","error_description":"Access is denied"}
Now I don't know exactely where I have to operate. In the security-context.xml or add some class to check the token.
The Exception
Body = {"error":"unauthorized","error_description":"There is no client authentication. Try adding an appropriate authentication filter."}
Is actually related to AuthTest test class where the WebApplicationContext doesn't contain the filter chain for the Spring Security. You need to make the following changes for your AuthTest test class.
#WebAppConfiguration
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = SalustroApplication.class)
public class AuthTest {
#Autowired
private WebApplicationContext context;
// Inject this
#Autowired
private Filter springSecurityFilterChain;
#Test
public void testLogin() throws Exception {
ResultActions doLogin = doLogin();
assertEquals(doLogin.andReturn().getResponse().getContentAsString(), "A valid Token");
}
protected ResultActions doLogin() throws Exception {
DefaultMockMvcBuilder webAppContextSetup = MockMvcBuilders.webAppContextSetup(context).addFilter(springSecurityFilterChain); // Add filter
MockMvc build = webAppContextSetup.build();
final ResultActions loginResult = build.perform(post("/api/auth").param("grant_type", "password")
.param("client_id", "testId").param("client_secret", "testSecret").param("username", "aUserName")
.param("password", "123456").accept(MediaType.APPLICATION_JSON)).andDo(print());
return loginResult;
}
}
Related
i have my custom filter :
#Component("MyAuthFilter")
public class MyAuthFilter extends UsernamePasswordAuthenticationFilter {
private int errCode = 0;
#Autowired
#Qualifier("authenticationManager")
#Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
System.out.println("running my own version of UsernmePasswordFilter ... ");
String login = (String) request.getParameter("login");
String password = (String) request.getParameter("password");
errCode = validate(login, password);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(login, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private int validate(String login, String password) {
if (login.isEmpty() && password.isEmpty()) {
return 4;
}
if (login.isEmpty() && !password.isEmpty()) {
return 2;
}
if (!login.isEmpty() && password.isEmpty()) {
return 3;
}
return 1;
}
}
this is my spring-security.xml:
<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-4.2.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.2.xsd">
<http auto-config="false" use-expressions="true">
<intercept-url pattern="/courses*" access="hasRole('ROLE_USER')" />
<custom-filter before="FORM_LOGIN_FILTER" ref="MyAuthFilter" />
<form-login
login-page="/login"
default-target-url="/courses"
authentication-failure-url="/login"
username-parameter="loginField"
password-parameter="passwordField"
/>
<csrf disabled="true" />
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider>
<user-service>
<user name="ars" password="1234" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
i want to set DefaultFailureUrl to ("/login?error=" + errCode);
how can i do this from my filter?
If i'm trying to do something like
#Bean
#Override
public AuthenticationFailureHandler getFailureHandler() {
SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler();
handler.setDefaultFailureUrl("/login?error=" + errCode);
return handler;
}
it shows an error that AuthenticationFailureHandler doesn't found. I just want to use default spring security Failure handler and just change its url.
I made a custom filter and failureHandler. But to make it work i need to register handler in filter. So i willd be glad if someone will write how to do it in my code. I know that there are a lot of examples in stackowerflow but i'm new to spring and java and to understand how it works i need an example for my app. Please don't answer with "this is a duplicate".
My filter:
#Component("MyAuthFilter")
public class MyAuthFilter extends UsernamePasswordAuthenticationFilter {
private int errCode = 5;
#Autowired
#Qualifier("authenticationManager")
#Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
System.out.println("running my own version of UsernmePasswordFilter ... ");
String login = (String) request.getParameter("login");
String password = (String) request.getParameter("password");
errCode = validate(login, password);
System.out.println(login + " - " + password);
System.out.println(request.getQueryString());
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(login, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private int validate(String login, String password) {
if (login.isEmpty() && password.isEmpty()) {
return 4;
}
if (login.isEmpty() && !password.isEmpty()) {
return 2;
}
if (!login.isEmpty() && password.isEmpty()) {
return 3;
}
return 1;
}
}
here is my handler:
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
public LoginFailureHandler() {
System.out.println("i debug");
}
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
System.out.println("do smth");
super.onAuthenticationFailure(request, response, exception);
}
}
and my spring-security.xml:
<beans:bean id="authenticationFailureHandler" class="com.webproject.LoginFailureHandler" />
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/courses*" access="hasRole('ROLE_USER')" />
<custom-filter before="FORM_LOGIN_FILTER" ref="MyAuthFilter" />
<form-login
login-page="/login"
default-target-url="/courses"
username-parameter="loginField"
password-parameter="passwordField"
authentication-failure-handler-ref="authenticationFailureHandler"
/>
<csrf disabled="true" />
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider>
<user-service>
<user name="ars" password="1234" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
Declare to beans and autowire them to your filter
#Bean
public AuthenticationFailureHandler getFailureHandler(){
SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler();
handler.setDefaultFailureUrl("/login.html");
return handler;
}
MyAuthFilter
#Autowired
#Qualifier("authenticationManager")
#Override
public void setAuthenticationManager(AuthenticationManager authenticationManager, AuthenticationFailureHandler failureHandler) {
this.setAuthenticationManager(authenticationManager);
this.setAuthenticationFailureHandler(failureHandler);
}
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");
}
}
}
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.
I am working with spring-security 3.1 to implement two different logons. The first thing I have to a database that brings me CustomUserDetailService credentials datos.Este the same database is for administrator access. The second port is for the user but the information comes from a web service I call him with me a method validates the user. The problem I have with the second port, and to develop a CustomAuthenticationProvider for the second AuthenticationManager (web service), but when I try to access the spring-security user sends me to error page login.html? Error = true the furmulario administrator access. Esteb is my configuration file:
<http pattern="../resources/**" security="none" />
<http pattern="/login.html*" security="none" />
<http pattern="/loginUser.html*" security="none" />
<!-- USER -->
<http auto-config="true" authentication-manager-ref="wsAuthenticationManager" use-expressions="true" pattern="/testUser/**">
<intercept-url pattern="/loginUser.html" access="permitAll" />
<intercept-url pattern="/testUser/**" access="hasRole('user')" />
<access-denied-handler error-page="/403" />
<form-login login-page="/loginUser.html"
authentication-failure-url="/loginUser.html?login_error=true"
default-target-url="/testUser" />
<logout invalidate-session="true" logout-success-url="/logintUser.html" />
</http>
<beans:bean id="customAuthenticationProvider" class="net.universia.test.service.CustomAuthenticationProvider" />
<!-- Acceso contra WS -->
<authentication-manager id="wsAuthenticationManager">
<authentication-provider ref="customAuthenticationProvider" />
</authentication-manager>
<!--ADMIN -->
<http auto-config="true" use-expressions="true" authentication-manager-ref="authenticationManager" >
<intercept-url pattern="/login.html" access="permitAll" />
<intercept-url pattern="/test/**" access="hasRole('admin')" />
<intercept-url pattern="/member/**" access="hasAnyRole('moderator','admin')" />
<intercept-url pattern="/testUser/**" access="hasRole('admin')" />
<access-denied-handler error-page="/403" />
<form-login login-page="/login.html"
authentication-failure-url="/login.html?login_error=true"
username-parameter="j_username" password-parameter="j_password"/>
<logout invalidate-session="true" logout-success-url="/loginUser.html" />
<remember-me user-service-ref="customUserDetailsService" />
</http>
<beans:bean id="customUserDetailsService" class="net.universia.test.service.CustomUserDetailsService" />
<beans:bean id="md5PasswordEncoder"
class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" />
<!-- Acceso contra base de datos -->
<authentication-manager alias="authenticationManager" id="authenticationManager">
<authentication-provider user-service-ref="customUserDetailsService">
<password-encoder hash="md5" />
</authentication-provider>
</authentication-manager>
</beans:beans>
CustomUserDetailService para administrator:
#Service
#Transactional(readOnly=true)
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private HibernateTestAdminDaoImpl userDAO;
public UserDetails loadUserByUsername(String login)throws UsernameNotFoundException {
TestAdmin userAdmin = null;
try {
userAdmin = userDAO.getTestAdmin(login);
} catch (BussinessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
return new User(
userAdmin.getLoginAdmin(),
userAdmin.getPasswordAdmin(),
enabled,
accountNonExpired,
credentialsNonExpired,
accountNonLocked,
getAuthorities(userAdmin.getRole().getIdRole())
);
}
public Collection<? extends GrantedAuthority> getAuthorities(Integer role) {
List<GrantedAuthority> authList = getGrantedAuthorities(getRoles(role));
return authList;
}
public List<String> getRoles(Integer role) {
List<String> roles = new ArrayList<String>();
if (role.intValue() == 1) {
roles.add("admin");
roles.add("moderator");
} else if (role.intValue() == 2) {
roles.add("moderator");
}
return roles;
}
public static List<GrantedAuthority> getGrantedAuthorities(List<String> roles) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
}
CustomAuthenticationProvider user:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private HibernateTestUsuarioDaoImpl userDAO;
UniversiaUser usw;
public CustomAuthenticationProvider() {
super();
}
// Retorna credenciales del usuario web service
public Authentication authenticate(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
/*
final String loginUser = authentication.getName();
final String password = authentication.getCredentials().toString();
try {
usw = userDAO.loginUserWS(loginUser, password);
} catch (UserRegistryWSException e) {
String errorCode = e.getLocalizedMessage();
System.out.print(errorCode);
} catch (Exception e) {
UsuarioUniversiaException ee = new UsuarioUniversiaException(
UsuarioUniversiaException.FERIA_VIRTUAL_USER_ERROR_LOGIN,
e);
ee.setLogin(loginUser);
throw ee;
}
if (usw.getEmail().equals("loginUser")) {
final List<GrantedAuthority> grantedAuths = new ArrayList<>();
grantedAuths.add(new SimpleGrantedAuthority("user"));
final UserDetails principal = new User(loginUser, password, grantedAuths);
final Authentication auth = new UsernamePasswordAuthenticationToken(principal, password, grantedAuths);
return auth;
} else {
return null;
}
*/
//Test parameters
final String loginUser = request.getParameter("username");
final String password = request.getParameter("password");
if (loginUser.equals("admin") && password.equals("system")) {
final List<GrantedAuthority> grantedAuths = new ArrayList<>();
grantedAuths.add(new SimpleGrantedAuthority("user"));
final UserDetails principal = new User(loginUser, password, grantedAuths);
final Authentication auth = new UsernamePasswordAuthenticationToken(principal, password, grantedAuths);
return auth;
} else {
return null;
}
}
#Override
public boolean supports(final Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// TODO Auto-generated method stub
return null;
}
}
In customautheticationprovider discuss what comes from the webservice and send test parameters
Thanks and any help is welcome
Now I have two running !!!
One customAuthenticationProvider user and customAuthenticationDetailService for Administrator and implement each filter
I'm using spring security 3.1.4 and I'm trying to create a costume session registry that uses data base for saving open sessions.
I used this SessionRegistryImpl implementation as reference :https://github.com/spring-projects/spring-security/blob/master/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java
This is my implementation:
#Component
public class ClusteredSessionRegistryImpl implements SessionRegistry, ApplicationListener<SessionDestroyedEvent> {
#Autowired
private PrincipalRepository principalRepository;
#Autowired
private SessionIdRepository sessionIdRepository;
public List<Object> getAllPrincipals() {
return new ArrayList<Object>(
this.principalRepository.findAllPrincipals());
}
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
final Set<String> sessionsUsedByPrincipal = this.principalRepository.findByPrincipal(
principal).getSessionsUsedByPrincipal();
if (sessionsUsedByPrincipal == null) {
return Collections.emptyList();
}
List<SessionInformation> list = new ArrayList<SessionInformation>(
sessionsUsedByPrincipal.size());
for (String sessionId : sessionsUsedByPrincipal) {
SessionInformation sessionInformation = getSessionInformation(sessionId);
if (sessionInformation == null) {
continue;
}
if (includeExpiredSessions || !sessionInformation.isExpired()) {
list.add(sessionInformation);
}
}
return list;
}
public SessionInformation getSessionInformation(String sessionId) {
Assert.hasText(
sessionId,
"SessionId required as per interface contract");
SessionId sessionIdDoc = this.sessionIdRepository.findBySessionId(sessionId);
if (sessionIdDoc == null)
return null;
return sessionIdDoc.getSessionInformation();
}
public void onApplicationEvent(SessionDestroyedEvent event) {
String sessionId = event.getId();
removeSessionInformation(sessionId);
}
public void refreshLastRequest(String sessionId) {
Assert.hasText(
sessionId,
"SessionId required as per interface contract");
SessionInformation info = getSessionInformation(sessionId);
if (info != null) {
info.refreshLastRequest();
}
}
public void registerNewSession(String sessionId, Object principal) {
Assert.hasText(
sessionId,
"SessionId required as per interface contract");
Assert.notNull(
principal,
"Principal required as per interface contract");
if (getSessionInformation(sessionId) != null) {
removeSessionInformation(sessionId);
}
this.sessionIdRepository.save(new SessionId(
sessionId, new SessionInformation(
principal, sessionId, new Date())));
Principal principalDoc = this.putIfAbsent(principal);
Set<String> sessionsUsedByPrincipal = principalDoc.getSessionsUsedByPrincipal();
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = new CopyOnWriteArraySet<String>();
}
sessionsUsedByPrincipal.add(sessionId);
principalDoc.setSessionsUsedByPrincipal(sessionsUsedByPrincipal);
this.principalRepository.save(principalDoc);
}
public void removeSessionInformation(String sessionId) {
Assert.hasText(
sessionId,
"SessionId required as per interface contract");
SessionInformation info = getSessionInformation(sessionId);
if (info == null) {
return;
}
SessionId sessionIdDoc = this.sessionIdRepository.findBySessionId(sessionId);
if (sessionIdDoc == null) {
return;
}
this.sessionIdRepository.delete(sessionIdDoc);
Set<String> sessionsUsedByPrincipal = this.principalRepository.findByPrincipal(
info.getPrincipal()).getSessionsUsedByPrincipal();
if (sessionsUsedByPrincipal == null) {
return;
}
sessionsUsedByPrincipal.remove(sessionId);
if (sessionsUsedByPrincipal.isEmpty()) {
Principal principalDoc = this.principalRepository.findByPrincipal(info.getPrincipal());
this.principalRepository.delete(principalDoc);
}
else {
Principal principalDoc = this.principalRepository.findByPrincipal(info.getPrincipal());
principalDoc.setSessionsUsedByPrincipal(sessionsUsedByPrincipal);
this.principalRepository.save(principalDoc);
}
}
private Principal putIfAbsent(Object principal) {
Principal principalDoc = this.principalRepository.findByPrincipal(principal);
if (principalDoc == null) {
return this.principalRepository.save(new Principal(
principal, null));
}
else {
return principalDoc;
}
}
}
my security xml:
<security:http use-expressions="true" auto-config="false"
entry-point-ref="loginUrlAuthenticationEntryPoint">
<security:intercept-url pattern="/player/login"
access="permitAll" />
<security:custom-filter position="FORM_LOGIN_FILTER"
ref="twoFactorAuthenticationFilter" />
<security:intercept-url pattern="/**"
access="isAuthenticated()" />
<security:logout logout-url="/player/logout"
success-handler-ref="playerLogoutSuccessHandler" />
<security:session-management>
<security:concurrency-control
max-sessions="1" session-registry-ref="clusteredSessionRegistryImpl"
error-if-maximum-exceeded="true" />
</security:session-management>
</security:http>
<bean id="loginUrlAuthenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/demo/player/login" />
</bean>
<bean id="twoFactorAuthenticationFilter" class="com.xxx.filter.TwoFactorAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationFailureHandler" ref="failureHandler" />
<property name="authenticationSuccessHandler" ref="playerAuthenticationSuccessHandler" />
<property name="postOnly" value="true" />
<property name="extraParameter" value="domain" />
</bean>
<bean id="failureHandler"
class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="defaultFailureUrl" value="/login?login_error=true" />
</bean>
<bean id="bCryptPasswordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider
ref="authenticationProvider">
</security:authentication-provider>
</security:authentication-manager>
</beans>
The problam is it never enters the public void registerNewSession(String sessionId, Object principal)
so nothing is saved in the db.
EDIT:
I added a call to registerNewSession() from my authenticationSuccessHandler, is this a legitimate solution?
My new problem is that the max-sessions="1" doesn't work and I can open more then 1 session. (I override the equals and hash code of my principal)