5

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

Cataclysm
  • 7,592
  • 21
  • 74
  • 123
Kumar Sambhav
  • 7,503
  • 15
  • 63
  • 86
  • 1
    One quick fix I could figure out was to set server.session.timeout to a lower value (30s may be) on the auth server and move the "/me" endpoint to a resource server. – Kumar Sambhav Jan 27 '17 at 02:40
  • Has any other ways to figure it out instead reducing session time-out ? – Cataclysm Jul 18 '17 at 04:21

3 Answers3

3

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.

Cataclysm
  • 7,592
  • 21
  • 74
  • 123
  • 1
    The thing is when you use JWT there is no session on the authserver. It's stateless. In other words, when the authserver issues a JWT token, the only way to invalidate it is with the expiration time you set on them. When your client app has a valid token, it will continue using it until the expiration time comes. Then the resource server will see that the token is not valid anymore and will send the client to get a new one on the authserver. So the only thing I can figure about this use case is to find a way to forget the token in the client app to provoke getting another from the authserver. – Juan Carlos Mendoza Jul 21 '17 at 18:07
  • Yes, as you described, I can't invalidate JWT tokens. I used **JDBC token store** but I am not sure this still used JWT ? The session that I mean was browser session for login page of authorization server. When I tried to login again after successfully logout, the browser does not issue the login form page of Auth server even I revoke tokens and revoke approvals. – Cataclysm Jul 22 '17 at 10:02
  • @JuanCarlosMendoza For authserver, we can't create session mangement with `sessionCreationPolicy(SessionCreationPolicy.STATELESS)`. – Cataclysm Jul 23 '17 at 05:38
  • I meant stateless if your authserver is using JWT as Kumar Sambhav indicated. – Juan Carlos Mendoza Jul 23 '17 at 11:59
2

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.

Stanley Kirdey
  • 602
  • 5
  • 20
2

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.

Juan Carlos Mendoza
  • 5,736
  • 7
  • 25
  • 50
  • For JWT case *(and OP's question)*, I think your answer is good enough. At my oauth2 with JWT token based project, I can logout out delete user's authetication but when I tried to login again, the authserver didnot issued login form. Now your answer fix this solution. Now I am working with **JDBC tokenStore** and I configured as my answer. Thank you. Bounty reward is yours. – Cataclysm Jul 25 '17 at 02:47