automate the OAuth2 refresh_token process with SpringBoot 2 - spring-boot

I have a SpringBoot2 application, a MainApp as a resource-server, KeyCloak as AuthorizationServer and a maven module, which is related to the MainApp, as a OAuth2LoginClient.
In other words, in MavenModule I have the follow SecurityConfig:
#Configuration
#PropertySource("classpath:idm.properties")
public class Auth0Provider extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.requestMatchers(PROTECTED_URLS).authenticated()
.anyRequest().authenticated()
)
.oauth2Login().redirectionEndpoint().baseUri("/callback*");
http.csrf().disable();
}
private static final RequestMatcher PROTECTED_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/idmauth/**")
);
}
There is also a controller that intercepts the protected call:
#Value("${oauth.redirectURL}")
private String redirectURL;
#Autowired
private OAuth2AuthorizedClientService clientService;
#RequestMapping(method = RequestMethod.GET, path = "/redirect")
public RedirectView redirectWithUsingRedirectView(OAuth2AuthenticationToken oauthToken, RedirectAttributes attributes) {
OAuth2AuthorizedClient client =
clientService.loadAuthorizedClient(
oauthToken.getAuthorizedClientRegistrationId(),
oauthToken.getName());
String token = client.getAccessToken().getTokenValue();
attributes.addAttribute("jwt", token);
return new RedirectView(redirectURL);
}
This return the AccessToken to my frontend. Clearly in my idm.properties file I have the spring.oauth2.client.provider and spring.oauth2.client.registration info.
Now the MainApp is a SpringBoot2 WebApp with this simple SecurityConfig:
#EnableWebSecurity
#Configuration
public class Oauth2RestApiSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.requestMatchers().antMatchers("/api/**")
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
}
}
And in it's application.properties just the line:
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://<host>/protocol/openid-connect/certs
All works fine but, when the token expire, the only way I have currently found to refresh my token
is to manually do this HTTP-POST:
POST /auth/realms/<audience>/protocol/openid-connect/token HTTP/1.1
Host: <host>
Content-Type: application/x-www-form-urlencoded
Content-Length: 844
client_id=<my_client_id>
&client_secret=<my_client_secret>
&refresh_token=<refresh_token_previously_obtained>
&grant_type=refresh_token
Is there a better way to do this? Maybe inside the SecurityConfig or with a specific path inside spring.oauth2.x properties?

Note that refreshing an access token is done on the OAuth 2.0 client side.
This is done automatically by Spring Security if you have configured a WebClient to be used when requesting protected resources.
#Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
When you have done so, the expired OAuth2AccessToken will be refreshed (or renewed) if an OAuth2AuthorizedClientProvider is available to perform the authorization.

Related

Spring Security with OAuth2(Keycloak) disable default login page

I have successfully configured Spring Boot Spring Security with Keycloak. Everything works fine. In order to login, I use the following URL: http://localhost:8081/realms/MY_REALM_NAME
But when I try to access the following page: http://localhost:8080/login I see the following page:
I'd like to disable/remove this page. How to properly configure it with Spring Security?
UPDATED
My SpringSecurity configuration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends VaadinWebSecurityConfigurerAdapter {
private final ClientRegistrationRepository clientRegistrationRepository;
private final GrantedAuthoritiesMapper authoritiesMapper;
private final ProfileService profileService;
SecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository,
GrantedAuthoritiesMapper authoritiesMapper, ProfileService profileService) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.authoritiesMapper = authoritiesMapper;
this.profileService = profileService;
SecurityContextHolder.setStrategyName(VaadinAwareSecurityContextHolderStrategy.class.getName());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
// Enable OAuth2 login
.oauth2Login(oauth2Login ->
oauth2Login
.clientRegistrationRepository(clientRegistrationRepository)
.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint
// Use a custom authorities mapper to get the roles from the identity provider into the Authentication token
.userAuthoritiesMapper(authoritiesMapper)
)
// Use a Vaadin aware authentication success handler
.successHandler(new KeycloakVaadinAuthenticationSuccessHandler(profileService))
)
// Configure logout
.logout(logout ->
logout
// Enable OIDC logout (requires that we use the 'openid' scope when authenticating)
.logoutSuccessHandler(logoutSuccessHandler())
// When CSRF is enabled, the logout URL normally requires a POST request with the CSRF
// token attached. This makes it difficult to perform a logout from within a Vaadin
// application (since Vaadin uses its own CSRF tokens). By changing the logout endpoint
// to accept GET requests, we can redirect to the logout URL from within Vaadin.
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
);
}
#Bean
#Primary
public SpringViewAccessChecker springViewAccessChecker(AccessAnnotationChecker accessAnnotationChecker) {
return new KeycloakSpringViewAccessChecker(accessAnnotationChecker, "/oauth2/authorization/keycloak");
}
private OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler() {
var logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
return logoutSuccessHandler;
}
#Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
// Don't apply security rules on our static pages
web.ignoring().antMatchers("/session-expired");
}
#Bean
public PolicyFactory htmlSanitizer() {
// This is the policy we will be using to sanitize HTML input
return Sanitizers.FORMATTING.and(Sanitizers.BLOCKS).and(Sanitizers.STYLES).and(Sanitizers.LINKS);
}
}
Have tried formLogin().disable() method?
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
//your config here
.and().formLogin().disable();
}

spring boot actuator endpoints with Keycloak security

we have a spring boot project (2.3.0.RELEASE) with actuator endpoints and we are introducing keycloak to the project with KeycloakWebSecurityConfigurerAdapter how can I prevent actuator endpoints being secured by the keycloak filter chain.
We would like to have the "/actuator/**" endpoints secured by basic auth.
Currently we have a custom WebSecurityConfigurerAdapter with #Order(1) where we apply the basic auth to "/actuator/**" and then we have with #Order(2) antotated the KeycloakWebSecurityConfigurerAdapter
so 2 filter chains gets registered and when I call the actuator endpoints the second filter chain fails as unauthorised 401
is it possible to prevent handling the "/actuator/**" resorce path on the second filter chain?
First actuator security configuration.
#Configuration
#Order(1)
public class ActuatorWebSecurityConfig extends WebSecurityConfigurerAdapter {
private final String username;
private final String password;
private final PasswordEncoder encoder;
public ActuatorWebSecurityConfig(
#Value("${spring.security.user.name}") String username,
#Value("${spring.security.user.password}") String password,
Optional<PasswordEncoder> encoder) {
this.username = username;
this.password = password;
this.encoder = encoder.orElseGet(PasswordEncoderFactories::createDelegatingPasswordEncoder);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(username)
.password(encoder.encode(password))
.roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.antMatcher("/actuator/**")
.authorizeRequests(authorize -> authorize.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
}
}
second keycloak securoty configuration
#Order(2)
#KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
private final String swaggerUrl;
private final CorsFilter corsFilter;
private final CustomSecurityConfig customSecurityConfig;
#Autowired
public SecurityConfig(
#Value("${springdoc.swagger-ui.url:#{null}}") String swaggerUrl,
CorsFilter corsFilter,
CustomSecurityConfig customSecurityConfig) {
this.swaggerUrl = swaggerUrl;
this.corsFilter = corsFilter;
this.customSecurityConfig = customSecurityConfig;
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakProvider = keycloakAuthenticationProvider();
keycloakProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakProvider);
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable()
.requestMatcher(new NegatedRequestMatcher(new AntPathRequestMatcher("/actuator/**")));
.headers().frameOptions().disable()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/public/**", "/resources/**", "/resources/public/**").permitAll()
.antMatchers(OPTIONS, "/**").permitAll();
.authorizeRequests()
.antMatchers("/**")
.authenticated();
}
}
I have tried with on keycloak config
.antMatchers("/actuator/**").permitAll();
and with
http.requestMatcher(new NegatedRequestMatcher(new AntPathRequestMatcher("/actuator/**")));
but nothing works I receive unauthorised 401 for actuator
the registered filter chains :
2022-01-18 17:38:44,688 INFO org.springframework.security.web.DefaultSecurityFilterChain [main] Creating filter chain: Ant [pattern='/actuator/**'], [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#25c6a9de, org.springframework.security.web.context.SecurityContextPersistenceFilter#56f3f9da, org.springframework.security.web.header.HeaderWriterFilter#33dcbdc2, org.springframework.security.web.csrf.CsrfFilter#522fdf0c, org.springframework.security.web.authentication.logout.LogoutFilter#365ad794, org.springframework.security.web.authentication.www.BasicAuthenticationFilter#23df16cf, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#227cba85, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#b38dc7d, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#142422a4, org.springframework.security.web.session.SessionManagementFilter#2f0b7b6d, org.springframework.security.web.access.ExceptionTranslationFilter#74bca236, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#30587737]
2022-01-18 17:38:44,691 INFO org.springframework.security.web.DefaultSecurityFilterChain [main] Creating filter chain: NegatedRequestMatcher [requestMatcher=Ant [pattern='/actuator/**']], [com.betex.auth.filters.CorsFilter#20a9f5fb, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#10e28d97, org.springframework.security.web.context.SecurityContextPersistenceFilter#c6b08a5, org.springframework.security.web.header.HeaderWriterFilter#5f05cd7e, org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter#2a54c92e, org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter#55b62db8, org.springframework.security.web.authentication.logout.LogoutFilter#274f51ad, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#54980154, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#25874884, org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter#8cb7185, org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticatedActionsFilter#4dac40b, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#37d43b9b, org.springframework.security.web.session.SessionManagementFilter#11e8e183, org.springframework.security.web.access.ExceptionTranslationFilter#56f1db5f, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#78543f0d]
When you extend KeycloakWebSecurityConfigurerAdapter, the adapter register a Bean of type KeycloakAuthenticationProcessingFilter. This filter is registered in the Spring Security's SecurityFilterChain, and because it's a Bean, it is also automatically registered by Spring Boot in the original chain, therefore even if Spring Security doesn't apply it, it will be applied later on in original the filter chain.
Try disabling this filter from being registered by Spring Boot, like so:
#Bean
public FilterRegistrationBean registration(KeycloakAuthenticationProcessingFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
In addition, if you are using OAuth 2, you may consider using spring-security-oauth2-resource-server and simplifying your Resource Server's configuration. Take a look at the documentation. This way you don't need to extend the custom adapter, just rely on the out-of-the-box configuration from Spring Security.

Spring Boot webservice (REST) - How to change JUnit 5 tests from basic authentication to OAuth2 (Keycloak)

I have a Spring Boot webservice with REST controllers and with basic authentication (username and password).
On this base I developed JUnit 5 test.
Now I switch to OAuth2, currently trying the Resource Owner Password Credentials grant type.
What do I need to change on my JUnit 5 tests to run now with OAuth2?
Of course, before running my new tests with OAuth2 I have to start first Keycloak, afterwards the tests.
Following is my setup for the current basic authentication and the new OAuth2.
BASIC AUTHENTICATION (old implementation)
On my webservice side the web security config class looks like following:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/articles/**").hasRole("ADMIN")
// More antMatchers...
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.formLogin().disable();
}
#Bean
#Override
public UserDetailsService userDetailsService() {
UserDetails admin = User
.withUsername("admin")
.password("{noop}" + "admin123")
.roles("ADMIN")
.build();
// More users...
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(admin);
...
return userDetailsManager;
}
}
For the JUnit 5 tests I allways use the user admin, for example
#SpringBootTest
#AutoConfigureMockMvc
#WithUserDetails(value = "admin")
#TestInstance(Lifecycle.PER_CLASS)
public class MyRestControllerMockMvcTest {
#Autowired
private MockMvc mockMvc;
#BeforeAll
public void init(ApplicationContext appContext) throws Exception {
TestUtils.setupSecurityContext(appContext);
// some initialization
}
#AfterAll
public void cleanup(ApplicationContext appContext) throws Exception {
TestUtils.setupSecurityContext(appContext);
// some cleanup
}
#Test
public void getSomeInformationFromMyRestController() throws Exception {
MvcResult mvcResult = TestUtils.performGet(mockMvc, "...REST controller endpoint...", status().isOk());
MockHttpServletResponse response = mvcResult.getResponse();
ObjectMapper objectMapper = new ObjectMapper();
... = objectMapper.readValue(response.getContentAsString(), ...);
assertNotNull(...);
}
}
public class TestUtils {
public static void setupSecurityContext(ApplicationContext appContext) {
UserDetailsService uds = (UserDetailsService) appContext.getBean("userDetailsService");
UserDetails userDetails = uds.loadUserByUsername ("admin");
Authentication authToken = new UsernamePasswordAuthenticationToken (userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
public static MvcResult performGet(MockMvc mockMvc, String endpoint, ResultMatcher status) throws Exception {
MvcResult mvcResult = mockMvc.perform(get(endpoint))
.andDo(print())
.andExpect(status)
.andReturn();
return mvcResult;
}
}
Looking right now on the test setup in #BeforeAll and #AfterAll I'm not sure all of a sudden if I have to do
TestUtils.setupSecurityContext(appContext);
because now I use
#WithUserDetails(value = "admin")
#TestInstance(Lifecycle.PER_CLASS)
on the class. Just curious if the tests would still run without TestUtils.setupSecurityContext(appContext);, will try.
OAUTH2 (new implementation, replacing basic authentication above)
application.properties
...
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8183/auth/realms/myrealm/protocol/openid-connect/certs
With OAuth2 I changed the web security config class in my webservice (resource server) as following:
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/articles/**").hasRole("ADMIN")
// More antMatchers...
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter())
;
return httpSecurity.build();
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new MyRoleConverter());
return jwtAuthenticationConverter;
}
public class MyRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
#Override
public Collection<GrantedAuthority> convert(final Jwt jwt) {
jwt.getClaims().get("realm_access");
// Create roles
return ...;
}
}
}
My users are now defined in Keycloak.
Keycloak is configured to use Resource Owner Password Credentials.
#jzheaux is right (sure, he's spring-security team member...).
Changes will occure in your security configuration but the test won't change ... for the most part: You'll probably want to have an Authentication of the right type in your test security-context.
If your new security configuration populates security-context with JwtAuthenticationToken, it would be nice to have JwtAuthenticationToken in test security-context too. #WithUserDetails(value = "admin") won't build JwtAuthenticationToken.
You should have a look at this lib I wrote and specifically at #WithMockJwtAuth. Usage is demonstrated there:
#Test
#WithMockJwtAuth(authorities = "ROLE_AUTHORIZED_PERSONNEL", claims = #OpenIdClaims(sub = "Ch4mpy"))
public void greetJwtCh4mpy() throws Exception {
api.get("/greet").andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
}
P.S.
You'll find in this same git repo samples for other kind of Authentication better adapted to OIDC than JwtAuthenticationToken like KeycloakAuthenticationToken (written by Keycloak team for Keycloak exclusively) or OidcAuthentication (written by myself for any OpenID Connect complient authorization server), along with #WithMockKeycloakAuth and #WithMockOidcAuth

How can I do to my spring boot resource server oauth 2 get user's extra data from api when user authenticate e keep it into security context?

I have a resource server done with Spring Boot. I'm using Spring Security 5.3 to authenticate and authorize the frontend exchange data. I've configured a authorization server "issuer-uri" in application.yml that provides and validates the access_token (jwt).
Until there ok. The problem that authorization server doesn't provide at once every user's information that I need in access_token or id_token. With the sub claim and access_token I need to do a request to endpoint to get more extra data about user.
I would like to know how can I do a request to get that information just when the user authenticates and put them into security context togheter the information that's already comes. So that way, I could get that information in some service when needed without make a request to endpoint each time:
SecurityContextHolder.getContext().getAuthentication().getDetails()
It's here my WebSecurityConfigurerAdapter
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String CLAIM_ROLES = "role";
private static final String AUTHORITY_PREFIX = "ROLE_";
#Value("${sso.issuers_uri}")
private String issuers;
Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);
#Override
protected void configure(HttpSecurity http) throws Exception {
String[] result = issuers.split(",");
List<String> arrIssuers = Arrays.asList(result);
arrIssuers.stream().forEach(issuer -> addManager(authenticationManagers, issuer));
http
.httpBasic().disable()
.formLogin(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.authorizeRequests(auth -> auth
.antMatchers(
"/*",
"/signin-oidc",
"/uri-login_unico",
"/assets/**","/views/**",
"index.html",
"/api/segmentos/listar_publicados",
"/api/modelos",
"/api/modelos/*"
).permitAll()
.antMatchers(
"/api/admin/**"
).hasRole("role.PGR.Admin")
.antMatchers(
"/api/govbr/**"
).hasAnyAuthority("SCOPE_govbr_empresa")
.anyRequest().authenticated()
).oauth2ResourceServer(oauth2ResourceServer -> {
oauth2ResourceServer.authenticationManagerResolver(this.authenticationManagerResolver);
});
}
public void addManager(Map<String, AuthenticationManager> authenticationManagers, String issuer) {
JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(JwtDecoders.fromOidcIssuerLocation(issuer));
authenticationProvider.setJwtAuthenticationConverter(getJwtAuthenticationConverter());
authenticationManagers.put(issuer, authenticationProvider::authenticate);
}
private Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(getJwtGrantedAuthoritiesConverter());
return jwtAuthenticationConverter;
}
private Converter<Jwt, Collection<GrantedAuthority>> getJwtGrantedAuthoritiesConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthorityPrefix(AUTHORITY_PREFIX);
converter.setAuthoritiesClaimName(CLAIM_ROLES);
return converter;
}
}
I don't know if I need to do a custom AuthenticationManger or if I can do this with a security filter after authenticated. If someone could help me, I really apprecite it. Tks!!!

Storing JWT tokens on OAuth2 web client using Spring Security

I'm implementing an OAuth2 web application Client using Spring Boot 2.1.3 and Spring Security 5.1.3 that is obtaining JWT tokens from an authorization server through authorization code grant type and calls a protected resource server.
This is how the implementation looks up till now:
Security configuration and a restTemplate bean used to call the protected resource:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.and()
.oauth2Client()
.and().logout().logoutSuccessUrl("/");
}
#Bean
public RestTemplate restTemplate(OAuth2AuthorizedClientService clientService) {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new AuthorizationHeaderInterceptor(clientService));
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
The interceptor that adds the authorization header (from the framework's InMemoryOAuth2AuthorizedClientService) in the restTemplate:
public class AuthorizationHeaderInterceptor implements ClientHttpRequestInterceptor {
private OAuth2AuthorizedClientService clientService;
public AuthorizationHeaderInterceptor(OAuth2AuthorizedClientService clientService) {
this.clientService = clientService;
}
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String accessToken = null;
if (authentication != null && authentication.getClass().isAssignableFrom(OAuth2AuthenticationToken.class)) {
OAuth2AuthenticationToken auth = (OAuth2AuthenticationToken) authentication;
String clientRegistrationId = auth.getAuthorizedClientRegistrationId();
OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(clientRegistrationId, auth.getName());
accessToken = client.getAccessToken().getTokenValue();
request.getHeaders().add("Authorization", "Bearer " + accessToken);
}
return execution.execute(request, bytes);
}
}
And the controller that calls the protected resource server:
#Controller
#RequestMapping("/profile")
public class ProfileController {
#Autowired
private RestTemplate restTemplate;
#Value("${oauth.resourceServerBase}")
private String resourceServerBase;
#GetMapping
public String getProfile(Model model) {
Profile profile = restTemplate.getForEntity(resourceServerBase + "/api/profile/", Profile.class).getBody();
model.addAttribute("profile", profile);
return "profile";
}
}
The OAuth2 client configuration is directly in the application.yml:
spring:
security:
oauth2:
client:
registration:
auth-server:
client-id: webClient
client-secret: clientSecret
scope: read,write
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8081/client/login/oauth2/code/auth-server
provider:
auth-server:
authorization-uri: http://localhost:8080/auth-server/oauth/authorize
token-uri: http://localhost:8080/auth-server/oauth/token
user-info-uri: http://localhost:8082/resource-server/users/info
user-name-attribute: user_name
After doing some debugging I've observed that at the end of a successful authentication flow through OAuth2LoginAuthtenticationFilter the framework is storing the obtained access and refresh JWT tokens under OAuth2AuthorizedClient model in memory through the provided InMemoryOAuth2AuthorizedClientService.
I am trying to find out how to override this behaviour so that the tokens can remain available after a server restart. And also keep the user logged in based on this.
Should I just provide a custom OAuth2AuthorizedClientService implementation? How could I configure Spring Security to use it? And should this custom implementation store the tokens in a cookie?
Should I just provide a custom OAuth2AuthorizedClientService
implementation?
I think yes, for solving your use case
How could I configure Spring Security to use it?
From spring doc:
If you would like to provide a custom implementation of
AuthorizationRequestRepository that stores the attributes of
OAuth2AuthorizationRequest in a Cookie, you may configure it as shown
in the following example:
#EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client()
.authorizationCodeGrant()
.authorizationRequestRepository(this.cookieAuthorizationRequestRepository())
...
}
private AuthorizationRequestRepository<OAuth2AuthorizationRequest> cookieAuthorizationRequestRepository() {
return new HttpCookieOAuth2AuthorizationRequestRepository();
}
}

Resources