Background: I have 2 applications:
http://192.168.0.4:8001
http://192.168.0.4:8002
I have noted that during the redirect process the HTTP.POST /oauth2/token
Method from the redirect javascript doesn't attach some properties. i.e Cookie and some headers (Basic Auth Code/Key). That could be the root of the issues. These are required by the Spring OAuth Server to maintain state.
Resource Server:
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.3'
id 'io.spring.dependency-management' version '1.1.3'
id 'org.hibernate.orm' version '6.2.7.Final'
id 'org.graalvm.buildtools.native' version '0.9.24'
}
group = 'io.resource'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
}
tasks.named('test') {
useJUnitPlatform()
}
hibernate {
enhancement {
enableAssociationManagement = true
}
}
The Redirect script:
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function parseQueryParams(query) {
let params = {};
query.split('&').forEach(function(part) {
let item = part.split('=');
params[item[0]] = decodeURIComponent(item[1]);
});
return params;
}
function run() {
let oauth2 = window.opener.swaggerUIRedirectOauth2;
let sentState = oauth2.state;
let redirectUrl = oauth2.redirectUrl;
let qp = window.location.hash.substring(1) || location.search.substring(1);
let params = parseQueryParams(qp);
let isValid = params.state === sentState;
let errorCb = function(message) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: message
});
};
if (['accessCode', 'authorizationCode', 'authorization_code'].includes(oauth2.auth.schema.get("flow")) && !oauth2.auth.code) {
if (!isValid) {
errorCb("Authorization may be unsafe, passed state was changed in server. Passed state wasn't returned from auth server.");
return;
}
if (params.code) {
delete oauth2.state;
oauth2.auth.code = params.code;
oauth2.callback({ auth: oauth2.auth, redirectUrl: redirectUrl });
} else {
errorCb(params.error ? `[${params.error}]: ${params.error_description || 'no accessCode received from the server'}. ${params.error_uri || ''}` : "[Authorization failed]: no accessCode received from the server");
}
} else {
oauth2.callback({ auth: oauth2.auth, token: params, isValid: isValid, redirectUrl: redirectUrl });
}
window.close();
}
window.addEventListener('DOMContentLoaded', run);
</script>
</body>
</html>
OpenAPI/Swagger-UI Config:
package io.resource.account.spring.config.swagger;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.OAuthFlow;
import io.swagger.v3.oas.annotations.security.OAuthFlows;
import io.swagger.v3.oas.annotations.security.OAuthScope;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Collections;
@Configuration
@SecurityScheme(
name = "security_auth",
type = SecuritySchemeType.OAUTH2,
flows = @OAuthFlows(
authorizationCode = @OAuthFlow(
authorizationUrl = "${springdoc.oAuthFlow.authorization-url}",
tokenUrl = "${springdoc.oAuthFlow.token-url}",
scopes = {
@OAuthScope(name = "account.create", description = "Create account"),
@OAuthScope(name = "account.read", description = "Read account"),
@OAuthScope(name = "account.update", description = "Update account"),
@OAuthScope(name = "account.delete", description = "Delete account")
})))
public class OpenAPIConfig {
@Bean
public OpenAPI gateWayOpenApi() {
return new OpenAPI().info(new Info().title("Accounts Service")
.description("Accounts Microservice Swagger API")
.version("v1.0.0")
.contact(new Contact()
.name("Accounts Dev Team")
.email("[email protected]")))
.addSecurityItem(new SecurityRequirement()
.addList("bearer-jwt", Arrays.asList("account.create", "account.read", "account.update", "account.delete"))
.addList("bearer-key", Collections.emptyList()));
}
}
Resource Server Config:
package io.resource.account.spring.config.security;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
@Log4j2
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ResourceServerConfig {
// @formatter:off
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll())
.authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.GET, "/auth").fullyAuthenticated())
.authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.POST, "/accounts").hasAuthority("SCOPE_account.create"))
.authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.GET, "/accounts/**", "/accounts").hasAuthority("SCOPE_account.read"))
.authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.PATCH, "/accounts/**").hasAuthority("SCOPE_account.update"))
.authorizeHttpRequests(req -> req.requestMatchers(HttpMethod.DELETE, "/accounts/**").hasAuthority("SCOPE_account.delete"))
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
// .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.authorizeHttpRequests(req -> req.requestMatchers(
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui**",
"/api-docs-ui.html",
"/oauth2-redirect.html",
"/swagger-ui/oauth2-redirect.html",
"/public/**",
"/api-docs",
"/api-docs/**",
"/webjars/**",
"/swagger-resources/**",
"/actuator/**")
.permitAll());
return httpSecurity.build();
}
// @formatter:on
}
I tested the authorization_code flow (& with PKCE) via postman successfully, but I've had no luck with Swagger-UI upon retrieving the authorization_code from the Auth-Server. When attempting to use the authorization code to retrieve the access_token, I then encountered the following error:
Access to fetch at 'http://192.168.0.4:8001/oauth2/token' from origin 'http://192.168.0.4:8002' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
I was expecting to get the access_token from the authorization server:
curl --location 'http://192.168.100.102:8001/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic Y2xpZW50OnNlY3JldA==' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=client' \
--data-urlencode 'redirect_uri=http://192.168.0.4:8002/swagger-ui/oauth2-redirect.html' \
--data-urlencode 'code=S-yW9-fNexepByQEvWnr5MLMSO8YAtX1npBp7VUEF3FAo-YqljVbVGGuf1eIa8kpJnviZXGzsgM59HJlULJc5nHI1blWbcxG7xINm7FpVkcG0zxQKdWBUSxsADy23bKD'
CORS is a common issue and many stack overflow questions deal with it. Having said that, if you need to allow pre-flight requests from pages served by the the resource server (Swagger UI), you'll need to configure CORS with Spring Security. The guide How-to: Authenticate using a Single Page Application with PKCE contains an example configuration you can use to get started.