Why getting CORS error in Spring Security - spring

My backend application is Spring boot and frontend application is React. I am trying to access spring boot app from react and getting this error:
there is some spring security configuration;
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
/**
* This method is permitting endpoints
* #author ***
* */
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors()
.and()
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(handler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/auth/**", "/swagger-ui.html", "/swagger-ui/index.html", "/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
.anyRequest().authenticated();
httpSecurity.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
and there is a JWT filter;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = extractJwtFromRequest(request);
if (jwt != null && jwtTokenProvider.validateJwtToken(jwt)) {
String username = jwtTokenProvider.getUserIdFromJwtToken(jwt);
UserDetails userDetails = userDetailsServiceImp.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
} catch (Exception e) {
e.printStackTrace();
}
filterChain.doFilter(request, response);
}
finally request from react in the network tab;
Getting;
java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
from backend server.

Your problem is because you have set
config.setAllowCredentials(true);
in combination with:
config.addAllowedOrigin("*");
setAllowCredentials means that the response will include the Access-Control-Allow-Credentials header with the value true to the calling client. This header will set if the javascript in the browser is allowed to access credential information like session cookies, authorization headers or TLS client certificates.
You have essentially set that ANY origin (since you are using a wildcard *) is allowed to touch/use the credential information that gets sent back.
This is very unsecure
And thats what the warning is telling you:
When allowCredentials is true, allowedOrigins cannot contain the special
value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header.
To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
You cannot have allowCredentials to true and allowedOrigins to * at the same time.
If you want the javascript in the browser to be able to touch credential information, you must explicitly set what origins that are allowed using the allowedOriginPatterns.
For instance:
final List<String> allowedOrigins = new ArrayList<>()
allowedOrigins.add("http://127.0.0.1:6161");
config.allowedOriginPatterns(allowedOrigins);
Or you just disable the allow credentials all together.

Related

Spring Authorization Server: Access to XMLHttpRequest has been blocked by CORS policy

I have a basic Spring Authorization Server set up as a Spring Boot application. I am attempting to access this server via an angular application using angular-auth-oidc-client.
When I attempt to log in, I get this error:
Access to XMLHttpRequest at 'http://localhost:9000/mydomain/.well-known/openid-configuration' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
I've made multiple attempts to fix this issue, but have been unsuccessful.
Relevant parts of the Configuration for the authorization server are below:
// #formatter:off
#Bean
public RegisteredClientRepository registeredClientRepository() {
// removed
}
// #formatter:on
#Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
// #formatter:off
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http
.formLogin(Customizer.withDefaults())
.cors().configurationSource(corsConfigurationSource());
return http.build();
}
// #formatter:on
That CORS Configuration seems to be wide open, but I'm still seeing the issue.
Am I just making a stupid mistake that I'm not seeing?
Edit: yes, I've configured that port and domain in application.yml:
server:
port: 9000
servlet:
context-path: /mydomain

Spring oauth2 authorization server: unable to logout users

I have created an angular app that serves as an oauth2 client. I have created my authorization server with spring oauth2 using the following security configs
#Bean
#Order(1)
public SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(corsFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable()
.headers().frameOptions().disable()
.and()
.antMatcher("/auth/account/**")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and()
.oauth2ResourceServer().jwt();
return http.build();
}
#Bean
#Order(2)
public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
// #formatter:off
http
.addFilterBefore(corsFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/management/**").permitAll()
.antMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and()
.formLogin(withDefaults());
return http.build();
}
and here is my authorization server config
#Configuration
public class AuthServerConfig {
private final DataSource dataSource;
private final AuthProperties authProps;
private final PasswordEncoder encoder;
public AuthServerConfig(DataSource dataSource, AuthProperties authProps, PasswordEncoder encoder) {
this.dataSource = dataSource;
this.authProps = authProps;
this.encoder = encoder;
}
#Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource);
}
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
#Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
JdbcRegisteredClientRepository clientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
RegisteredClient webClient = RegisteredClient.withId("98a9104c-wertyuiop")
.clientId(authProps.getClientId())
.clientName(authProps.getClientName())
.clientSecret(encoder.encode(authProps.getClientSecret()))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:4200/xxxx/yyy")
.redirectUri("http://127.0.0.1:8000/xxxx/yyy")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("farmer:read")
.scope("farmer:write")
.tokenSettings(tokenSettings())
.build();
clientRepository.save(webClient);
return clientRepository;
}
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
#Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
#Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer(authProps.getIssuerUri())
.build();
}
#Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofDays(1))
.refreshTokenTimeToLive(Duration.ofDays(1))
.build();
}
}
Here is my build.gradle file
plugins {
id 'org.springframework.boot' version '2.6.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'org.liquibase.gradle' version '2.1.0'
id 'java'
}
group = 'com.shamba.records'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/release' }
}
ext {
set('springCloudVersion', "2021.0.0")
set('liquibaseVersion', "4.6.1")
}
configurations {
liquibaseRuntime.extendsFrom runtimeClasspath
}
dependencies {
implementation 'tech.jhipster:jhipster-framework:7.4.0'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:0.2.1'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.security:spring-security-cas:5.6.1'
// mapstruct
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
// jackson
implementation 'com.fasterxml.jackson.module:jackson-module-jaxb-annotations'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hppc'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'org.zalando:problem-spring-web:0.26.0'
// configure liquibase
implementation "org.liquibase:liquibase-core:${liquibaseVersion}"
liquibaseRuntime 'org.liquibase:liquibase-groovy-dsl:3.0.0'
liquibaseRuntime 'info.picocli:picocli:4.6.1'
liquibaseRuntime 'org.postgresql:postgresql'
liquibaseRuntime group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
liquibaseRuntime 'org.liquibase.ext:liquibase-hibernate5:3.6'
liquibaseRuntime sourceSets.main.output
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
and here is part of the properties, I have omitted other things because of brevity
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://${AUTH_SERVICE_HOST:127.0.0.1}:5000
jwk-set-uri: http://${AUTH_SERVICE_HOST:127.0.0.1}:5000/oauth2/jwks
I am able to sign in and sign out users using authorization code flow but the issue comes in after the first successful sign-in, when the users click on the sign in the user is automatically logged in by the auth server even after calling the /oauth2/revoke endpoint and specifying the logout configs below in the auth server
.and()
.logout()
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
I also tried to implement a custom endpoint /auth/account/revoke to manually log out users but nothing seems to work. here is the implementation
#RestController
#RequestMapping("auth/account")
public class AccountResource {
#GetMapping("/revoke")
public void revoke(HttpServletRequest request) {
Assert.notNull(request, "HttpServletRequest required");
HttpSession session = request.getSession(false);
if (!Objects.isNull(session)) {
session.removeAttribute("SPRING_SECURITY_CONTEXT");
session.invalidate();
}
SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
}
}
what could be the issue? any help counts
---------updates-------------
After upgrading spring-security-oauth2-authorization-server version 0.2.2 I updated this method
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
to this
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer.tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint
.revocationResponseHandler((request, response, authentication) -> {
Assert.notNull(request, "HttpServletRequest required");
HttpSession session = request.getSession(false);
if (!Objects.isNull(session)) {
session.removeAttribute("SPRING_SECURITY_CONTEXT");
session.invalidate();
}
SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
response.setStatus(HttpStatus.OK.value());
})
);
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
return http.formLogin(Customizer.withDefaults()).build();
}
There are two concepts in play that are somewhat confusingly related.
Secure logout
Token revocation
Regarding logging out of an application, this is necessary when a browser-based session is in use, which would usually be the case with the authorization_code flow. Strictly speaking, terminating the session is all that is required to achieve your goal.
Regarding token revocation, this is more of an OAuth-related security concern and is distinct in that sense from traditional logout functionality. Typically, the most immediate need for token revocation is as a risk mitigation strategy when a refresh_token (or to a lesser extent the associated access_token) is stolen. If a token is not stolen, one could conceivably discard the token from memory on the client-side, and simply let it expire on the server. However, this is unlikely to be a recommendation, as it allows risk to exist that can be closed down proactively by revoking the token on logout.
So the question is, how do we achieve both simultaneously, right? Unfortunately, it's not built-in to either Spring Security or Spring Authorization Server at the moment, though there are specifications that could be used as the basis of feature development.
In general, you will need to solve this on the backend, as there's not much a frontend can do. If you simply POST to a /logout endpoint from your UI, you can't manipulate the cookies stored for that host in the browser (when using CORS). I recommended using the backend-for-frontend pattern for your OAuth client. But even (and especially) if you don't, you will need to ensure you can terminate the session regardless of whether the cookie exists in the browser. This means storing the session in a database, associating it with the refresh token in some way, and making a secure call to the revocation endpoint using the refresh token to delete both from the database simultaneously.
You can achieve this by setting the AuthenticationSuccessHandler on the OAuth2TokenRevocationEndpointFilter, like so:
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer.tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint
.revocationResponseHandler((request, response, authentication) -> {
/* delete session here... */
response.setStatus(HttpStatus.OK.value());
})
);
// ...
Hopefully that's enough to get you started. You may find some benefit in working through the specifics of how this might be achieved yourself (e.g. it's a good learning experience). If you're stuck, it could be a good opportunity to request a How-to guide, as we're fielding ideas for guides right now. See #499 for a list of existing how-to guides, and please feel free to submit your own!
For anyone facing the same issue, I was not able to make it work when using angular directly as a client, but following the advice of #SteveRiesenberg I recommended using the backend-for-frontend pattern for your OAuth client I was able to make it work, therefore I advise anyone to use this pattern as it will help you avoid some of the pitfalls you would otherwise have faced as I did, plus it integrates seamlessly. For a head start you can refer to the sample project below created by the spring security team SpringOne 2021 repository Also here is a link to the presentation Spring Security 5.5 From Taxi to Takeoff
I created success handler for revocation endpoint like Steve pointed out and invalidated sessions manually. Revoke is called from another application as a part of logout success handler.
RevocationResponseHandler:
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, OAuth2AuthorizationService authorizationService, FindByIndexNameSessionRepository<?> sessions) throws Exception {
var authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<HttpSecurity>();
authorizationServerConfigurer.tokenRevocationEndpoint(c -> c.revocationResponseHandler(sessionInvalidatingSuccessHandler(authorizationService, sessions)));
// ...
}
private AuthenticationSuccessHandler sessionInvalidatingSuccessHandler(OAuth2AuthorizationService authorizationService, FindByIndexNameSessionRepository<?> sessions) {
return (request, response, authentication) -> {
String token = request.getParameter(OAuth2ParameterNames.TOKEN);
if (token != null) {
OAuth2Authorization authorization = authorizationService.findByToken(token, null);
if (authorization != null) {
sessions.findByPrincipalName(authorization.getPrincipalName()).forEach((sessionId, session) -> sessions.deleteById(sessionId));
}
}
response.setStatus(HttpStatus.OK.value());
};
}
I faced the same issue. after spending five days of hard work, I found JSESSIONID to be removed from Server Side and Client Side.
I was trying the following things
revoked oauth2 token by calling authorization server's oauth2/revoke endpoint (both refresh and access tokens) but logout did not work.
I tried to call /logout endpoint of Authorization Server that also did not work out
I tried to remove JSESSIONID from client browser which also went in vein.
I tried to remove JSESSIONID from Server and Client side. that worked out. Now I am able to implement logout successfully
I added the following endpoint in Spring Authorization Server
#GetMapping("/do/logout")
#ResponseBody
public void doLogout(HttpSession session,HttpServletRequest request, HttpServletResponse response) {
System.out.println("entered logout point");
session.invalidate();
final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
auth.setAuthenticated(false);
SecurityContextHolder.clearContext();
SecurityContextHolder.getContext().setAuthentication(null);
}
Cookie cookieWithSlash = new Cookie("JSESSIONID", null);
cookieWithSlash.setPath(request.getContextPath() + "/");
cookieWithSlash.setDomain("auth-server");
cookieWithSlash.setMaxAge(0);
response.addCookie(cookieWithSlash); // For Tomcat
}
and the following endpoint in client application
#PostMapping("/complete/logout/process")
public String testLogout(HttpSession session, HttpServletRequest request,
HttpServletResponse response,
#RegisteredOAuth2AuthorizedClient("messaging-client-oidc") OAuth2AuthorizedClient authorizedClient)
throws IOException {
final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
auth.setAuthenticated(false);
SecurityContextHolder.clearContext();
for (Cookie cookie : request.getCookies()) {
String cookieName = cookie.getName();
LOG.info("cookie name={}", cookieName);
Cookie cookieToDelete = new Cookie(cookieName, null);
cookieToDelete.setPath(request.getContextPath() + "/");
cookieToDelete.setMaxAge(0);
response.addCookie(cookieToDelete);
}
SecurityContextHolder.getContext().setAuthentication(null);
}
return "logout";
}
and in html I called two endpoints /server side logout and client side logout
that cleared both the JSESSIONID cookie from the browser
<form method="post" id="clientLogout" th:action="#{/complete/logout/process}">
</form>
button class="btn btn-sm btn-outline-primary" type="button"
onclick="javascript:logout();">Logout</button>
<script>
function logout() {
// calling server side logout
var tmpwin = window.open("http://auth-server:9000/do/logout", "_blank");
// calling client side logout
document.getElementById("clientLogout").submit();
tmpwin.close();
}
</script>
logging out from server side and client side clears both the JSESSIONID cookies finally able to logout
if anything I am missing or doing wrong please educate me. Thanks
Additionally you may invalidate oauth2 access and refresh tokens if you want
#PostMapping("do/oauth2/token/revoke")
#ResponseBody
public void doOauth2TokenRevoke(#RegisteredOAuth2AuthorizedClient("messaging-client-oidc") OAuth2AuthorizedClient authorizedClient) {
String clientId = authorizedClient.getClientRegistration().getClientId();
String clientSecret = "secret";
String accesstoken = authorizedClient.getAccessToken().getTokenValue();
String refreshtoken = authorizedClient.getRefreshToken().getTokenValue();
LinkedMultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
map.add("token", refreshtoken);
map.add("token_type_hint", "refresh_token");
WebClient client3 = WebClient.builder().baseUrl("http://auth-server:9000").build();
client3.post()
.uri("/oauth2/revoke")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.headers(h -> h.setBasicAuth(clientId, clientSecret))
.body(BodyInserters.fromMultipartData(map))
.retrieve()
.bodyToMono(Void.class)
.block();
LinkedMultiValueMap<String, String> map2 = new LinkedMultiValueMap<String, String>();
map2.add("token", accesstoken);
map2.add("token_type_hint", "access_token");
WebClient client2 = WebClient.builder().baseUrl("http://auth-server:9000").build();
client2.post()
.uri("/oauth2/revoke")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.headers(h -> h.setBasicAuth(clientId, clientSecret))
.body(BodyInserters.fromMultipartData(map2))
.retrieve()
.bodyToMono(Void.class)
.block();
}
On defaultSecurityFilterChain set SessionCreationPolicy to NEVER and add a custom logOut Filter that invalidates the session and redirects to your redirect Uri.
And from your UI make a post to /logout?redirect_uri=
#Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, UserService userService) throws Exception {
http.csrf().disable();
http
.formLogin().loginPage("/login")
.and()
.logout(logout->{
logout.logoutUrl("/logout");
})
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.authorizeRequests()
.antMatchers("/login*").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(new CustomLogoutFilter(), SecurityContextPersistenceFilter.class);
return http.build();
}
public class CustomLogoutFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(request.getServletPath().equals("/logout")) {
HttpSession session = request.getSession(false);
if(session != null) {
log.info("Invalidating session");
session.invalidate();
}
SecurityContext context = SecurityContextHolder.getContext();
if(context != null) {
log.info("Clearing security context");
SecurityContextHolder.clearContext();
context.setAuthentication(null);
}
String redirectUri = request.getParameter("redirect_uri");
if(Utils.isEmpty(redirectUri)) {
redirectUri = request.getHeader("referer");
}
if(Utils.notEmpty(redirectUri)) {
response.sendRedirect(redirectUri);
return;
}
}
filterChain.doFilter(request, response);
}
}
I managed to create a filter named TokenValidationFilter at resource server that calls authorization server's introspect endpoint resulting in the following:
/**
* Performs token introspection when
* an authenticated endpoint gets hit.
*/
#Component
public class TokenValidationFilter extends OncePerRequestFilter {
#Value( "${spring.security.oauth2.client.provider.authz-server.introspect}" )
private String introspectEndpoint;
#Value( "${spring.security.oauth2.client.registration.authz-server.client-id}" )
private String clientId;
#Value(
"${spring.security.oauth2.client.registration.authz-server.client-secret}" )
private String clientSecret;
#Override
public void doFilterInternal( HttpServletRequest request,
HttpServletResponse response, FilterChain chain )
throws ServletException {
try {
var token = request.getHeader( "authorization" );
// For endpoints that do not require Authorization header claim
if (token == null) {
chain.doFilter(request, response);
return;
}
var result = performTokenIntrospection( token );
// Parses authz-server http response into json
ObjectMapper mapper = new ObjectMapper();
JsonNode json = mapper.readTree( result.body() );
// Checks token validation
if ( json.get( "active" ).asBoolean( false ) ) {
chain.doFilter(request, response);
} else {
prepareRejectionResponse( response );
}
}
catch ( Exception ex ) {
throw new ServletException( ex );
}
}
private HttpResponse<String> performTokenIntrospection( String token )
throws IOException, InterruptedException,
NullPointerException, URISyntaxException {
var uri = new URI( introspectEndpoint );
// Prepares request to authz server introspection endpoint
var body = String.format( "token=%s&client_id=%s&client_secret=%s",
token.replaceAll( "[Bb][Ee][Aa][Rr][Ee][Rr] ", "" ),
clientId, clientSecret);
var req = HttpRequest
.newBuilder()
.uri(uri)
.headers(
"content-type", MediaType.APPLICATION_FORM_URLENCODED_VALUE,
"authorization", token
)
.POST( HttpRequest.BodyPublishers.ofString( body ) )
.build();
// Performs token introspection and returns its result
return HttpClient.newHttpClient()
.send( req, HttpResponse.BodyHandlers.ofString() );
}
private void prepareRejectionResponse( HttpServletResponse response )
throws IOException {
response.setStatus( HttpStatus.UNAUTHORIZED.value() );
response.setContentType( MediaType.APPLICATION_JSON_VALUE );
var writer = response.getWriter();
writer.print( "{\"message\": \"invalid token\"}" );
writer.close();
}
}
As you can see the filter attributes uses resource server's ( client-id and client-secret ) credentials, and i added token introspection's endpoint on my application.yml file.
Finally, registering the filter in the configuration class my ResourceServerConfig looked like:
#EnableWebSecurity
public class ResourceServerConfig {
#Bean
SecurityFilterChain securityFilterChain( HttpSecurity http )
throws Exception {
//... omitted code above ...
http.addFilterBefore( tokenValidationFilter,
WebAsyncManagerIntegrationFilter.class );
return http.build();
}
// ... omitted code bellow ...
}
I just tested on development environment but yet to find its drawbacks, there might be better solutions but it works without spamming another application.
As far as i know (which is little btw), the solution presented backend-for-frontend looks better in security perspective.
Obs: if you apply this solution remeber always to revoke by refresh_token as oauth2 framework specifies.
.antMatchers("/auth/account/**").authenticated() ?

