I'm working on a web application that uses Angular for the frontend and a Spring Boot backend. We have implemented OAuth2 login via Microsoft's identity platform. Logging in works fine, but I've run into an issue when trying to implement the logout.
Here's what happens
https://login.microsoftonline.com/12345-345-450c-be26-e9df0d55c2fb/oauth2/v2.0/logout?id_token_hint=...&post_logout_redirect_uri=http://localhost:9000
The Angular client tries to follow this URL and then I get a CORS error:
Access to XMLHttpRequest at 'https://login.microsoftonline.com/...' (redirected from 'http://localhost:9000/logout') from origin 'http://localhost:9000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
I don't understand why Angular is even trying to follow the redirect like a regular HTTP request. Isn't it supposed to be handled like a typical browser redirect?
Any idea what's going on here? And more importantly, how do I fix this?
Thank you in advance!
RP-Initiated Logout is based on a redirection, not a cross-origin request from your single page or mobile application.
The reason why you face a cross-origin request is that this redirection is a follow-up of a REST request from your SPA using the HttpClient
(the POST
to /logout
).
To avoid sending such a cross-origin request, I observe the response of the POST request to /logout
and set window.location.href
with the value of the response Location
header. Something like:
async logout() {
return lastValueFrom(
this.http.post(`/logout`, null, { observe: 'response' })
).then((response) => {
const location = response.headers.get('Location');
if (!!location) {
window.location.href = location;
}
});
}
Setting the window location to an external URI "exits" your Angular app to "enter" a new website (as opposed to using the HttpClient
).
But, to observe a response
, the HTTP status must be in 2xx
range. The easiest way for that is probably to proxy an existing ServerLogoutSuccessHandler
to change the response HTTP status. If your authorization server is fully compliant with RP-Initiated Logout (I have no sufficient experience with login.microsoftonline.com
to be sure, but from what I can see from the logout URI you provide, it seems to be), using Spring's OidcClientInitiatedServerLogoutSuccessHandler
as delegate is a good option.
static class AngularLogoutSucessHandler implements ServerLogoutSuccessHandler {
private final OidcClientInitiatedServerLogoutSuccessHandler delegate;
public AngularLogoutSucessHandler(ReactiveClientRegistrationRepository clientRegistrationRepository, String postLogoutRedirectUri) {
this.delegate = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
this.delegate.setPostLogoutRedirectUri(postLogoutRedirectUri);
}
@Override
public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
return delegate.onLogoutSuccess(exchange, authentication).then(Mono.fromRunnable(() -> {
exchange.getExchange().getResponse().setStatusCode(HttpStatus.ACCEPTED);
}));
}
}
@Bean
SecurityWebFilterChain clientFilterCHain(
ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository,
@Value("${post-logout-redirect-uri}") String postLogoutRedirectUri) {
http.logout(logout -> {
logout.logoutSuccessHandler(new AngularLogoutSucessHandler(clientRegistrationRepository, postLogoutRedirectUri));
});
...
return http.build()
}
The post-logout-redirect-uri
is expected in properties (should point to your Angular app, with a path accessible to anonymous users).
spring-addons-starter-oidc
I wrote a Spring Boot starter to extend the auto-confioguration features of spring-boot-starter-oauth2-client
and spring-boot-starter-oauth2-resource-server
.
With just a few properties (and no Java conf at all), you can configure RP-Initiated Logout with custom redirection status and post-logout URI. This post-logout URI can even be overridden by the frontend using a custom header or logout request param.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>7.8.8</version>
</dependency>
</dependencies>
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: ${oauth2-issuer}
user-name-attribute: preferred_username
registration:
quiz-bff:
provider: keycloak
client-id: ${oauth2-client-id}
client-secret: ${oauth2-client-secret}
authorization-grant-type: authorization_code
scope: openid, profile, email, offline_access
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: ${oauth2-issuer}
authorities:
- path: $.realm_access.roles
client:
client-uri: ${gateway-uri}
security-matchers:
- /login/**
- /oauth2/**
- /logout/**
- /v1/**
permit-all:
- /login/**
- /oauth2/**
- /v1/**
csrf: cookie-accessible-from-js
post-login-redirect-host: ${reverse-proxy-uri}
post-login-redirect-path: /ui/
post-logout-redirect-host: ${reverse-proxy-uri}
post-logout-redirect-path: /ui/
oauth2-redirections:
rp-initiated-logout: ACCEPTED