Spring-Security OAUTH2 and PKCE, IdentityServer4 How do I add code_challenge info - spring-boot

I am trying to access Identity Server. When I try to access I get an error that says
MessageTemplate: code_challenge is missing
I have a basic Spring Boot Application. How do I get the app to add in the Code Challenge and code Challenge type.
I have tried to add in this:
#Configuration
public class OAuth2ClientConfiguration {
#Bean
public SecurityWebFilterChain pkceFilterChain(ServerHttpSecurity http, ServerOAuth2AuthorizationRequestResolver resolver) {
http.authorizeExchange(r -> r.anyExchange().authenticated());
http.oauth2Login(auth -> auth.authorizationRequestResolver(resolver));
return http.build();
}
#Bean
public ServerOAuth2AuthorizationRequestResolver pkceResolver(ReactiveClientRegistrationRepository repo) {
DefaultServerOAuth2AuthorizationRequestResolver resolver = new DefaultServerOAuth2AuthorizationRequestResolver(repo);
resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
return resolver;
}
but that causes an error saying that
Description:
Parameter 0 of method pkceFilterChain in com.landstar.security.poc.securitypoc.config.OAuth2ClientConfiguration required a bean of type 'org.springframework.security.config.web.server.ServerHttpSecurity' that could not be found.
Action:
Consider defining a bean of type 'org.springframework.security.config.web.server.ServerHttpSecurity' in your configuration.
Additional Info
#Configuration
#EnableWebSecurity
public class SecurityConfig {
private static final String[] WHITE_LIST_URLS = {
"/user",
"/helloPublic",
"/register",
"/verifyRegistration*",
"/resendVerifyToken*"
};
#Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.authorizeHttpRequests()
.antMatchers(WHITE_LIST_URLS).permitAll()
.antMatchers("/api/**").authenticated()
.and()
.oauth2Login(oauth2login ->
oauth2login.loginPage("/auth2/authorization/landstar"))
.logout(l -> l
.logoutSuccessUrl("/").permitAll()
)
.oauth2Client(Customizer.withDefaults());
return http.build();
}
}
Please, any help.!!! thanks

Related

Spring Security in Spring Boot 3

