Spring Security OAuth2 AngularJS | Logout Flow

旧街凉风 提交于 2019-12-05 11:16:08

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!