I am getting related error during validation using Spring Security oauth2 vs Keycloak. I don't think it's missing from my dependencies.
Error
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Unsatisfied dependency expressed through method 'setFilterChains' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'filterChain' defined in class path resource [com/framework/security/SecurityConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'filterChain' threw exception; nested exception is java.lang.NoClassDefFoundError: org/springframework/security/oauth2/server/resource/web/BearerTokenResolver
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:767) ~[spring-beans-5.3.23.jar:5.3.23]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) ~[spring-context-5.3.23.jar:5.3.23]
at
Caused by: java.lang.ClassNotFoundException: org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
at jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) ~[?:?]
My Security class
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/login/**").hasRole("role-name").anyRequest().authenticated().and().oauth2ResourceServer().jwt();
return http.build();
}
}
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
</dependencies>
application.yml
security:
oauth2:
resource-server:
jwt:
issuer-uri: http://keycloak-url/realms/realm-id
jwk-set-uri: http://keycloak-url/realms/realm-id/protocol/openid-connect/certs
client:
registration:
client-ui:
provider: keycloak
client-id: client-ui
client-secret: abcdefgh
authorization-grant-type: authorization_code
scope: openid
provider:
client-ui:
authorization-uri: http://keycloak-url/realms/realm-id/protocol/openid-connect/auth
token-uri: http://keycloak-url/realms/realm-id/protocol/openid-connect/token
user-info-uri: http://keycloak-url/realms/realm-id/protocol/openid-connect/userinfo
jwk-set-uri: http://keycloak-url/realms/realm-id/protocol/openid-connect/certs
user-name-attribute: preferred_username
I tried to make many edits but I couldn't find the problem. How can i solve it.
First, you should decide if your app is primarily an OAuth2 client with login or an OAuth2 resource server. Here you are mixing client with login (registration with authorization_code flow) and resource server configuration in the same security filter chain. This is something you should not be doing. Security mechanisms are too different:
oauth2Login (authorization_code flow) relies on sessions, requires CSRF protection and you usually expect 302 redirect to login to unauthorized requestsoauth2ResourceServer relies on access tokens, can be stateless, without CSRF protection and should respond with 401 unauthorizedoauth2resourceServer (REST API)Complete tutorial.
Resource servers don't care about login nor OAuth2 flows which are client concerns. It only cares if a request is authorized (has a valid access token), if this token should be introspected or decoded, and if it should grant access to the requested resources based on the token claims.
Use Postman or any OAuth2 client which can authorize its requests and send any type of request (not just GET, but also POST, PUT and DELETE) to try your running API.
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
@SpringBootApplication
public class ResourceServerMinimalApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceServerMinimalApplication.class, args);
}
@Order(Ordered.HIGHEST_PRECEDENCE)
@Bean
SecurityFilterChain resourceServerSecurityFilterChain(
HttpSecurity http,
@Value("${resource-server.cors.allowed-origins:}#{T(java.util.Collections).emptyList()}") List<String> allowedOrigins)
throws Exception {
http.oauth2ResourceServer(oauth2 -> oauth2.jwt());
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.csrf(csrf -> csrf.disable());
http.exceptionHandling(handeling -> handeling.authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
}));
http.authorizeHttpRequests().anyRequest().authenticated();
http.cors(cors -> {
if (allowedOrigins.isEmpty()) {
cors.disable();
} else {
cors.configurationSource(corsConfig(allowedOrigins));
}
});
return http.build();
}
CorsConfigurationSource corsConfig(List<String> allowedOrigins) {
final var source = new UrlBasedCorsConfigurationSource();
final var configuration = new CorsConfiguration();
configuration.setAllowedOrigins(allowedOrigins);
configuration.setAllowedMethods(List.of("*"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setExposedHeaders(List.of("*"));
source.registerCorsConfiguration("/**", configuration);
return source;
}
@RestController
@RequestMapping("/api/v1")
public static class GreetingController {
@GetMapping("/greet")
public ResponseEntity<GreetingDto> getGreeting(Authentication auth) {
return ResponseEntity.ok(new GreetingDto("Hello %s!".formatted(auth.getName())));
}
static record GreetingDto(String message) {
}
}
}
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://keycloak-url/realms/realm-id
resource-server:
cors:
allowed-origins:
- http://localhost:4200
oauth2LoginClient using authorization_code flow (gateway configured with TokenRelay filter or app with server-side rendered templates like Thymeleaf)
Complete tutorial.
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
@SpringBootApplication
public class ClientMinimalApplication {
public static void main(String[] args) {
SpringApplication.run(ClientMinimalApplication.class, args);
}
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
@Bean
SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception {
http.oauth2Login();
final var logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
http.logout(logout -> logout.logoutSuccessHandler(logoutSuccessHandler));
http.cors().disable();
// sessions are required (enabled by default)
// CSRF protection is required (enabled by default) because security is based on sessions
http.authorizeHttpRequests().requestMatchers("/login/**", "/oauth2/**").permitAll() // this is required for unauthenticated users to login
.anyRequest().authenticated();
return http.build();
}
@Controller
@RequestMapping
public static class UiController {
@GetMapping("/")
public RedirectView getIndex() throws URISyntaxException {
return new RedirectView("/ui/greet");
}
@GetMapping("/ui/greet")
public String getGreeting(Authentication auth, Model model) throws URISyntaxException {
model.addAttribute("message", "Hello %s!".formatted(auth.getName()));
return "greet";
}
}
}
spring:
security:
oauth2:
client:
provider:
keycloak-realm-id:
issuer-uri: http://keycloak-url/realms/realm-id
registration:
keycloak-confidential-user:
authorization-grant-type: authorization_code
client-name: a local Keycloak instance
client-id: client-ui
client-secret: ${keycloak-secret}
provider: keycloak-realm-id
scope: openid,profile,email,offline_access
/src/main/resources/templates/greet.html:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Greetings!</title>
</head>
<body>
<h1 th:utext="${message}">..!..</h1>
</body>
oauth2Login and oauth2ResourceServerIf your app exposes both a REST API to be consumed by an OAuth2 client and some other resources to be queried by a browser (without the help of an OAuth2 client lib for a framework like Angular, React or Vue), and only in that case, define the two different filter chains above and add a securityMatcher to the first in order to limit it to the routes it should secure.
In the samples provided above, the security filter-chain with the lowest order is the resource server one. Adding something like http.securityMatcher("/api/**"); would do the trick: all other routes will be secured with the client filter-chain which is tried after and will intercept all requests that were not already intercepted by the resource server filter chain.
Complete tutorial.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With