I'm currently in the process of migrating our REST application from Spring Boot 2.7.5 to 3.0.0-RC2. I want everything to be secure apart from the Open API URL. In Spring Boot 2.7.5, we used to do this:
#Named
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/openapi/openapi.yml").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}
and it worked fine. In Spring Boot 3, I had to change it to
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests
.requestMatchers("/openapi/openapi.yml").permitAll()
.anyRequest()
.authenticated())
.httpBasic();
return http.build();
}
}
since WebSecurityConfigurerAdapter has been removed. It's not working though. The Open API URL is also secured via basic authentication. Have I made a mistake when upgrading the code or is that possibly an issue in Spring Boot 3 RC 2?
Update
Since most of the new API was already available in 2.7.5, I've updated our code in our 2.7.5 code base to the following:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests((requests) -> requests
.antMatchers(OPTIONS).permitAll() // allow CORS option calls for Swagger UI
.antMatchers("/openapi/openapi.yml").permitAll()
.anyRequest().authenticated())
.httpBasic();
return http.build();
}
}
In our branch for 3.0.0-RC2, the code is now as follows:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests((requests) -> requests
.requestMatchers(OPTIONS).permitAll() // allow CORS option calls for Swagger UI
.requestMatchers("/openapi/openapi.yml").permitAll()
.anyRequest().authenticated())
.httpBasic();
return http.build();
}
}
As you can see, the only difference is that I call requestMatchers instead of antMatchers. This method seems to have been renamed. The method antMatchers is no longer available. The end effect is still the same though. On our branch for 3.0.0-RC2, Spring Boot asks for basic authentication for the OpenAPI URL. Still works fine on 2.7.5.
Author: https://github.com/wilkinsona
#Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers(new AntPathRequestMatcher("/openapi/openapi.yml")).permitAll()
.anyRequest().authenticated())
.httpBasic();
return http.build();
}
Source: https://github.com/spring-projects/spring-boot/issues/33357#issuecomment-1327301183
I recommend you use Spring Boot 3.0.0 (GA) right now, not RC version.
Inside my WebSecurityConfig, I did this:
private static final String[] AUTH_WHITELIST = {
// -- Swagger UI v2
"/v2/api-docs",
"v2/api-docs",
"/swagger-resources",
"swagger-resources",
"/swagger-resources/**",
"swagger-resources/**",
"/configuration/ui",
"configuration/ui",
"/configuration/security",
"configuration/security",
"/swagger-ui.html",
"swagger-ui.html",
"webjars/**",
// -- Swagger UI v3
"/v3/api-docs/**",
"v3/api-docs/**",
"/swagger-ui/**",
"swagger-ui/**",
// CSA Controllers
"/csa/api/token",
// Actuators
"/actuator/**",
"/health/**"
};
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests( auth -> auth
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(withDefaults())
.addFilterBefore(authenticationJwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
//.addFilterAfter(authenticationJwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
#Bean
public SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests((requests) -> requests
.requestMatchers( new AntPathRequestMatcher("swagger-ui/**")).permitAll()
.requestMatchers( new AntPathRequestMatcher("/swagger-ui/**")).permitAll()
.requestMatchers( new AntPathRequestMatcher("v3/api-docs/**")).permitAll()
.requestMatchers( new AntPathRequestMatcher("/v3/api-docs/**")).permitAll()
.anyRequest().authenticated())
.httpBasic();
return httpSecurity.build();
}
This and using Dockerfile (doing mvn clean package and running .jar from Docker) made me had no issues with authentication inside swagger ui.
Hope this can help you :)
Use
http.securityMatcher("<patterns>")...
to specify authentication for endpoints.
authorizeHttpRequests((requests) -> requests
.requestMatchers("<pattern>")
only works for authorization, if you don't set securityMatcher , SecurityFilterChain by default gets any request for authentication. And any request will be authenticated by an authentication provider.
In your case, you can define two security filter, chains: one for public endpoitns, another for secured. And give them proper order:
#Bean
#Order(1)
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.securityMatcher(OPTIONS,"/openapi/openapi.yml").csrf().disable()
.authorizeHttpRequests((requests) -> requests
.anyRequest().permitAll() // allow CORS option calls for Swagger UI
);
return http.build();
}
#Bean
Order(2)
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.securityMatcher("/**")
.csrf().disable()
.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated())
.httpBasic();
return http.build();
}
The official documentation suggests an example which I have abridged here with your config:
http
.authorizeExchange((exchanges) ->
exchanges
.pathMatchers("/openapi/openapi.yml").permitAll()
.anyExchange().authenticated())
.httpBasic();
return http.build();
You could try this, since it changes the "request" for the "exchange" wording, in line with the migration to declarative clients (#PostExchange vs. #PostMapping) I suppose. Hope it helps.
My security cfg looks like:
Spring 3.0.0
#Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(requests -> requests
.requestMatchers(HttpMethod.GET, "/", "/static/**", "/index.html", "/api/users/me").permitAll()
.requestMatchers(HttpMethod.POST, "/api/users").permitAll()
.requestMatchers(HttpMethod.GET, "/api/users/login", "/api/users/{username}", "/api/users/logout", "/api/costumers", "/api/storages").authenticated()
.requestMatchers(HttpMethod.POST, "/api/costumers", "/api/storages").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/costumers/{id}", "/api/storages/{id}").authenticated()
.requestMatchers(HttpMethod.DELETE, "/api/users/{id}", "/api/storages/{id}", "/api/costumers/{id}").authenticated()
.anyRequest().denyAll())
.httpBasic();
return http.build();
}
it works
This seems to be a bug in Spring Boot 3. I've raised an issue.

