I´m developing an application with spring security authentication and MongoDB. The authentication method is working fine but only with ROLE_ADMIN. All methods that I need authentication for are annotated with:
#PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
When I try to authenticate with a user that has ROLE_USER I always get access declined error.
My spring security config is:
<security:global-method-security pre-post-annotations="enabled" />
<security:http auto-config="true" use-expressions="true">
<intercept-url pattern="/login" access="permitAll" />
<intercept-url pattern="/logout" access="permitAll" />
<intercept-url pattern="/admin/**"
access="hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')" />
<security:form-login login-page="/login" default-target-url="/admin/main"
authentication-failure-url="/accessdenied" />
<security:logout logout-success-url="/logout" />
<security:session-management>
<security:concurrency-control error-if-maximum-exceeded="true"
max-sessions="1"/>
</security:session-management>
</security:http>
If I use:
<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')" />
I get access denied for both ROLE_ADMIN and ROLE_USER.
If I use:
<intercept-url pattern="/admin/**" access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')" />
I can log in with a ROLE_ADMIN user but I can´t with ROLE_USER.
and in my LoginService I have:
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
User user = getUserDetail(email);
userdetails = new org.springframework.security.core.userdetails.User(
user.getEmail(), user.getPwd(), enabled,
accountNonExpired, credentialsNonExpired, accountNonLocked,
getAuthorities(user.getIsAdmin()));
return userdetails;
}
public List<GrantedAuthority> getAuthorities(Integer role) {
List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
if (role.intValue() == 0) {
authList.add(new SimpleGrantedAuthority("ROLE_USER"));
} else if (role.intValue() == 1) {
authList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
System.out.println(authList);
return authList;
}
What am I missing?
When you have this kind of problem, please try first adding many System.out.println (or debug log) to see if your method is working, also change:
hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')
for
hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')
And check if the role.intValue() == 0 prints true with a sysout.
This
access="hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')
means that both roles must be set the same time and sample code shows that it never happenes, because only one of two roles can be set.
It is different statement from hasAnyRole('ROLE_ADMIN', 'ROLE_USER') which should be true if at least one of these roles is set. Statment hasAnyRole is like or not and condition.
Related
I am working on a Spring-MVC application in which I am using Spring-security for authentication and authorization. In one of the features, we are sending invitation to users to join the application. When the user clicks on the email link, I login the user from backend.
If there is an user who is already logged in, the session from old user is not removed. I have set in the XMl to create a newSession, but that is not helping either. The result is I have 2 users logged in the same browser. ANy ideas, Thank you.
security-application-context.xml code :
<security:http pattern="/resources/**" security="none"/>
<security:http create-session="ifRequired" use-expressions="true" auto-config="false" disable-url-rewriting="true">
<security:form-login login-page="/login" username-parameter="j_username" password-parameter="j_password"
login-processing-url="/j_spring_security_check" default-target-url="/canvaslisting"
always-use-default-target="false" authentication-failure-url="/login?error=auth"/>
<security:remember-me key="_spring_security_remember_me" user-service-ref="userDetailsService"
token-validity-seconds="1209600" data-source-ref="dataSource"/>
<security:logout delete-cookies="JSESSIONID" invalidate-session="true" logout-url="/j_spring_security_logout"/>
<security:intercept-url pattern="/**" requires-channel="https"/>
<security:port-mappings>
<security:port-mapping http="80" https="443"/>
</security:port-mappings>
<security:logout logout-url="/logout" logout-success-url="/" success-handler-ref="myLogoutHandler"/>
<security:session-management session-fixation-protection="migrateSession">
<security:concurrency-control session-registry-ref="sessionReg" max-sessions="5" expired-url="/login"/>
</security:session-management>
</security:http>
<beans:bean id="sessionReg" class="org.springframework.security.core.session.SessionRegistryImpl"/>
Controller code :
#RequestMapping(value = "/activatemembership/{token}")
public String activateMembershipForExistingUser(){
boolean val = this.groupMembersService.activateMembers(token,true);
}
Service layer code :
// Logout is true when clicked from email.
#Override
public boolean activateMembers(String token, boolean logout) {
try {
String[] parts = token.split(TOKEN_SEPARATOR);
String username = parts[0].toLowerCase();
Long groupAccountId = Long.valueOf(parts[2]);
GroupAccount groupAccount = this.groupAccountService.getGroupObjectOnlyById(groupAccountId);
Person person = this.personService.findPersonByUsername(username);
if(logout) {
Person loggedInUser = this.personService.getCurrentlyAuthenticatedUser();
if (!(loggedInUser == null)) {
loggedInUser.setOnlineStatus(null);
this.personService.directPersonUpdate(loggedInUser);
SecurityContextHolder.getContext().setAuthentication(null);
}
}
//Other service layer code
if(logout) {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
Authentication authentication = new UsernamePasswordAuthenticationToken(person, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
Any ideas? Thank you.
I've read and applied the Creating a Custom Login tutorial for custom login page and also custom authentication provider to my project.
As far as I understand from the documentations, spring security handles the login error by putting a parameter to the url such as "login?error=blabla".
But in my situation, no matter what user enters except for the true credentials, no request parameters are shown. But normal logins (meaning true logins) works fine.
Is there something I miss ?
CustomAuthenticationProviderImpl.java
#Component
public class CustomAuthenticationProviderImpl implements UnalAuthenticationProvider {
#Autowired
private CustomUserDetailService customUserDetailService;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
UserDetails user = null;
try{
user = (UserDetails) customUserDetailService.loadUserByUsername(username);
}
catch(UsernameNotFoundException ex){
System.out.println("User name not found");
throw ex;
}
if (user == null) {
throw new BadCredentialsException("Username not found.");
}
if (!password.equals(user.getPassword())) {
throw new BadCredentialsException("Wrong password.");
}
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
return new UsernamePasswordAuthenticationToken(user, password, authorities);
}
public boolean supports(Class<?> arg0) {
return true;
}
}
spring-security-config.xml
<global-method-security pre-post-annotations="enabled" />
<http auto-config="true">
<intercept-url pattern="/resources/**" access="permitAll" />
<intercept-url pattern="/login" access="permitAll" />
<intercept-url pattern="/access-denied" access="permitAll" />
<intercept-url pattern="/admin**" access="hasRole('ADMIN')" />
<intercept-url pattern="/**" access="hasRole('USER')" />
<form-login login-page="/login" default-target-url="/main" always-use-default-target="true"/>
<logout logout-url="/logout" logout-success-url="/"/>
<headers>
<frame-options policy="SAMEORIGIN"/>
</headers>
<session-management>
<concurrency-control expired-url="/login" />
</session-management>
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider ref="customAuthenticationProviderImpl" />
</authentication-manager>
After giving some thought, I also decided to remove all intercept-url, thinking that might be a unintented redirection because of those... But it also didn't work.
At a first glance you miss a configuration parameter in your <form-login> section, namely the authentication-failure-url.
This is where you instruct Spring Security to redirect your failing login attempts.
Try to add it as follows:
<form-login login-page="/login" <!--other configurations--> authentication-failure-url="/login?error=true" />.
And, of course, take care of managing the error parameter in your custom login jsp.
Here is my spring security config:
<http pattern="/auth/login" security="none" />
<http pattern="/auth/loginFailed" security="none" />
<http pattern="/resources/**" security="none" />
<http auto-config="true" access-decision-manager-ref="accessDecisionManager">
<intercept-url pattern="/auth/logout" access="permitAll"/>
<intercept-url pattern="/admin/**" access="ADMINISTRATIVE_ACCESS"/>
<intercept-url pattern="/**" access="XYZ_ACCESS"/>
<form-login
login-page="/auth/login"
authentication-failure-url="/auth/loginFailed"
authentication-success-handler-ref="authenticationSuccessHandler" />
<logout logout-url="/auth/logout" logout-success-url="/auth/login" />
</http>
The authenticationSuccessHandler extends the SavedRequestAwareAuthenticationSuccessHandler ensuring that the user is redirected to the page he originally requested.
However, since /auth/login is marked as security="none", I am unable to successfully redirect the user to the homepage if he accesses the login page after being logged in. I believe this is the right user experience too.
I tried the below too but the Principal object is always null, presumably because of the security="none" attribute again.
#RequestMapping(value = "/auth/login", method = GET)
public String showLoginForm(HttpServletRequest request, Principal principal) {
if(principal != null) {
return "redirect:/";
}
return "login";
}
I've checked the topic more deeply than last time and found that you have to determine if user is authenticated by yourself in controller. Row Winch (Spring Security dev) says here:
Spring Security is not aware of the internals of your application
(i.e. if you want to make your login page flex based upon if the user
is logged in or not). To show your home page when the login page is
requested and the user is logged in use the SecurityContextHolder in
the login page (or its controller) and redirect or forward the user to
the home page.
So solution would be determining if user requesting /auth/login is anonymous or not, something like below.
applicationContext-security.xml:
<http auto-config="true" use-expressions="true"
access-decision-manager-ref="accessDecisionManager">
<intercept-url pattern="/auth/login" access="permitAll" />
<intercept-url pattern="/auth/logout" access="permitAll" />
<intercept-url pattern="/admin/**" access="ADMINISTRATIVE_ACCESS" />
<intercept-url pattern="/**" access="XYZ_ACCESS" />
<form-login login-page="/auth/login"
authentication-failure-url="/auth/loginFailed"
authentication-success-handler-ref="authenticationSuccessHandler" />
<logout logout-url="/auth/logout" logout-success-url="/auth/login" />
</http>
<beans:bean id="defaultTargetUrl" class="java.lang.String">
<beans:constructor-arg value="/content" />
</beans:bean>
<beans:bean id="authenticationTrustResolver"
class="org.springframework.security.authentication.AuthenticationTrustResolverImpl" />
<beans:bean id="authenticationSuccessHandler"
class="com.example.spring.security.MyAuthenticationSuccessHandler">
<beans:property name="defaultTargetUrl" ref="defaultTargetUrl" />
</beans:bean>
Add to applicationContext.xml bean definition:
<bean id="securityContextAccessor"
class="com.example.spring.security.SecurityContextAccessorImpl" />
which is class
public final class SecurityContextAccessorImpl
implements SecurityContextAccessor {
#Autowired
private AuthenticationTrustResolver authenticationTrustResolver;
#Override
public boolean isCurrentAuthenticationAnonymous() {
final Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
return authenticationTrustResolver.isAnonymous(authentication);
}
}
implementing simple interface
public interface SecurityContextAccessor {
boolean isCurrentAuthenticationAnonymous();
}
(SecurityContextHolder accessing code is decoupled from controller, I followed suggestion from this answer, hence SecurityContextAccessor interface.)
And last but not least redirect logic in controller:
#Controller
#RequestMapping("/auth")
public class AuthController {
#Autowired
SecurityContextAccessor securityContextAccessor;
#Autowired
#Qualifier("defaultTargetUrl")
private String defaultTargetUrl;
#RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
if (securityContextAccessor.isCurrentAuthenticationAnonymous()) {
return "login";
} else {
return "redirect:" + defaultTargetUrl;
}
}
}
Defining defaultTargetUrl String bean seems like a hack, but I don't have better way not to hardcode url... (Actually in our project we use <util:constant> with class containing static final String fields.) But it works after all.
You could also restrict your login page to ROLE_ANONYMOUS and set an <access-denied-handler />:
<access-denied-handler ref="accessDeniedHandler" />
<intercept-url pattern="/auth/login" access="ROLE_ANONYMOUS" />
And in your handler check if the user is already authenticated:
#Service
public class AccessDeniedHandler extends AccessDeniedHandlerImpl {
private final String HOME_PAGE = "/index.html";
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && !(auth instanceof AnonymousAuthenticationToken)) {
response.sendRedirect(HOME_PAGE);
}
super.handle(request, response, e);
}
}
Implement a Redirect Interceptor for this purpose:
The Interceptor (implementing HandlerInterceptor interface) check if someone try to access the login page, and if this person is already logged in, then the interceptor sends a redirect to the index page.
public class LoginPageRedirectInterceptor extends HandlerInterceptorAdapter {
private String[] loginPagePrefixes = new String[] { "/login" };
private String redirectUrl = "/index.html";
private UrlPathHelper urlPathHelper = new UrlPathHelper();
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (isInLoginPaths(this.urlPathHelper.getLookupPathForRequest(request))
&& isAuthenticated()) {
response.setContentType("text/plain");
sendRedirect(request, response);
return false;
} else {
return true;
}
}
private boolean isAuthenticated() {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
if (authentication instanceof AnonymousAuthenticationToken) {
return false;
}
return authentication.isAuthenticated();
}
private void sendRedirect(HttpServletRequest request,
HttpServletResponse response) {
String encodedRedirectURL = response.encodeRedirectURL(
request.getContextPath() + this.redirectUrl);
response.setStatus(HttpStatus.SC_TEMPORARY_REDIRECT);
response.setHeader("Location", encodedRedirectURL);
}
private boolean isInLoginPaths(final String requestUrl) {
for (String login : this.loginPagePrefixes) {
if (requestUrl.startsWith(login)) {
return true;
}
}
return false;
}
}
You can keep it simple flow by access-denied-page attribute in http element or as dtrunk said to write handler for access denied as well as. the config would be like
<http access-denied-page="/403" ... >
<intercept-url pattern="/login" access="ROLE_ANONYMOUS" />
<intercept-url pattern="/user/**" access="ROLE_USER" />
<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
<form-login login-page="/login" default-target-url="/home" ... />
...
</http>
in controller for /403
#RequestMapping(value = "/403", method = RequestMethod.GET)
public String accessDenied() { //simple impl
return "redirect:/home";
}
and for /home
#RequestMapping(value = "/home", method = RequestMethod.GET)
public String home(Authentication authentication) {
// map as many home urls with Role
Map<String, String> dashBoardUrls = new HashMap<String, String>();
dashBoardUrls.put("ROLE_USER", "/user/dashboard");
dashBoardUrls.put("ROLE_ADMIN", "/admin/dashboard");
String url = null;
Collection<? extends GrantedAuthority> grants = authentication
.getAuthorities();
// for one role per user
for (GrantedAuthority grantedAuthority : grants) {
url = dashBoardUrls.get(grantedAuthority.getAuthority());
}
if (url == null)
return "/errors/default_access_denied.jsp";
return "redirect:" + url;
}
and when you make request for /admin/dashboard without logged in, it will redirect /login automatically by security
<http pattern="/login" auto-config="true" disable-url-rewriting="true">
<intercept-url pattern="/login" access="ROLE_ANONYMOUS"/>
<access-denied-handler error-page="/index.jsp"/>
</http>
You can try checking
if(SecurityContextHolder.getContext().getAuthentication() == null)
True means the user isn't authenticated, and thus can be sent to the login page. I don't know how robust/reliable this is, but it seems reasonable to try.
I am using Spring and Ldap for user authentication , Now I want to redirect the user to homepage when user is already logged in , I tried few solutions that I read through google but no luck. Here is my configuration code ...
Spring-Security.xml
<security:http auto-config="true" use-expressions="true"
access-denied-page="/denied" access-decision-manager-ref="accessDecisionManager"
disable-url-rewriting="true">
<security:remember-me key="_spring_security_remember_me"
token-validity-seconds="864000" token-repository-ref="tokenRepository" />
<security:intercept-url pattern="/login/login"
access="permitAll" />
<security:intercept-url pattern="/resources/**"
access="permitAll" />
<security:intercept-url pattern="/member/*"
access="permitAll" />
<security:intercept-url pattern="user/admin/admin"
access="hasRole('ROLE_ADMIN')" />
<security:intercept-url pattern="/user/user"
access="hasRole('ROLE_USERS')" />
<security:form-login login-page="/login/login"
authentication-failure-url="/login/login?error=true"
default-target-url="/checking" />
<security:logout invalidate-session="true"
logout-success-url="/login/login" logout-url="/login/logout" />
<security:custom-filter ref="captchaCaptureFilter"
before="FORM_LOGIN_FILTER" />
<!-- <security:custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"
/> -->
<!-- <security:session-management
invalid-session-url="/logout.html">
<security:concurrency-control
max-sessions="1" error-if-maximum-exceeded="true" />
</security:session-management>
<security:session-management
session-authentication-strategy-ref="sas" /> -->
</security:http>
My Login Controller
public class LoginController {
#Autowired
private User user;
#Autowired
SecurityContextAccessor securityContextAccessor;
#RequestMapping(value = "login/login", method = RequestMethod.GET)
public String getLogindata(
#RequestParam(value = "error", required = false) String error,
Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
model.addAttribute("error", error);
if (securityContextAccessor.isCurrentAuthenticationAnonymous() && auth.getPrincipal() == null) {
return "login/login";
} else {
return "redirect:/member/user";
}
}
}
auth.getName() always gives me anonymous user even if I am already logged in....
You should write your target redirection to the home page,
after successfully login in to the default-target-url in spring-security.xml, instead of default-target-url="/checking" .
I am not sure what are you trying to achieve in /login/login Controller ?
If you only want to redirect user, after successfully login to the /member/user , you should write:
<security:form-login login-page="/login"
always-use-default-target='true'
default-target-url="/member/user"
authentication-failure-url="/login/login?error=true"
/>
And login Controller should look like this:
#RequestMapping(value="/login", method = RequestMethod.GET)
public String login(ModelMap model) {
return "login";
}
If your authentication provider is OK and correct , Spring security will redirect you automatically to the /member/user after successfully login.
I have this Spring XML:
<!-- Configure the authentication -->
<security:http auto-config="true" use-expressions="true">
<security:form-login login-page="/login"
authentication-failure-url="/login?error=true"
default-target-url="/index" />
<security:logout invalidate-session="true"
logout-success-url="/login"
logout-url="/logout" />
</security:http>
<security:authentication-manager>
<security:authentication-provider user-service-ref="testUDS" />
</security:authentication-manager>
<bean id="testUDS" class="net.safecycle.services.security.TestUserDetailService" />
My UserDetailsService implementation looks like this:
public class TestUserDetailService implements UserDetailsService {
public UserDetails loadUserByUsername
(
String username
) throws UsernameNotFoundException {
System.out.println ("loadUserByUsername (" + username + ")");
Collection<GrantedAuthority> authorities;
authorities = new LinkedList<GrantedAuthority> ();
authorities.add (new GrantedAuthorityImpl ("Admin"));
UserDetails ud = new User (username,
"ca",
true,
true,
true,
true,
authorities);
return ud;
}
}
When I log in with any username and the password 'ca', I should see the print statement at the top of my loadUserByUsername, but I do not. What is most perplexing is that I have used this code in another project with no problem. Is there anything I am missing, a copy mistake I'm hoping?
Here is my code from my xml file. Only missing thing is the allias.. try to add alias="authenticationManager" maybe it will help.
<beans:bean id="CustomUserDetailsService"
class="com.dennis.ehospital.hibernate.security.CustomUserDetailsService" />
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="CustomUserDetailsService" />
</authentication-manager>
Try to specify which resources are protected
<security:intercept-url pattern="/**" access="isAuthenticated()"/>