Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 874
Kumar Sambhav Avatar asked Jan 25 '17 11:01

Kumar Sambhav


People also ask

Does Spring Security using OAuth2?

Spring Security provides comprehensive OAuth 2 support.

Is OAuth2RestTemplate deprecated?

Deprecated. See the OAuth 2.0 Migration Guide for Spring Security 5. Rest template that is able to make OAuth2-authenticated REST requests with the credentials of the provided resource.

How does OAuth2 2.0 work in spring boot?

In Spring boot, we have one mechanism which helps us to do Authorization; this is called as oauth2. 0; by the use of this, we can easily authorize the interaction between two services. The main purpose of oauth2 is to authorize two services on behalf of the user who has access to the resource.

How does OAuth2 2.0 work in REST API?

In OAuth 2.0, the following three parties are involved: The user, who possesses data that is accessed through the API and wants to allow the application to access it. The application, which is to access the data through the API on the user's behalf. The API, which controls and enables access to the user's data.


1 Answers

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.

like image 125
Cataclysm Avatar answered Oct 29 '22 15:10

Cataclysm