Spring Security not working on any endpoint with multiple WebSecurityConfigurerAdapter implementations

I'm trying to setup Spring Security in my application, which has 3 components:
REST API (under v1 path)
Spring Admin & actuator (under /admin path)
Docs (under /docs and /swagger-ui paths)
I want to setup security like this:
REST API secured with JWT token
Admin secured with HTTP basic
Docs unsecured (public resource)
I've tried to configure authentication for those 3 parts in separate implementations of WebSecurityConfigurerAdapter, and the result looks like this:
For REST API:
#Configuration
#EnableWebSecurity
#Order(1)
class ApiWebSecurityConfig : WebSecurityConfigurerAdapter() {
// FIXME: Temporary override to disable auth
public override fun configure(http: HttpSecurity) {
http
.antMatcher("/v1/*")
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.csrf().disable()
}
}
For Spring Admin:
#Configuration
#EnableWebSecurity
#Order(2)
class AdminWebSecurityConfig(
private val adminServerProperties: AdminServerProperties
) : WebSecurityConfigurerAdapter() {
public override fun configure(http: HttpSecurity) {
http.antMatcher("${adminServerProperties.contextPath}/**")
.authorizeRequests()
.antMatchers("${adminServerProperties.contextPath}/assets/**").permitAll()
.antMatchers("${adminServerProperties.contextPath}/login").permitAll()
.anyRequest()
.authenticated()
.and()
.cors()
.and()
.formLogin()
.loginPage("${adminServerProperties.contextPath}/login")
.and()
.logout()
.logoutUrl("${adminServerProperties.contextPath}/logout")
.and()
.httpBasic()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers(
AntPathRequestMatcher("${adminServerProperties.contextPath}/instances", HttpMethod.POST.toString()),
AntPathRequestMatcher("${adminServerProperties.contextPath}/instances/*", HttpMethod.DELETE.toString()),
AntPathRequestMatcher("${adminServerProperties.contextPath}/actuator/**")
)
}
#Bean
fun corsConfigurationSource(): CorsConfigurationSource = UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", CorsConfiguration().apply {
allowedOrigins = listOf("*")
allowedMethods = listOf("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH")
allowCredentials = true
allowedHeaders = listOf("Authorization", "Cache-Control", "Content-Type")
})
}
}
And for public docs:
#Configuration
#EnableWebSecurity
#Order(3)
class DocsWebSecurityConfig : WebSecurityConfigurerAdapter() {
public override fun configure(http: HttpSecurity) {
http
.requestMatchers()
.antMatchers("/swagger-ui/**", "/docs/**", "/docs-oas3/**")
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
}
}
And my main application class looks like this:
#SpringBootApplication
#EnableAdminServer
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#EnableConfigurationProperties(FirebaseConfigurationProperties::class, JwtConfigurationProperties::class)
class HldpDeviceManagementApplication
fun main(args: Array<String>) {
runApplication<HldpDeviceManagementApplication>(*args)
}
When I run the application, there's no error or any security information, besides this log output:
Will not secure Ant [pattern='/v1/**']
Will not secure Ant [pattern='/admin/**']
Will not secure Or [Ant [pattern='/swagger-ui/**'], Ant [pattern='/docs/**'], Ant [pattern='/docs-oas3/**']]
Any suggestion why doesn't the configuration work? Or maybe another way I can secure the application like this? I've tried doing a few changes in the configuration, but nothing seems to help.
I've found the problem - it's a bug in the latest version of Spring:
https://github.com/spring-projects/spring-security/issues/10909
You didn't mention when it doesn't work, is it when you make a request, or on application startup? However, I can help you with your configuration and get the information needed to solve the problem.
I'll try to simplify your configuration with the new way to configure HttpSecurity, by exposing a SecurityFilterChain bean.
#Configuration
public class SecurityConfiguration {
#Bean
#Order(0)
public SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.requestMatchers(requests -> requests.antMatchers("/v1/**"))
...
// the rest of the configuration
return http.build();
}
#Bean
#Order(1)
public SecurityFilterChain admin(HttpSecurity http) throws Exception {
http
.requestMatchers(requests -> requests.antMatchers("/admin/**"))
...
// the rest of the configuration
return http.build();
}
#Bean
#Order(2)
public SecurityFilterChain docs(HttpSecurity http) throws Exception {
http
.requestMatchers(requests -> requests.antMatchers("/docs/**"))
...
// the rest of the configuration
return http.build();
}
}
This is in Java, but you can adapt to Kotlin easily, I'm sorry to not provide it in Kotlin already. With this simplified configuration, now you can add logging.level.org.springframework.security=TRACE to your application.properties file and check what Spring Security is doing by reading the logs.

