Skip to content

Latest commit

 

History

History
149 lines (110 loc) · 12.6 KB

File metadata and controls

149 lines (110 loc) · 12.6 KB

Logout from an OAuth2 Client Application

In this section we continue our discussion of how to use Spring Security with Angular in a "single page application". Here we show how to take the OAuth2 samples and add a different logout experience. Many people who implement OAuth2 single sign on find that they have a puzzle to solve of how to logout "cleanly"? The reason it’s a puzzle is that there isn’t a single correct way to do it, and the solution you choose will be determined by the user experience you are looking for, and also the amount of complexity you are willing to take on. The reasons for the complexity stem from the fact that there are potentially multiple browser sessions in the system, all with different backend servers, so when a user logs out from one of them, what should happen to the others? This is the ninth section of a tutorial, and you can catch up on the basic building blocks of the application or build it from scratch by reading the first section, or you can just go straight to the source code in Github.

Logout Patterns

The user experience with logout of the oauth2 sample in this tutorial is that you logout of the UI app, but not from the authserver, so when you log back into the UI app the autheserver does not challenge again for credentials. This is completely expected, normal, and desirable when the autheserver is external - Google and other external authserver providers neither want nor allow you to logout from their servers from an untrusted application - but it isn’t the best user experience if the authserver is really part of the same system as the UI.

There are, broadly speaking, three patterns for logout from a UI app that is authenticated as an OAuth2 client:

  1. External Authserver (EA, the original sample). The user perceives the authserver as a 3rd party (e.g. using Facebook or Google to authenticate). You don’t want to log out of the authserver when the app session ends. You do want approval for all grants. The oauth2 (and oauth2-vanilla) sample from this tutorial implement this pattern.

  2. Gateway and Internal Authserver (GIA). You only need to log out of 2 apps, and they are part of the same system, as perceived by the user. Usually you want to autoapprove all grants.

  3. Single Logout (SL). One authserver and multiple UI apps all with their own authentication, and when the user logs out of one, you want them all to follow suit. Likely to fail with a naive implementation because of network partitions and server failures - you basically need globally consistent storage.

Sometimes, even if you have an external authserver, you want to control the authentication and add an internal layer of access control (e.g. scopes or roles that the authserver doesn’t support). Then it’s a good idea to use the EA for authentication, but have an internal authserver that can add the additional details you need to the tokens. The auth-server sample from this other OAuth2 Tutorial shows you how to do that in a very simple way. You can then apply the GIA or SL patterns to the system that includes the internal authserver.

Here are some options if you don’t want EA:

  • Log out from authserver as well as UI app in browser client. Simple approach and works with some careful CRSF and CORS configuration. No SL.

  • Logout from authserver as soon as a token is available. Hard to implement in the UI, where the token is acquired, because you don’t have the session cookie for the authserver there. There is a feature request in Spring OAuth which shows an interesting approach: invalidate the session in the authserver as soon as an auth code is generated. The Github issue contains an aspect that implements the session invalidation, but it’s easier to do as a HandlerInterceptor. No SL.

  • Proxy authserver through the same gateway as UI and hope that one cookie is enough to manage the state for the whole system. Doesn’t work because unless there is a shared session, which defeats the object to some extent (otherwise there is no session storage for the authserver). SL only if the session is shared between all apps.

  • Cookie relay in gateway. You are using the gateway as the source of truth for authentication, and the authserver has all the state it needs because the gateway manages the cookie instead of the browser. The browser never has a cookie from more than one server. No SL.

  • Use the token as global authentication and invalidate it when user logs out of the UI app. Downside: requires tokens to be invalidated by client apps, which isn’t really what they were designed to do. SL possible, but usual constraints apply.

  • Create and manage a global session token (in addition to the user token) in the authserver. This is the approach taken by OpenId Connect, and it does provide some options for SL, at the cost of some extra machinery. None of the options is immune from the usual distributed system limitations: if networks and application nodes are not stable there are no guarantees that a logout signal is shared among all participants when needed. All of the logout specs are still in draft form, and here are some links to the specs: Session Management, Front Channel Logout, and Back Channel Logout.

Note that where SL is hard or impossible, it might be better to put all the UIs behind a single gateway anyway. Then you can use GIA, which is easier, to control logout from your whole estate.

The easiest two options, which apply nicely in the GIA pattern can be implemented in the tutorial sample as follows (take the oauth2 sample and work from there).

Logout of Both Servers from Browser

It’s quite easy to add a couple of lines of code to the browser client that logout from the authserver as soon as the UI app is logged out. E.g.

logout() {
    this.http.post('logout', {}).finally(() => {
        self.authenticated = false;
        this.http.post('http://localhost:9999/uaa/logout', {}, {withCredentials:true})
            .subscribe(() => {
                console.log('Logged out');
        });
    }).subscribe();
};

