Cors issue when redirecting to external auth after logout in Spring Security

I am implementing OIDC based authentication in a Spring Boot based Camunda application. I have been able to successfully implement user logging in but I am facing difficulty in logging the user out. I am using Azure B2C as the auth server and as far as I understand, I need to sign out both from the app and the auth server. Azure gives an endpoint to logout of the form https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{policy}/oauth2/v2.0/logout?post_logout_redirect_uri=https%3A%2F%2Fjwt.ms%2F.

Now, the configuration I use is as such

@Override
protected void configure(HttpSecurity http) throws Exception {
    OidcUserService oidcUserService = new OidcUserService();

    http
        .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        .and()
        .requestMatchers()
        .antMatchers("/camunda/**", "/api/**", "/login/**", "/oauth2/**")
        .and()
        .authorizeRequests()
        .anyRequest()
        .authenticated()
        .and()
        .oauth2Login()
        .loginPage("/oauth2/authorization/azure-ac")
        .userInfoEndpoint().oidcUserService(oidcUserService)
        .and()
        .and()
        .logout()
        .logoutUrl("/camunda/api/admin/auth/user/default/logout")
        .logoutSuccessUrl("https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{policy}/oauth2/v2.0/logout?post_logout_redirect_uri=https%3A%2F%2Fjwt.ms%2F");
}

Note: I replace the {} with my values.

This doesn’t work as I get CORS issues in the browser.

Access to XMLHttpRequest at 'https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{policy}/oauth2/v2.0/logout?post_logout_redirect_uri=https%3A%2F%2Fjwt.ms%2F' (redirected from 'https://localhost:6333/camunda/api/admin/auth/user/default/logout') 
from origin 'https://localhost:6333' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

This issue is very similar to https://stackoverflow.com/questions/64713413/keycloak-cors-issue-on-logout-redirect but in that case, the front end is 1st party and I would prefer not to modify the Camunda webapps. Also in the accepted answer it is not clear if frontend must be the one to control the redirection.

Is there anyway to achieve this from Spring itself?

EDIT:

I have been researching and I have found some things that might be useful. Everytime the browser tries to redirect to the external url, it generates a pre-flight request. This request’s response has the Access-Control-Allow-Origin: https://localhost:6333 headers but the actual request doesn’t have the header. Reading some stuff online it seems like sending a pre flight request to servers that are not expecting one can throw them off. Moreover, the redirect request also contains the X-XSRF-TOKEN header. Is it possible that the presence of the XSRF header is confusing the browser? This the full fetch of the request.

fetch("https://tenant.b2clogin.com/tenant.onmicrosoft.com/policy/oauth2/v2.0/logout?post_logout_redirect_uri=https%3A%2F%2Fjwt.ms%2F", {
  "headers": {
    "accept": "application/json, text/plain, */*",
    "accept-language": "en-US,en;q=0.9",
    "cache-control": "no-cache",
    "pragma": "no-cache",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "cross-site",
    "x-xsrf-token": "THETOKENISHERE"
  },
  "referrer": "https://localhost:6333/",
  "referrerPolicy": "strict-origin-when-cross-origin",
  "body": null,
  "method": "GET",
  "mode": "cors",
  "credentials": "omit"
});

Have you tried the OidcClientInitiatedLogoutSuccessHandler provided by Spring Security instead of creating the url directly? It is specifically for the RP-initiated logout. It may give you a slightly different redirect request.

If you’re still having issues, it may also be helpful to include the request/response data from the request itself in the browser.

Hi
I have tried using OidcClientInitiatedLogoutSuccessHandler too but with no luck. I have solved the problem for now so let me try and describe what I have observed.

The Issue

When I press the logout button, it creates an AJAX POST request to the /camunda/api/admin/auth/user/default/logout endpoint. Now, in Spring, if I do something like

.logout()
.logoutUrl("/camunda/api/admin/auth/user/default/logout")
.logoutSuccessUrl(azureLogoutUrl)

it sends back a 302 response. Spring sends back a 302 response even if I am using SimpleUrlLogoutSuccessHandler or OidcClientInitiatedLogoutSuccessHandler. The problem lies with how this response is handled by the webapp. Since, its a 302 response, the Angular app essentially rejects this. Meanwhile a browser captures it but instead of redirecting the page, it just makes another AJAX call to the Location provided by the 302 response. Since this location is outside the current origin and the request is an AJAX request, it fails with a CORS error.
Let me know if there is some obvious issue with my diagnosis of the issue.

The Solution

Now, the solution is tricky and I don’t know if its safe or not. But it works. First, I had to ensure that Spring doesn’t return 302 on logout since Angular (or any frontend AJAX frameworks for that matter) can’t handle 302. So, I created a custom logout success handler on the veins of HttpStatusReturningLogoutSuccessHandler. This custom handler not only returns 200, like HttpStatusReturningLogoutSuccessHandler but also returns a custom header.

@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    @Value("${logout-url}")
    String idpLogoutUrl;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        response.setStatus(200);
        response.setHeader("Custom-Location", idpLogoutUrl);
    }
}

Then, I customised the webapp by adding a HTTP intercepter. In this intercepter, I intercepted all responses, and if a response contained my custom header, I issued a manual window.location based redirect.

'use strict';
define('logout-interceptor-ng-module', ['angular'], function (angular) {
    let customModule = angular.module('logout.interceptor.module', []);

    customModule.factory('myHttpInterceptor', function () {
        return {
            response: function (response) {
                let customLocation = response.headers()["custom-location"];
                if (beezLocation) {
                    window.location = customLocation ;
                }
                return response;
            }
        };
    });
    customModule.config(function ($httpProvider) {
        $httpProvider.interceptors.push('myHttpInterceptor');
    });

    return customModule;
});

I am using Spring Boot and this is the folder structure one needs to use


Each of the config.js are essentially the same with only the global variable changing. The config.js for admin looks like

window.camAdminConf = {
    customScripts: {
        ngDeps: ['logout.interceptor.module'],
        deps: ['logout-interceptor-ng-module'],
        paths: {
            'logout-interceptor-ng-module': '../logout-interceptor-ng-module/scripts'
        }
    },
};

This solution works with the only irritant being the Camunda login screen flashing for a fraction of a second before the redirect happens but this is something I can live with although I would like to fix that too at some point of time.

I had thought of another way to fix this and that was too directly hook into the method getting called when the logout button was pressed but I didn’t find a way to do that.

Let me know your thoughts on this.

1 Like

My initial thought is that I’m sorry your solution was even necessary. Sometimes coding around a problem is easier than banging your head against something. I think you captured the root of the problem when you say

Meanwhile a browser captures it but instead of redirecting the page, it just makes another AJAX call to the Location provided by the 302 response. Since this location is outside the current origin and the request is an AJAX request, it fails with a CORS error.

It’s really hard to speculate without seeing the requests in the browser, but XHR Post to /camunda/api/admin/auth/user/default/logout that results in a 302 redirect should be easily followed by the browser and, as evidenced by the CORS problem, it’s trying. I still think if Spring was setting up the response properly, the browser would oblige. You could even test this out a bit by redirecting to somewhere relative and you would see the redirect with no problem, e.g.

…
.logoutSuccessUrl(“/logout-test”)

As a side note, your config is pretty close to the Keycloak example for Camunda.

At this point, you have a solution and without a reproduction it’s a bit challenging to be of more help to you :slight_smile: If you’d like to revisit, maybe we can figure out a small repro.