Springboot with Keycloak always return 403

I have created a Springboot application with Keycloak by following this tutorial Baeldung
When I try to enter /api/foos it always returns 403 without any error messages.
// SecurityConfig
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
class SecurityConfig(
private val unauthorizedHandler: JwtAuthenticationEntryPoint
) : WebSecurityConfigurerAdapter() {
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.cors()
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**", "/api/foos")
.hasAnyRole("free_user")
.antMatchers(HttpMethod.POST, "/api/foos")
.hasAnyRole("free_user")
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt()
}
}
// Controller
#RestController
#RequestMapping(value = ["/api/foos"])
class SecurityTestController() {
#GetMapping(value = ["/{id}"])
fun findOne(#PathVariable id: Long?): String {
return "fineOne with id $id"
}
#GetMapping
fun findAll(): Message {
return Message("findAll")
}
}
// application.properties
# Resource server config
rest.security.issuer-uri=http://localhost:8081/auth/realms/dev
spring.security.oauth2.resourceserver.jwt.issuer-uri=${rest.security.issuer-uri}
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${rest.security.issuer-uri}/protocol/openid-connect/certs
// build.gradle (app)
plugins {
id("org.springframework.boot")
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter- oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.security:spring-security-test")
}
Here is my user on Keycloak
Postman result:
What I have already tried
disable csrf - Not working
Comment SecurityConfig class - Working without security
It looks like "free_user" is a client role.
Two solutions :
Either remove the current "free_user" client role, create a "free_user" realm role in the global Roles tab, and assign it to your user.
Or, add the property keycloak.use-resource-role-mappings=true to your Spring boot configuration to allow spring security to map your current client role.
I found the answer here LINK
We need to create a custom mapping between Spring security and Keycloak roles.
public class KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
#Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
final Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access");
return ((List<String>)realmAccess.get("roles")).stream()
.map(roleName -> "ROLE_" + roleName) // prefix to map to a Spring Security "role"
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}

Spring Boot Authorization Basic Header Never Changes

