Spring Security 4 intercept anonymous user AccessDeniedException - spring-security

My website allows unauthenticated (anonymous) users to perform a search of content which requires authentication to view. Currently, when they click on a link to view the details of one of the content items on the search results page Spring Security correctly identifies the user as unauthenticated and displays the login page. However, I would like to intervene and instead display a page to encourage the anonymous user to sign up for the website. I have traced what is happening in the filter chain but it's not clear to me whether I should extend an existing filter or handler or create a custom filter or handler. If it's the later I'm not sure where it should go.
When I run this through debug I can see the following happening:
ExceptionTranslationFilter.doFilter executes FilterSecurityInterceptor which determines that the detail page requires authentication (returns a -1 vote and throws an AccessDeniedException)
ExceptionTranslationFilter catches the exception, determines the user is anonymous and calls the authenticationEntryPoint, in this case LoginUrlAuthenticationEntryPoint
LoginUrlAuthenticationEntryPoint invokes the DefaultRedirectStrategy which redirects to the login page
So, basically I need to override the redirection to the login page for this one use case. My best guess is to create a custom filter that checks for the combination of an anonymous user accessing this specific detail page and forces a redirect to the join up page, inserting the filter in the chain after ExceptionTranslationFilter. Or is this total overkill for handling a single page redirect and there's an easier way to accomplish this?

For anyone interested, here's the code for the custom auth entry point, borrowing from LoginUrlAuthenticationEntryPoint and ExceptionTranslationFilter.
public class CustomAuthLoginEntryPoint extends LoginUrlAuthenticationEntryPoint {
private final Logger logger = LoggerFactory.getLogger(getClass());
private PortResolver portResolver = new PortResolverImpl();
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private RequestCache requestCache = new HttpSessionRequestCache();
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private String joinPageUrl;
public CustomAuthLoginEntryPoint(String loginFormUrl) {
super(loginFormUrl);
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
logger.debug("commence");
String redirectUrl = null;
if (!StringUtils.isBlank(joinPageUrl)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || trustResolver.isAnonymous(auth)) {
SavedRequest savedRequest = requestCache.getRequest(request, response);
redirectUrl = savedRequest.getRedirectUrl();
if (redirectUrl.indexOf("viewDetail") > 0) {
String joinPageUrl = buildRedirectUrlToJoinPage(request);
logger.debug("Redirecting to '" + joinPageUrl + "'");
redirectStrategy.sendRedirect(request, response, joinPageUrl);
return;
}
}
}
super.commence(request, response, authException);
}
protected String buildRedirectUrlToJoinPage(HttpServletRequest request) {
int serverPort = portResolver.getServerPort(request);
String scheme = request.getScheme();
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme(scheme);
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(serverPort);
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(joinPageUrl);
return urlBuilder.getUrl();
}
public void setJoinPage(String joinPageUrl) {
this.joinPageUrl = joinPageUrl;
}
}
I added this to my WebSecurityConfigurerAdapter:
#Bean
public CustomAuthLoginEntryPoint customAuthLoginEntryPoint() {
CustomAuthLoginEntryPoint entryPoint = new CustomAuthLoginEntryPoint("/user/login");
entryPoint.setJoinPage("/user/join");
return entryPoint;
}
and the http configure:
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.authenticationEntryPoint(customAuthLoginEntryPoint())

Related

Spring Security OAuth2 AngularJS | Logout Flow