In this sample we hardcoded the authserver logout endpoint URL into the JavaScript, but it would be easy to externalize that if you needed to. It has to be a POST directly to the authserver because we want the session cookie to go along too. The XHR request will only go out from the browser with a cookie attached if we specifically ask for withCredentials:true.

Conversely, on the server we need some CORS configuration because the request is coming from a different domain. E.g. in the WebSecurityConfigurerAdapter

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
	.requestMatchers().antMatchers("/login", "/logout", "/oauth/authorize", "/oauth/confirm_access")
  .and()
    .cors().configurationSource(configurationSource())
    ...
}

private CorsConfigurationSource configurationSource() {
  UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  CorsConfiguration config = new CorsConfiguration();
  config.addAllowedOrigin("*");
  config.setAllowCredentials(true);
  config.addAllowedHeader("X-Requested-With");
  config.addAllowedHeader("Content-Type");
  config.addAllowedMethod(HttpMethod.POST);
  source.registerCorsConfiguration("/logout", config);
  return source;
}

The "/logout" endpoint has been given some special treatment. It is allowed to be called from any origin, and explicitly allows credentials (e.g. cookies) to be sent. The allowed headers are just the ones that Angular sends in teh sample app.

In addition to the CORS configuration we also need to disable CSRF for the logout endpoint, because Angular will not send the X-XSRF-TOKEN header in a cross-domain request. The authserver didn’t require any CSRF configuration before now, but it’s easy to add an ignore for the logout endpoint:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .csrf()
      .ignoringAntMatchers("/logout/**")
    ...
}
Warning
Dropping CSRF protection is not really advisable, but you might be prepared to tolerate it for this restricted use case.

With those two simple changes, one in the UI app client, and one in the authserver, you will find that once you logout of the UI app, when you log back in, you will always be prompted for a password.

Another useful change is to set the OAuth2 client to autoapprove, so that the user doesn’t have to approve the token grant. This is common in a internal authserver, where the user doesn’t perceive it as a separate system. In the AuthorizationServerConfigurerAdapter you just need a flag when the client is initialized:

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  clients.inMemory().withClient("acme")
    ...
  .autoApprove(true);
}

Invalidate Session in Authserver

If you don’t like to give up the CSRF protection on the logout endpoint, you can try the other easy approach, which is to invalidate the user session in the authserver as soon as a token is granted (actually as soon as an auth code is generated). This is also super easy to implement: starting from the oauth2 sample, just add a HandlerInterceptor to the OAuth2 endpoints.

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
    throws Exception {
  ...
  endpoints.addInterceptor(new HandlerInterceptorAdapter() {
    @Override
    public void postHandle(HttpServletRequest request,
        HttpServletResponse response, Object handler,
        ModelAndView modelAndView) throws Exception {
      if (modelAndView != null
          && modelAndView.getView() instanceof RedirectView) {
        RedirectView redirect = (RedirectView) modelAndView.getView();
        String url = redirect.getUrl();
        if (url.contains("code=") || url.contains("error=")) {
          HttpSession session = request.getSession(false);
          if (session != null) {
            session.invalidate();
          }
        }
      }
    }
  });
}

This interceptor looks for a RedirectView, which is a signal that the user is being redirected back to the client app, and checks if the location contains an auth code or an error. You could add "token=" if you were using implicit grants as well.

With this simple change, as soon as you authenticate, the session in the authserver is already dead, so there’s no need to try and manage it from the client. When you log out of the UI app, and then log back in, the authserver doesn’t recognize you and prompts for credentials. This pattern is the one implemented by the oauth2-logout sample in the source code for this tutorial. The downside of this approach is that you don’t really have true single sign on any more - any other apps that are part of your system will find that the authserver session is dead and they have to prompt for authentication again - it isn’t a great user experience if there are multiple apps.

Conclusion

In this section we have seen how to implement a couple of different patterns for logout from an OAuth2 client application (taking as a starting point the application from section five of the tutorial), and some options for other patterns were discussed. These options are not exhaustive, but should give you a good idea of the trade offs involved, and some tools for thinking about the best solution for your use case. There were only couple of lines of JavaScript in this section, and that wasn’t really specific to Angular (it adds a flag to XHR requests), so all the lessons and patterns are applicable beyond the narrow scope of the sample apps in this guide. A recurring theme is that all approaches to single logout (SL) where there are multiple UI apps and a single authserver tend to be flawed in some way: the best you can do is choose the approach that makes your users the least uncomfortable. If you have an internal authserver and a system that is composed of many components, then possibly the only architecture that feels to the user like a single system is a gateway for all user interactions.