I am attempting to setup a very basic spring boot authenticated application. I am setting the Authorization header in the client and sending it to the backend. I can verify that the client is sending the correct header.
The backend receives the header correctly on the first attempt to login. However if the login credentials are incorrect subsequent requests retain whatever the header for the intial request was (caching it or something).
I am using Redis to Cache the session. My config is as follows:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired AuthenticationEntryPoint authenticationEntryPoint;
#Autowired CsrfTokenRepository csrfTokenRepository;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf()
.disable()
.authorizeRequests().antMatchers("**")
.permitAll()
.anyRequest()
.authenticated()
;
}
}
AuthenticationEntryPoint
public class AuthenticationEntryPointBean {
#Bean
AuthenticationEntryPoint authenticationEntryPoint() {
return new RestAuthenticationEntryPoint();
}
}
Any direction would be appreciated.
** Edit **
Adding cache settings
#Configuration
#EnableRedisHttpSession
public class HttpSessionConfig {
#Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory(); // <2>
}
}
Also I am trying to invalidate cache but that doesn't seem to work
#CrossOrigin
#RequestMapping(value="/auth/login", method = RequestMethod.GET, produces="application/json")
public #ResponseBody String login(#RequestHeader(name = "authorization") String authorization, HttpSession session, HttpServletRequest request)
{
try
{
authorization = authorization.substring("Basic ".length());
String decoded = new String(Base64.getDecoder().decode(authorization),"UTF-8");
Gson gson = new Gson();
LoginRequest login = gson.fromJson(decoded,LoginRequest.class);
UserAuthenticationEntity entity = service.getSecurityContext(login).orElseThrow(() ->
new BadCredentialsException("Authentication Failed.")
);
session.setMaxInactiveInterval((int)TimeUnit.MINUTES.toSeconds(expiresInMinutes));
SecurityContextHolder.getContext().setAuthentication(new EntityContext(entity,expiresInMinutes));
String response = gson.toJson(BasicResponse.SUCCESS);
return response;
}
catch (Exception e)
{
session.invalidate();
e.printStackTrace();
throw new AuthenticationCredentialsNotFoundException("Authentication Error");
}
}
Adding the following to my web security config seemed to do the trick.
.requestCache()
.requestCache(new NullRequestCache())
I am not sure what side effects are of doing this. I picked it up off of a blog https://drissamri.be/blog/2015/05/21/spring-security-and-spring-session/
If there is any more insight into if this is good practice or bad practice I would appreciate any comments.
My final web security config looks like the following:
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf()
.disable()
.authorizeRequests().antMatchers("**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.requestCache()
.requestCache(new NullRequestCache())
.and()
.sessionManagement()
.sessionFixation()
.newSession()
;

Spring security AccessDecisionManager: roleVoter, Acl Voter

I'm trying to setup a Spring Security 3.2 project using Java Config and no XML at all.
I want to have an Access decision voter that supports both RoleHierarchyVoter and AclEntryVoters. This is the configuration I'm using:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AclEntryVoter aclUpdatePropertyVoter;
#Autowired
private AclEntryVoter aclDeletePropertyVoter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.and()
.logout()
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/")
.and()
.authorizeRequests()
.accessDecisionManager(accessDecisionManager())
.antMatchers("/login", "/signup/email", "/logout", "/search", "/").permitAll()
.anyRequest().authenticated();
}
#Bean
public RoleHierarchyVoter roleVoter() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_USER > ROLE_ANONYMOUS");
RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy);
return roleHierarchyVoter;
}
#Bean
public AffirmativeBased accessDecisionManager() {
List<AccessDecisionVoter> decisionVoters = new ArrayList<>();
WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
decisionVoters.add(webExpressionVoter);
decisionVoters.add(roleVoter());
decisionVoters.add(aclDeletePropertyVoter);
decisionVoters.add(aclUpdatePropertyVoter);
AffirmativeBased affirmativeBased = new AffirmativeBased(decisionVoters);
return affirmativeBased;
}
}
However, when the app gets initialized I get the following exception:
I get the exception:
java.lang.IllegalArgumentException: AccessDecisionManager does not support secure object class: class org.springframework.security.web.FilterInvocation
When debugging the code I can see that when AbstractAccessDecisionManager is called and the following code is executed:
public boolean supports(Class<?> clazz) {
for (AccessDecisionVoter voter : this.decisionVoters) {
if (!voter.supports(clazz)) {
return false;
}
}
return true;
}
RoleHierarchyVoter support FilterInvocation, however AclEntryVoters fail to pass it. What I'm doing wrong in the configuration? How can I set the project so that it supports both types of voters? Thanks a lot in advance
As you've observed, the acl voters don't support filter invocations as they are intended for checking secured methods, not web requests.
You should configure a separate AccessDecisionManager for use with your method security and add the acl voters to that.

Resources