spring-attic / spring-security-oauth

Support for adding OAuth1(a) and OAuth2 features (consumer and provider) for Spring web applications.
http://github.com/spring-projects/spring-security-oauth
Apache License 2.0
4.7k stars 4.04k forks source link

[SECOAUTH-432] Support automatically destroying session on OAuth2 authorization server as soon as user redirected to client app #140

Open dsyer opened 10 years ago

dsyer commented 10 years ago

Priority: Minor Original Assignee: Dave Syer Reporter: Harleen Sahni Created At: Wed, 18 Dec 2013 02:41:26 +0000 Last Updated on Jira: Mon, 30 Dec 2013 14:52:48 +0000

It'd be a nice feature to only maintain the session on the authorization server as long the user is granting approval to an oauth2 client, and no longer. This is useful for when your authorization server's sole role is for authorizing oauth2 clients, and nothing else. In this case, a longer lived session has no real value.

Also, if an oauth2 client wants to switch users for oauth2 access, it can't do so easily today. As long as the user's session is alive on the authorization server what will happen is as the client sends the user to /oauth/authorize, the client will be using the user's old session, and the client will be auto approved since it has a current access token for that user.

This feature should be configurable.

Currently I've accomplished this with use of aspects:

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;

@Service
@Aspect
public class SessionInvalidationOauth2GrantAspect {

    private static final String FORWARD_OAUTH_CONFIRM_ACCESS = "forward:/oauth/confirm_access";
    private static final Logger logger = Logger.getLogger(SessionInvalidationOauth2GrantAspect.class);

    @AfterReturning(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && @annotation(org.springframework.web.bind.annotation.RequestMapping)", returning = "result")
    public void authorizationAdvice(JoinPoint joinpoint, ModelAndView result) throws Throwable {

        // If we're not going to the confirm_access page, it means approval has been skipped due to existing access
        // token or something else and they'll be being sent back to app. Time to end session.
        if (!FORWARD_OAUTH_CONFIRM_ACCESS.equals(result.getViewName())) {
            invalidateSession();
        }
    }

    @AfterReturning(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && @annotation(org.springframework.web.bind.annotation.RequestMapping)", returning = "result")
    public void authorizationAdvice(JoinPoint joinpoint, View result) throws Throwable {
        // Anything returning a view and not a ModelView is going to be redirecting outside of the app (I think). 
        // This happens after the authorize approve / deny page with the POST to /oauth/authorize. This is the time
        // to kill the session since they'll be being sent back to the requesting app.
        invalidateSession();
    }

    @AfterThrowing(value = "within(org.springframework.security.oauth2.provider.endpoint..*) &&  @annotation(org.springframework.web.bind.annotation.RequestMapping)", throwing = "error")
    public void authorizationErrorAdvice(JoinPoint joinpoint) throws Throwable {
        invalidateSession();
    }

    private void invalidateSession() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();
        HttpSession session = request.getSession(false);
        if (session != null) {
            logger.warn(String.format("As part of OAuth application grant processing, invalidating session for request %s", request.getRequestURI()));

            session.invalidate();
            SecurityContextHolder.clearContext();
        }
    }

}

Comments:

david_syer on Mon, 30 Dec 2013 10:57:23 +0000

I'm not really sure what the use case is for a "client switching users". Maybe you could be a bit more specific about that?

In any case I would prefer not to have explicit HttpSession-specific code in Spring OAuth if we can avoid it. A short session timeout in the server is definitely a good idea, but that's better left to the server setup.

If the client is a webapp (I think that's the only scenario that makes sense in your description), then the user's session will be tied to their browser, and they can control sessions by logging out and logging back in again (as long as the server provides that feature, which isn't really anything to do with OAuth)?

harleen on Mon, 30 Dec 2013 14:52:48 +0000

A short lived session on the authorization server would help accomplish about the same thing, but not as effectively.

When talking about the client that's being authorized for oauth2 access, it's true that the session on that client is tied to the browser. We do provide a logout out of the client session. However, in our case, the client's sole means of authenticating the user is via being redirected to login to the oauth2 authorization server, and be redirected back the client web app. The client has no user store of its own and no login of its own. So when the the client web app logs out of it's session, the user is asked to log back in by being redirected to the authorization server to login again. If the authorization server's session is still active, the user would be automatically logged back in. This isn't desirable for us, and that's why we're killing the authorization server's session immediately after authorization.

amoraes commented 8 years ago

Great! This is exactly what I was looking for.

balarayen commented 8 years ago

Awesome Harleen. This may not be an oath protocol, but this should be a basic/necessary feature in spring-oauth to make oauth protocol work correctly.

depressiveRobot commented 7 years ago

User switching or parallel logins (in the same browser) are a typical use case in businesses, so please provide an option for that.

oharaandrew314 commented 7 years ago

This worked wonderfully for me. When a user logs out of my ui service, they would just get logged back in automatically since their auth server session never expired. Now, when the user logs out of the ui server, they are brought to the auth server login page as expected.

alvint commented 7 years ago

Outstanding--exactly what I needed! @harleen, when my people come to enslave your planet, you will not be harmed.

dsyer commented 7 years ago

Note that the aspect above will work, but it's neater to do the same thing with a HandlerInterceptor (may not have been possible at the time the feature request was made). E.g. this should work in a AuthorizationServerConfigurerAdapter:

        @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();
                            }
                        }
                    }
                }
            });
        }