Referring to the logout flow in oauth2 spring-guides project, once the the user has authenticated using user/password for the first time, the credentials are not asked next time after logout.
How can I ensure that username/password are asked every time after a logout.
This is what I am trying to implement:-
OAuth2 server issuing JWT token using "authorization_code" grant type
with auto approval. This has html/angularjs form to collect
username/password.
UI/Webfront - Uses #EnableSSO. ALL its endpoints are authenticated
i.e it does not have any unauthorized landing page/ui/link that user
clicks to go to /uaa server. So hitting http://localhost:8080
instantly redirects you to http://localhost:9999/uaa and presents
custom form to collect username/password.
Resource server - Uses #EnableResourceServer. Plain & simple REST api.
With the above approach I am not able to workout the logout flow. HTTP POST /logout to the UI application clears the session/auth in UI application but the users gets logged in again automatically ( as I have opted for auto approval for all scopes) without being asked for username password again.
Looking at logs and networks calls, it looks like that all the "oauth dance" happens all over again successfully without user being asked for username/password again and seems like the auth server remembers last auth token issued for a client ( using org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices? ).
How can I tell auth server to ask for username/password every time it is requested for code/token - stateless.
Or what is the best way to implement logout in my given scenario.
( To recreate somewhat near to my requirements, remove permitAll() part from the UiApplication and configure autoApproval in auth server of the mentioned boot project.)
github issue
I also faced the error as you described and I saw a solution from question
Spring Boot OAuth2 Single Sign Off. I don't mean this is the only and global truth solution.
But in the scenario,
authentication server has login form and you'd authenticated from it
browser still maintain the session with authentication server
after you have finished logout process (revoke tokens,remove cookies...)
and try to re-login again
authentication server do not send login form and automatically sign in
You need to remove authentication informations from authentication server's session as this answer described.
Below snippets are how did I configure for solution
Client (UI Application in your case) application's WebSecurityConfig
...
#Value("${auth-server}/ssoLogout")
private String logoutUrl;
#Autowired
private CustomLogoutHandler logoutHandler;
...
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl(logoutUrl)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.addLogoutHandler(logoutHandler)
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
// #formatter:on
}
Custom logout handler for client application
#Component
public class CustomLogoutHandler implements LogoutHandler {
private static Logger logger = Logger.getLogger(CustomLogoutHandler.class);
#Value("${auth-server}/invalidateTokens")
private String logoutUrl;
#Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
logger.debug("Excution CustomLogoutHandler for " + authentication.getName());
Object details = authentication.getDetails();
if (details.getClass().isAssignableFrom(OAuth2AuthenticationDetails.class)) {
String accessToken = ((OAuth2AuthenticationDetails) details).getTokenValue();
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("access_token", accessToken);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "bearer " + accessToken);
HttpEntity<Object> entity = new HttpEntity<>(params, headers);
HttpMessageConverter<?> formHttpMessageConverter = new FormHttpMessageConverter();
HttpMessageConverter<?> stringHttpMessageConverternew = new StringHttpMessageConverter();
restTemplate.setMessageConverters(Arrays.asList(new HttpMessageConverter[] { formHttpMessageConverter, stringHttpMessageConverternew }));
try {
ResponseEntity<String> serverResponse = restTemplate.exchange(logoutUrl, HttpMethod.POST, entity, String.class);
logger.debug("Server Response : ==> " + serverResponse);
} catch (HttpClientErrorException e) {
logger.error("HttpClientErrorException invalidating token with SSO authorization server. response.status code: " + e.getStatusCode() + ", server URL: " + logoutUrl);
}
}
authentication.setAuthenticated(false);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
new SecurityContextLogoutHandler().logout(request, response, auth);
}
}
I used JDBC tokenStore, so I need to revoke tokens.At the authentication server side, I added a controller to handle logout processes
#Controller
public class AuthenticationController {
private static Logger logger = Logger.getLogger(AuthenticationController.class);
#Resource(name = "tokenStore")
private TokenStore tokenStore;
#Resource(name = "approvalStore")
private ApprovalStore approvalStore;
#RequestMapping(value = "/invalidateTokens", method = RequestMethod.POST)
public #ResponseBody Map<String, String> revokeAccessToken(HttpServletRequest request, HttpServletResponse response, #RequestParam(name = "access_token") String accessToken, Authentication authentication) {
if (authentication instanceof OAuth2Authentication) {
logger.info("Revoking Approvals ==> " + accessToken);
OAuth2Authentication auth = (OAuth2Authentication) authentication;
String clientId = auth.getOAuth2Request().getClientId();
Authentication user = auth.getUserAuthentication();
if (user != null) {
Collection<Approval> approvals = new ArrayList<Approval>();
for (String scope : auth.getOAuth2Request().getScope()) {
approvals.add(new Approval(user.getName(), clientId, scope, new Date(), ApprovalStatus.APPROVED));
}
approvalStore.revokeApprovals(approvals);
}
}
logger.info("Invalidating access token :- " + accessToken);
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
if (oAuth2AccessToken != null) {
if (tokenStore instanceof JdbcTokenStore) {
logger.info("Invalidating Refresh Token :- " + oAuth2AccessToken.getRefreshToken().getValue());
((JdbcTokenStore) tokenStore).removeRefreshToken(oAuth2AccessToken.getRefreshToken());
tokenStore.removeAccessToken(oAuth2AccessToken);
}
}
Map<String, String> ret = new HashMap<>();
ret.put("removed_access_token", accessToken);
return ret;
}
#GetMapping("/ssoLogout")
public void exit(HttpServletRequest request, HttpServletResponse response) throws IOException {
new SecurityContextLogoutHandler().logout(request, null, null);
// my authorization server's login form can save with remember-me cookie
Cookie cookie = new Cookie("my_rememberme_cookie", null);
cookie.setMaxAge(0);
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
response.addCookie(cookie);
response.sendRedirect(request.getHeader("referer"));
}
}
At authorization server's SecurityConfig, you may need to allow this url as
http
.requestMatchers()
.antMatchers(
"/login"
,"/ssoLogout"
,"/oauth/authorize"
,"/oauth/confirm_access");
I hope this may help a little for you.
As you are using JWT tokens, you can not really revoke them.
As a workaround, you can have a logout rest endpoint that would store the timestamp and userid for logout call.
Later, you can compare the logout time with JWT token issue time, and decide wether to allow an api call or not.
I have realized that redirecting to a controller when you logout from your client app and then programmatically logout on your authserver does the trick. This is my configuration on the client app:
#Configuration
#EnableOAuth2Sso
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Value("${auth-server}/exit")
private String logoutUrl;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutSuccessUrl(logoutUrl)
.and().authorizeRequests().anyRequest().authenticated();
}
}
and this is my configuration on my authserver (is just a controller handling the /exit endpoint):
#Controller
public class LogoutController {
public LogoutController() {
}
#RequestMapping({"/exit"})
public void exit(HttpServletRequest request, HttpServletResponse response) {
(new SecurityContextLogoutHandler()).logout(request, null, null);
try {
response.sendRedirect(request.getHeader("referer"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
Here is a sample app that shows the full implementation using JWT. Check it out and let us know if it helps you.

When logout from Spring Security [Boot] and then again if I try to login it's logging in without password. How Can I prevent this

I am using Spring security in my application and configuration is mentioned below.
When User is logged out from system and click in again login then I want to display login page.
#Component
#EnableOAuth2Sso
public static class LoginConfigurer extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
RequestMatcher csrfRequestMatcher = new RequestMatcher() {
// Enabled CSFR protection on the following urls
private AntPathRequestMatcher[] requestMatchers = { new AntPathRequestMatcher("/dashboard/logout") };
#Override
public boolean matches(HttpServletRequest request) {
// If the request match one url the CSFR protection will be
// enabled
for (AntPathRequestMatcher rm : requestMatchers) {
if (rm.matches(request)) {
return true;
}
}
return false;
} // method matches
};
http.csrf().requireCsrfProtectionMatcher(csrfRequestMatcher).csrfTokenRepository(csrfTokenRepository())
.and().antMatcher("/dashboard/**").authorizeRequests().anyRequest()
.hasAnyRole("AUTHENTICATED_USER", "ANONYMOUS").and().sessionManagement().sessionFixation()
.migrateSession().and()
// .csrfTokenRepository(csrfTokenRepository()).and()
// .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class)
.logout().invalidateHttpSession(true).logoutUrl("/dashboard/logout").deleteCookies(new String[]{"XSRF-TOKEN","JSESSIONID","remember-me"})
.logoutSuccessUrl("/").permitAll().and().rememberMe().and()
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);
}
The case you mentioned mostly happens if you use a cookie for authentication as a token and the deleteCookie method is not deleting your authentication cookies correctly.
The best way to identify this is follow :
1.Clear all cookies and login and then observe the cookie name and value and path in inspect element of web browser.
2.Logout and then observe the inspect element request and response with cookies.
3.Observe the path of the cookie carefully.
It is important to observe the path of the cookie is because of the path of the cookie at its creation time is different than its deletion time,it will not get deleted.It has to be exactly the same.
When you call .deleteCookies(...),then internally it calls the CookieClearingLogoutHandler(spring boot security internal class) mentioned as below :
public final class CookieClearingLogoutHandler implements LogoutHandler {
private final List<String> cookiesToClear;
public CookieClearingLogoutHandler(String... cookiesToClear) {
Assert.notNull(cookiesToClear, "List of cookies cannot be null");
this.cookiesToClear = Arrays.asList(cookiesToClear);
}
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
for (String cookieName : cookiesToClear) {
Cookie cookie = new Cookie(cookieName, null);
String cookiePath = request.getContextPath();
if(!StringUtils.hasLength(cookiePath)) {
cookiePath = "/";
}
cookie.setPath(cookiePath);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
}
Observe the cookie setPath, you would need to set the cookie path for the cookie you are using for authentication in the same way mentioned in above code.
Hope this helps.

Spring security multiple logout urls?

I am using Spring security java config and I wanted to know a way to implemented log-out for multiple urls. i.e.
logout().logoutRequestMatcher(new
AntPathRequestMatcher("/invalidate")).logoutUrl("/logout");
In this code the normal logout url "/logout" works fine and its a post request but i also want the user to logout for the url "/invalidate" which doesn't seem to work.
According to Spring Security tutorial, it seems that the next is more elegant approach:
In the security form-login tag just add something like this:
<security:logout logout-url="/logout" success-handler-ref="logoutHandler"/>
Every time that you'll hit /logout URL the logoutHandler will be invoked, and on it, you can decide how to behave after a successful logout.
From Spring docs:
All you need to do is to create a new class that implements the interface marked in the image and implement its single method.
On that method decide how to act after a successful logout. for example:
#Component("logoutHandler")
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
#Override
public void onLogoutSuccess(HttpServletRequest request,HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if(request.getParameter("msgShow") != null && request.getParameter("msgShow").equals("false")){
redirectResponse(request, response, "http://" + request.getServerName() + ":" + request.getServerPort() + "/my_web_app/home?logout=false");
}
else{
redirectResponse(request, response,"http://" + request.getServerName() + ":" + request.getServerPort() + "/my_web_app/home?logout=true");
}
}
private void redirectResponse(HttpServletRequest request, HttpServletResponse response, String destination) {
response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
response.setHeader("Location", destination);
}
}
Now don't forget to add a #Component annotation to the above logout handler + on security configuration file add the next 2 statements:
<context:annotation-config />
<context:component-scan base-package="package.to.logout.handler" />
This might not be the most elegant way, but you can just specify a #Controller that is mapped to all the URLs you want for logout, e. g.
#Controller
public class LogoutController {
final String logoutRedirectUrl = "redirect:http://yourredirect.xy";
#RequestMapping("/logout")
public String logout1(HttpServletRequest request) throws ServletException {
request.logout();
return logoutRedirectUrl;
}
#RequestMapping("/second/logout/")
public String logout2(HttpServletRequest request) throws ServletException {
request.logout();
return logoutRedirectUrl;
}
}
Just a quick tip, the actual matching should be improved.
List logoutUrls = Arrays.asList(
"/rest/logout1",
"/rest/logout2"
);
RequestMatcher rm = new RequestMatcher() {
#Override
public boolean matches(HttpServletRequest request) {
String uriStr = request.getRequestURI().toString();
return logoutUrls.stream()
.filter(lu -> uriStr.contains(lu))
.findFirst()
.isPresent();
}
};
and then register the request matcher:
http.logout(logout -> logout.logoutRequestMatcher(rm));

Grails - Acegi, how to redirect user to a different domain during authentication?

I have inherited a Grails app, which uses the Acegi 0.5.3 plugin.
The application can be accessed via two completely different URLs e.g., app.domainone.com and app.domaintwo.com. The domain names map to two different user communities. Now I have been tasked with restricting user access from only the domain that they are related to. At the moment the users can visit any of the domains and login to the application.
I have some clue about how Acegi works but, can't say I understand all of it yet. So wanted to ask how I would be able to achieve this.
In an ideal scenario, when the user tries to login, I would like to redirect (if required) to their 'relevant' domain and automagically sign them in with their given credentials. However, as an interim solution even a plain redirect to the relevant login page would suffice.
Here goes my CustomAuthenticationProcessingFilter. There is probably a better solution out there but this helped me with the little knowledge that I have of Grails and Spring Security.
class CustomAuthenticationProcessingFilter extends GrailsAuthenticationProcessingFilter implements
InitializingBean {
//def authenticationManager
#Override
public int getOrder() {
return FilterChainOrder.AUTHENTICATION_PROCESSING_FILTER
}
#Override
void doFilterHttp(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) throws IOException,
ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
def loginUrl = "${request.getRequestURL().toString() - request.getRequestURI().toString()}${request.contextPath}"
def username = request.getParameter("j_username")
def password = request.getParameter("j_password")
if ( loginUrl && username && password) {
def user = User.findByEmailOrCompanyEmail(username,username)
if(user) {
def query = """select c from Community c, UserCommunity uc
where c.id = uc.comm.id
and uc.user.id = :userId"""
def comm = Community.executeQuery(query,[userId:user.id])
comm = comm?(comm?.get(0)):null
if(loginUrl!=comm?.url) {
println "Trying to login using the wrong URL"
response.sendRedirect(comm.url+'/login/auth')
return
}
}
}
}
//Resume the normal flow
super.doFilterHttp(request, response, chain)
}
#Override
public void afterPropertiesSet() throws Exception {
// TODO Auto-generated method stub
}
}

Extending AjaxAwareAuthenticationSuccessHandler, Remembering destination URL

Grails 1.3.7
Spring-Security-Core 1.1.2
I've implemented a custom class that extends AjaxAwareAuthenticationSuccessHandler so that specific roles can be taken to specific URL's after logging in which works great. However, if the session expires I need to be able to take the user to the requested URL when the session expired, overriding the Role based URL.
Here is a simplified version of my code
class MyAuthSuccessHandler extends AjaxAwareAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response,
final Authentication authentication) throws ServletException, IOException {
def goAdmin = false
authentication.authorities.each { ga ->
if (ga.authority.equals('ROLE_ADMIN')) {
goAdmin = true
}
}
if (goAdmin) {
response.sendRedirect(request.contextPath + '/admin/index')
}else{
super.onAuthenticationSuccess(request, response, authentication)
}
}
}
I tried adding a call to determineTargetUrl(request, response) but it always returns '/' even though I've requested a resource like /admin/foo which is protected.
Thanks.
Requesting
super.determineTargetUrl(request, response);
should work, if you use SavedRequestAwareAuthenticationSuccessHandler as super class. I am not sure if it is possible for you to switch to this class in your scenario. Maybe this can help, but I guess you are fully aware of it: http://omarello.com/2011/09/grails-custom-target-urls-after-login/

Resources