spring boot rest app secured by keycloak 18 return always error 401 Unauthorized - spring

I'm developing microservices some of them with spring boot web and other with spring boot data rest and I want secure them with keycloak server 18 through OpenId protocol. The microservices endpoint can be accessed from frontend adding in the "Authorization header" the bearer token obtained from post request to the url http://localhost:8280/auth/realms/--realm_name--/.well-known/openid-configuration inserting in the body request the key client_id, username, password, grant_type, client_secret.
I have create
1.a realm,
2.a client named "springboot-mc-dev" (with Access Type = confidential;,
"Root URL" and "Valid Redirect URIs" both setted to "http://localhost:8490",
"Standard Flow Enabled", "Direct Access Grants Enabled", "Service Accounts Enabled" and "Authorization Enabled" setted to "ON"),
3.before a role inside client ("named springboot-mc-dev-role" composite False) and after a role inside realm (named always "springboot-mc-dev-role" composite true that is associated Client Roles "springboot-mc-dev-role" of client springboot-mc-dev),
4.user mapped to role "springboot-mc-dev-role" in "realm roles"
After I have imported the following dependency in parent pom.xml
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java-version>1.8</java-version>
<keycloak.version>18.0.2</keycloak.version>
</properties>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>${keycloak.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Here there is the code of SecurityConfig.java class
package it.organization.project.microservice.datamart.config.security;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
#KeycloakConfiguration
#Configuration
#EnableWebSecurity
#Import(KeycloakSpringBootConfigResolver.class)
#EnableGlobalMethodSecurity(jsr250Enabled = true)
#ConditionalOnProperty(name = "ms-security-enable", havingValue = "true")
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper();
grantedAuthorityMapper.setPrefix("ROLE_");
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
//keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper);
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
#Override
/*protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}*/
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeRequests()
.antMatchers("/**").hasRole("springboot-mc-dev-role")
//.anyRequest().hasRole("springboot-mc-dev-role")
//.anyRequest().//authenticated()
//permitAll()
;
http.csrf().disable();
}
#Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
//public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
this is the code of main class
package it.organization.project.microservice.datamart;
import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
#SpringBootApplication
#EnableCaching
public class MAINDataMartApplication {
public static void main(String[] args) {
Security.addProvider(new BouncyCastleProvider());
SpringApplication.run(MAINDataMartApplication.class, args);
}
}
and last there is the application.yml with keycloak settings
#KEYCLOAK CONFIGURATION
keycloak:
auth-server-url: http://localhost:8280/auth
#ssl-required: external
realm: realmname
bearer-only: true
#public-client: true
use-resource-role-mappings: true
resource: springboot-mc-dev
credentials:
secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
this is the result
What is wrong?

Keycloak spring adapter is deprecated. Don't use it.
Have look at those tutorials for alternatives.
We are missing important info to answer preecisely:
Spring versions?
reason for 401 (from Postman console, value of WWW-Authenticate response header)?
There can be few reasons for 401 Unauthorized:
missing authorization header
expired or not yet valid token (for instance because of timezone misconfiguration on either authorization or resource server)
different issuer in access-token claim and Spring config: host, port, etc must be exactly the same
Spring configured for a different type of authentication than what authorization server supports (i.e. opaque token with introspaction and one side and JWT on the other)
...
If you're using Keycloak 18 with Quarkus, it is likely that your conf should reference http://localhost:8280 as issuer (and not http://localhost:8280/auth).
So, look at Postman console, open your access-token with a tool like https://jwt.io and compare values you find there with what you have in spring conf and logs.

Related

Spring Security 6 POST requests are unauthorised with permitAll()