ghost commented 7 years ago

This interceptor jest fine when form login shows as result of standard process - ie. GET /oauth/authenticate which redirects to /login.html at which you log in. But when you directly enter at /login.html (manually write URL or click backward in browser) and log in then this interceptor is not even called and session stayed alive.

dsyer commented 7 years ago

@tomasz-werminski that's expected though, I should think, because you aren't going to go through the OAuth2 protocol in that case. You just authenticated with the auth server, and now you could, for instance do some admin actions in there or something. It's nothing to do with OAuth2 at that point. Or am I missing something?

ghost commented 7 years ago

@dsyer Yes, you are right, /login.html page on her own is not a part of oauth2 flow. But from user's point of view it may be a bit confiusing. User starts authentication process - ie. /oauth/authenticate call happens, then redirect to /login.html, then interceptor kicks in and finally redirect to client-app happens. But if at this moment user clicks browser backward button he or she is able to see login page again. So he or she can log-in too and leave active session. In this case, next call for /oauth/authenticate won't trigger login page.

This is, of course, not a problem with this interceptor because its role is to invalidate session on oauth2 auhtentication process. But if your authorization server's only role is to authorize oauth clients above flow could be a problem (luckily it's no frequent).

I was faced with such a flow so I've decided to describe it here because it's related to this topic, maybe it'll be helpful for somebody. As a workaround in login controller I check if /login.html call is "a part of normal flow", ie. whether in session is any saved request which triggered login proces. If not I make redirect to some configured URL.

ReginaldoSantos commented 6 years ago

For newcomers, use the HandlerInterceptor approach instead of the Aspect's, because the last one will invalidate the session "always", even when user mistype his/her credentials. After that, a successful login will not be able to trigger the original redirection.

82dilip commented 6 years ago

How this feature can be implemented with Single Sign on Kerberos/Spnego authentication. I am using pretty older version of zuul 1.1.0 . A example or pointer would be great.

greenkode commented 6 years ago

Note that the aspect above will work, but it's neater to do the same thing with a HandlerInterceptor (may not have been possible at the time the feature request was made). E.g. this should work in a AuthorizationServerConfigurerAdapter:

      @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 might be a silly question . But how do I attach this handler interceptor via xml config? my organization uses <oauth2:authorization-server... and it's either that or I migrate everything to annotations. Currently I just placed a filter at the top of the chain that intercepts the auth code response and invalidates the session. However it seems a bit hacky to me.

PatrickHuetter commented 5 years ago

So what's the correct and better approach to fix this behaviour? The HandlerInterceptor or the SessionInvalidationOauth2GrantAspect?