Spring Boot Security + Keycloak adapter 401 Response when token expires while antMatchers is permitAll()

When i login i receive access_token and refresh_token which i send in every HTTP request as headers to the Spring boot server.
Problem that i have is when token expires i get 401 Response when i try to access routes that are accessible without authentication, so result is i can't access /v1/users/current which checks if token is valid if not take refresh token and give me a new access token.
What Im trying to do now is make /v1/users/current accessible without any type of authorization/authentication so i can receive a new access token, and I'm not sure how to achieve that using KeycloakWebSecurityConfigurerAdapter.
I've tryed to use HandlerInterceptor but preHandle request that i receive is already been trough keycloak and i recieve simmilar error like 401.
Thanks in advance!
EDIT: When i use POSTMAN and send a get request to /v1/users/current without access token(No auth) and with refresh token in header i still get access to /v1/users/current route and get the new access token i need.
#KeycloakConfiguration
class ResourceServerConfiguration extends KeycloakWebSecurityConfigurerAdapter {
#Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
//the below three lines will add the relevant CORS response headers
configuration.addAllowedOrigin("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<>(new HttpSessionEventPublisher());
}
#Autowired
public void configureGlobal(
AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider
= keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(
new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors().configurationSource(corsConfigurationSource())
.and()
.authorizeRequests().antMatchers(HttpMethod.GET,"/v1/users/current").permitAll().and()
.authorizeRequests()
.antMatchers(HttpMethod.POST,"/login").permitAll()
.antMatchers(HttpMethod.POST,"/register/admin").hasRole("admin")
.antMatchers(HttpMethod.POST, "/*")
.authenticated()
.anyRequest()
.permitAll();
}
/* #Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.GET,"/v1/users/current");
}*/
}
It seems that KeycloakAuthenticationProcessingFilter is checking each http request that has Authorization HTTP header, which means if you need to access a route without checking it you send it without Authorization headers.
I've changed my /v1/users/current to POST without header and I'm sending access_token and refresh_token in request body.

How to add CORS to outh2/resource server in Spring Boot 2.x?

I have an oauth server and a resource server that I have created with JWT.
I also created an angular front end with 2 buttons:
The first button calls the auth server and gets the JWT token and adds it to the input box.
The second button calls the rest server with the JWT token as a bearer Authorisation http header.
Calling the 2 services from PostMan works perfectly but I cannot get the CORS setup correctly configured for the back end services.
Both buttons are giving me the below error:
Access to XMLHttpRequest at 'http://localhost:8085/oauth/token' from origin 'http://localhost:4200' has been blocked by CORS policy: Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.
I added all 3 of these projects to my public github repo.
I have tried to add CORS with several ways:
The config on the resource rest service is smaller so I will outline that here
I tried adding the default .cors() on the HttpSecurity as well as setting it manually in the corsConfigurationSource() method.
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and().authorizeRequests().anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
//I tried manually configured the cors as well
/*http.csrf().disable()
.cors().configurationSource(corsConfigurationSource())
.and().authorizeRequests().anyRequest().authenticated();
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);*/
}
/* #Bean
CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
configuration.setAllowCredentials(true);
//the below three lines will add the relevant CORS response headers
configuration.addAllowedOrigin("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
*/
}
I also tried adding a servlet filter
#Component #Order(Ordered.HIGHEST_PRECEDENCE) public class
SimpleCorsFilter implements Filter {
#Override
public void init(final FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws
IOException, ServletException {
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final HttpServletRequest request = (HttpServletRequest) servletRequest;
response.addHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "content-type, x-requested-with, authorisation");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
#Override
public void destroy() {
} }
Just can't get it to work. Can anyone please give me some guidelines here?
Silly mistake on my end as in both my SimpleCorsFilter.java files I specified that authorisation header tags are allowed but it is not authorisation with an S but with a Z.
Changing both the files in my config server
response.setHeader("Access-Control-Allow-Headers", "content-type,
x-requested-with, Authorization");
Extends your class with withWebMvcConfigurer rather than WebSecurityConfigurerAdapter. The override the following method:
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE").allowedHeaders("*");
}
It should add the origin. You can play with '*' and make many combination. I have given you idean now it is your turn to play with this API.

Simple CORS setup is not working by adopting all means

The frontend server is running on localhost:8080 and try to do CORS PUT request to the Spring boot server running on localhost:1072
I googled all the possible solution to make the CORS request work.
However, it's only working by using Postman for the PUT request.
Got 401 on the Chrome browser.
How do I make the Spring server could take CORS requests.
Thanks!
Also, curious why Spring doesn't show the exception on the console and always give developers hard time lol
CORSConfig.java
#Configuration
public class CORSConfig implements WebMvcConfigurer {
#Override
public void addCorsMappings(CorsRegistry registry) {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:8080");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
registry.addMapping("/**")
.allowedMethods("*").allowedOrigins("*");
}
}
WebSecurityConfig.java
#Override
protected void configure(HttpSecurity http) throws Exception {
if (h2ConsoleEnabled)
http.authorizeRequests()
.antMatchers("/h2-console", "/h2-console/**").permitAll()
.and()
.headers().frameOptions().sameOrigin();
http.csrf().disable()
.cors()
.and()
.exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/v1/**","/articles/**", "/profiles/**", "/tags").permitAll()
.antMatchers(HttpMethod.PUT, "/v1/**","/articles/**", "/profiles/**", "/tags").permitAll()
.antMatchers(HttpMethod.POST, "/v1/**","/articles/**", "/profiles/**", "/tags").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.addAllowedOrigin("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
ArticleApi.java
#CrossOrigin(origins = "*")
#RestController
#RequestMapping(path = "/v1/groups/{groupId}/articles/{aId}")
public class ArticleApi {
#CrossOrigin(origins = "*")
#PutMapping
public String updateArticle(#PathVariable("groupId") String groupId,
#PathVariable("aId") String aId
) {
return
}
The 401 reponse is received when the pre-flight check for the CORS request fails. So, it might be that your cors is not setup correctly.When reading through your config it made the following observations :
If you are going to allow cross origin requests from all domains on all methods, you could remove the controller method level annotation #CrossOrigin(origins = "*") as it is already specified at class level.
You are providing two global configurations for the CORS config. One config with the bean order set as 0 accepts only origin http://localhost:8080 while that configured with spring security accepts all origin.Remove one and keep either of the two as per your need.
You could try removing the CORS configuration provided in the class CORSConfig. You have already provided cors configuration along with WebSecurityConfig. You could remove the cors configuration provided in the security config,either way it will work with just one configuration or try removing the below code :
#Configuration
public class CORSConfig implements WebMvcConfigurer {
#Override
public void addCorsMappings(CorsRegistry registry) {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:8080");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
registry.addMapping("/**")
.allowedMethods("*").allowedOrigins("*");
}
}

Resources