I am using Spring Boot 3, Spring Security 6. My Security configuration doesn't work properly. I have 2 paths on which any request should be permitted, and for everything else one needs to authenticate.
Both GET and POST method work on those that need authentication.
On those with permitAll(), only GET requests work. For POST, I get 401 Unauthorised.
I took care of CSRF, and anyway I expect all the POST requests to work, not only those with authentication.
On Postman, I selected POST, No Auth, put a RAW body and selected JSON. I really don't know why is it not working.
Here is my code:
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, KeycloakLogoutHandler keycloakLogoutHandler) throws Exception {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
delegate.setCsrfRequestAttributeName("_csrf");
// Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
// default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
CsrfTokenRequestHandler requestHandler = delegate::handle;
http
.authorizeHttpRequests().requestMatchers("/firstpath/**", "/secondpath/**", "/error/**").permitAll().and()
.authorizeHttpRequests().anyRequest().authenticated().and()
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
http.oauth2Login()
.and()
.logout()
.addLogoutHandler(keycloakLogoutHandler)
.logoutSuccessUrl("/");
http.csrf((csrf) -> csrf
.csrfTokenRepository(tokenRepository)
.csrfTokenRequestHandler(requestHandler));
return http.build();
}
}
#Slf4j
#RestController
#RequestMapping("/firstpath")
public class NameitController {
#PostMapping(value = "path", produces = WSConstants.JSON_MEDIATYPE)
#ResponseBody
public ResponseEntity saveMyObject(#RequestBody ObjectDTO dto) {
[...] //my code
}
}
I also tried http.authorizeHttpRequests().requestMatchers(HttpMethod.POST, "/firstpath/path").permitAll(), but at no use.
Edit: It still has to do with CSRF protection, because when I tired http.csrf().disable();, everything worked fine. But I still want CSRF protection, it seems like the token is not sent with permitAll()?...
Edit2: After adding Spring Security logs:
In your postman, I do not see X-XSRF-TOKEN header. If you’re not sending the XSRF-Token back to the server after fetching it from the cookie, you might wanna do it as shown in the end of the answer since it is one of the ways it is designed to protect against CSRF attacks and only works like that. In frameworks like angular, we can get it as a cookie from spring boot server and send it back as a header to differentiate malicious sites accessing the same URL, since such sites, inside the browser, cannot access the cookie associated with our genuine domain to send it back as a header.
Here is a simple working project which uses spring security 6 and crsf token with postman testing if it can help. It uses InMemoryUserDetailsManager, NoOpPasswordEncoder(Not recommended for production) and basic authentication.
SecurityConfig:
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
#Configuration
public class ProjectSecurityConfig {
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
delegate.setCsrfRequestAttributeName("_csrf");
CsrfTokenRequestHandler requestHandler = new CsrfTokenRequestHandler() {
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
Supplier<CsrfToken> csrfToken) {
delegate.handle(request, response, csrfToken);
}
};
return http
.cors().disable() // disabled cors for simplicity in this example in case of testing through a ui
.authorizeHttpRequests()
.requestMatchers("/error").permitAll()
.anyRequest().authenticated()
.and()
.csrf()
.csrfTokenRequestHandler(requestHandler)
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and().formLogin()
.and().httpBasic()
.and().build();
}
#Bean
InMemoryUserDetailsManager userDetailsService() {
UserDetails admin = User.withUsername("admin").password("pass").authorities("admin").build();
return new InMemoryUserDetailsManager(admin);
}
#Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
Controller:
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.learning.entity.DataObject;
#RestController
public class TestController {
#PostMapping("/post")
public String post(#RequestBody DataObject dataObject) {
return "succesfull post";
}
}
DataObject Model:
public class DataObject {
private String data;
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
application.properties:
logging.level.org.springframework.security.web.csrf=TRACE
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.learning</groupId>
<artifactId>spring-security-3-csrf-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security-3-csrf-example</name>
<description>spring learning</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Test using CSRF token in postman:
First add basic auth credentials-
Add data object json body-
Send a mock request to the server to get a XSRF Cookie
Use this Cookie value as a header with name "X-XSRF-TOKEN"-
Testing it-
Note:- Since version 6, Spring Security does not create sessions for basic authentication by default so no Cookie for session will be returned in this example.
UPDATE :-
Here is an article on a more sophisticated way to send XSRF-TOKEN through postman as pointed by #OctaviaAdler in the comments. TLDR in case the link goes down:- Create an environment in postman and add the variable "xsrf-token" in it. Inside the request, add the header X-XSRF-TOKEN with the value set to "{{xsrf-token}}" (name of the environment variable in double curly braces without quotes). Then add the following script inside the "Tests" tab -
var xsrfCookie = postman.getResponseCookie("XSRF-TOKEN");
postman.setEnvironmentVariable("xsrf-token", xsrfCookie.value);
THe order in which you define in security config please have a look order like this
`#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/auth/**")
.permitAll()
.antMatchers("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.anyRequest()
.authenticated()
.and()
.cors()
.and()
.exceptionHandling()
.authenticationEntryPoint(this.jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf()
.disable();
// Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}`

401 sent to angular frontend by spring boot backend after successful oauth2 authetication with google

Spring Boot rest api in the back and angular in the front.
Hi all, I have a problem after successful oauth2 authentication with google.
In srping boot debug I can read the following:
o.s.web.cors.DefaultCorsProcessor : Skip: response already contains "Access-Control-Allow-Origin".
Then a 401 is sent to angular with full authentication required to access /api/user/ resource which is the root to access user details in the backend side.
WebConfig.java
import java.util.Locale;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
#Configuration
public class WebConfig implements WebMvcConfigurer {
private final long MAX_AGE_SECS = 3600;
#Override
public void addCorsMappings(CorsRegistry registry) {
registry
.addMapping("/**")
.allowedOrigins("*")
.allowedMethods(
"HEAD",
"OPTIONS",
"GET",
"POST",
"PUT",
"PATCH",
"DELETE"
)
.maxAge(MAX_AGE_SECS);
}
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
#Bean
public LocaleResolver localeResolver() {
final CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
cookieLocaleResolver.setDefaultLocale(Locale.ENGLISH);
return cookieLocaleResolver;
}
#Override
public Validator getValidator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource());
return validator;
}
}
SecurityConfig.java
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.client.RestTemplate;
import com.springboot.dashboard.security.jwt.TokenAuthenticationFilter;
import com.springboot.dashboard.security.oauth2.DashBoardOAuth2UserService;
import com.springboot.dashboard.security.oauth2.DashBoardOidcUserService;
import com.springboot.dashboard.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository;
import com.springboot.dashboard.security.oauth2.OAuth2AccessTokenResponseConverterWithDefaults;
import com.springboot.dashboard.security.oauth2.OAuth2AuthenticationFailureHandler;
import com.springboot.dashboard.security.oauth2.OAuth2AuthenticationSuccessHandler;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private DashBoardOAuth2UserService dashBoardOAuth2UserService;
#Autowired
private DashBoardOidcUserService dashBoardOidcUserService;
#Autowired
private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
#Autowired
private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf()
.disable()
.formLogin()
.disable()
.httpBasic()
.disable()
.exceptionHandling()
.authenticationEntryPoint(new RestAuthenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers("/", "/error", "/api/all", "/api/auth/**", "/oauth2/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.authorizationEndpoint()
.authorizationRequestRepository(cookieAuthorizationRequestRepository())
.and()
.redirectionEndpoint()
.and()
.userInfoEndpoint()
.oidcUserService(dashBoardOidcUserService)
.userService(dashBoardOAuth2UserService)
.and()
.tokenEndpoint()
.accessTokenResponseClient(authorizationCodeTokenResponseClient())
.and()
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler);
// Add our custom Token based authentication filter
http.addFilterBefore(
tokenAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class
);
}
#Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter();
}
/*
* By default, Spring OAuth2 uses
* HttpSessionOAuth2AuthorizationRequestRepository to save the authorization
* request. But, since our service is stateless, we can't save it in the
* session. We'll save the request in a Base64 encoded cookie instead.
*/
#Bean
public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
return new HttpCookieOAuth2AuthorizationRequestRepository();
}
// This bean is load the user specific data when form login is used.
#Override
public UserDetailsService userDetailsService() {
return userDetailsService;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
#Bean(BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient() {
OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
tokenResponseHttpMessageConverter.setTokenResponseConverter(
new OAuth2AccessTokenResponseConverterWithDefaults()
);
RestTemplate restTemplate = new RestTemplate(
Arrays.asList(
new FormHttpMessageConverter(),
tokenResponseHttpMessageConverter
)
);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
tokenResponseClient.setRestOperations(restTemplate);
return tokenResponseClient;
}
}
Thanks in advance for your help.
oauth2 login successful cause user data is successfully to database, but can access to full authentication resource.
WebSecurityConfigurerAdapter is deprecated, don't use it (it is not even there any more in spring-boot 3). Expose a SecurityFilterChain bean instead:
#Bean
SecurityFilterChain filterChain(HttpSecurity http) {
// http configuration
return http.build();
}
Resource-server (REST API)
Instead of writing all of spring-boot-starter-oauth2-resource-server security configuration in java #Configuration (CSRF, CORS, JWT decoder or token introspector, authorities mapping, public routes), you can use one of the spring-boot starters here:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<!-- replace "webmvc" with "weblux" if your app is reactive -->
<!-- replace "jwt" with "introspecting" to use token introspection instead of JWT decoding -->
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<!-- this version is to be used with spring-boot 3.0.0-RC2, use 5.x for spring-boot 2.6.x or before -->
<version>6.0.5</version>
</dependency>
#EnableMethodSecurity
public static class WebSecurityConfig { }
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,ressource_access.client-id.roles
com.c4-soft.springaddons.security.cors[0].path=/**
Client (Angular app)
Use an OAuth2 client library. My favorite for Angular is angular-auth-oidc-client. It will save you tones of efforts to:
redirect users to authorisation-server for login
handle redirect back from authorization-server with authorization-code
exchange authorization-code for tokens (access-token of course, but also refresh and ID tokens if you requested offline_access and openid scopes)
auto-refresh access tokens before it expires (if you got a refresh-token)
automatically authorize request matching configured patterns (add Authorization Bearer header with access-token)
provide with Angular route guards
...
Authorization-server
Unless you deploy your resource-server to Google cloud, it is likely it can't use Google authorization-server directly. You might use an other authorization-server capable of identity federation in front of it. Keycloak does it pretty well:
run a Keycloak instance and configure it with Google as identity provider
configure your resource-server to use Keycloak as authorization-server (as done in code above)
configure Angular to use Keycloak as authorization-server too
I refer to Keycloak here, but most serious OIDC providers (either on premize or SaaS like Auth0, Okta, etc.) support "social" login and will allow Google users to login (as well as Facebook, Github, Tweeter, etc.)

How single authentication can work for multiple server nodes in Spring Security

I am using weblogic for deploying my spring boot application, and my same application is deployed on multiple nodes.
For example the two node in which the application is deployed is 9001 and 9002.
With basic security even if I am authenticated on the Node 9001 and trying to access the same URL on second node i.e on 9002, I am again getting redirected again to spring login page for authentication.
I want that once I authenticate using username and password on any node. I need not to authenticate again, Even if I am requesting to any other node.
Any kind of clue or help will be appreciated.
Thanks in advance.
The Security configuration file is
package com.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("test")
.password("{noop}test")
.authorities("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/userdetail").authenticated()
.anyRequest().permitAll()
.and()
.formLogin();
}
}
In my case it worked for both node when I enabled RedisHttpSession.
Below is the code which worked for me.
#Configuration
#EnableRedisHttpSession
public class RedisConfig {
#Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
}
also in pom.xml I needed to make two dependencies(For Spring boot).
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
You can also take reference about EnableRedisHttpSession from spring docs, and about spring session from
https://docs.spring.io/spring-session/docs/current/api/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.html
https://www.baeldung.com/spring-session

How use Spring Boot oAuth2 + Azure AD in a stateless micro-service?

When I try the sample code from Microsoft Azure to use oAuth2 and Spring Boot, it uses a stateful session to check authentication/authorization. You can see this in that:
It never passes any headers/JWT on any calls
It has a cookie "JSESSIONID" that you can use in new postman session (after obtaining it in a different browser) and it will consider you logged in
This will not work as our micro-services will be multiple instances.
How could I convert this to use a JWT (Authorization: Bearer AQab...) for subsequent calls instead of the cookie?
Dependencies:
//All using Spring Boot 2.0.5.RELEASE
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-webflux')
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.security:spring-security-oauth2-client')
compile('org.springframework.security:spring-security-oauth2-jose')
//Using 2.0.7
compile('com.microsoft.azure:azure-active-directory-spring-boot-starter')
Config:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
#EnableWebSecurity
#EnableGlobalMethodSecurity( prePostEnabled = true )
public class OAuthConfig extends WebSecurityConfigurerAdapter
{
#Autowired
private OAuth2UserService<OidcUserRequest, OidcUser> userService;
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService( userService );
}
}
Controller:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
#Controller
public class MainController
{
private OAuth2AuthorizedClientService clientService;
#Autowired
public MainController(OAuth2AuthorizedClientService clientService)
{
this.clientService = clientService;
}
#GetMapping( "checkrole" )
#ResponseBody
#PreAuthorize( "hasRole('ROLE__Test')" )
public String group1()
{
return "ok";
}
#GetMapping( "/" )
#ResponseBody
public String getUser(OAuth2AuthenticationToken userToken)
{
//Printing out the oAuth token just for testing
return clientService.loadAuthorizedClient(
userToken.getAuthorizedClientRegistrationId(),
userToken.getName()
).getAccessToken().getTokenValue();
}
}
application.yml:
spring:
security:
oauth2:
client:
registration:
azure:
client-id: ${YOUR_CLIENT_ID:}
client-secret: ${YOUR_CLIENT_SECRET:}
azure:
activedirectory:
tenant-id: ${YOUR_TENANT_OR_DIRECTORY_ID:}
active-directory-groups: Test
Complete Sample Code
https://github.com/Microsoft/azure-spring-boot/tree/master/azure-spring-boot-samples/azure-active-directory-spring-boot-backend-sample

AWS Cognito Angular SpringBoot Oauth2 - invalid_token error

we are using AWS Cognito for Oauth2. Our UI is built on Angular. After my user logs in, I initiate a call to Cognito to get Authorization token. I am using Authorization Code Grant with PKCE for getting the token from Cognito. After getting the toke from Cognito, I call my Spring Boot REST service. When calling Spring Boot service from Angular, I send the token in the Authorization header as "Bearer" token.
Here is my ResourceServerConfiguration.java:
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "resource-server-rest-api";
private static final String SECURED_READ_SCOPE = "#oauth2.hasScope('openid')";
private static final String SECURED_WRITE_SCOPE = "#oauth2.hasScope('openid')";
private static final String SECURED_PATTERN = "/**";
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers(SECURED_PATTERN).and().authorizeRequests()
.antMatchers(HttpMethod.POST, SECURED_PATTERN).access(SECURED_WRITE_SCOPE)
.anyRequest().access(SECURED_READ_SCOPE);
}
}
When calling the REST service my Angular UI gets HTTP Response 401 with the following error message:
DEBUG o.s.s.o.p.a.OAuth2AuthenticationProcessingFilter - Authentication request failed: error="invalid_token", error_description="Invalid access token: eyJraWQiOiIy.......
Any idea why I am getting invalid_token?
Does spring makes a call to Cognito to verify the token?
I am NOT storing the token in my REST servic layer. Is this required?
I enabled DEBUG using logging.level.root=DEBUG. But I do not see descriptive message in the output. How do i trouble shoot this issue?
The below changes worked for me
Remove #EnableResourceServer
Add below to your spring security configuration
http.authorizeRequests().antMatchers(HttpMethod.OPTIONS,"**").permitAll()
.anyRequest()
.authenticated()
.and().oauth2ResourceServer().jwt();
adding below 2 dependency to pom.xml
<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>
finally add following properties to your application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://cognito-idp.us-east-1.amazonaws.com/{{userpoolid}}
com:
ixortalk:
security:
jwt:
aws:
userPoolId: {{userpoolid}}
region: "us-east-1"

Resources