Summary
Implemented Spring security oauth2 and getting an access token and refresh token from Azure AD into my oauthToken in spring. Access token expiry is 60 minute. OpenID connect login flow working correctly.
Getting access token in service layer using autowired OAuth2AuthorizedClientService.
The access token string value is passed along to Azure Java SDK which uses Reactor Netty implementation to calls its own Azure REST APIs.
Seems Spring is refreshing access token when its retrieved via client inject with RegisteredOAuth2AuthorizedClient method param annotation, but not when called using the OAuth2AuthorizedClientService in my service layer.
@Autowired
OAuth2AuthorizedClientService oauthClientService;
public String getToken() {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthorizedClient client = oauthClientService
.loadAuthorizedClient(oauthToken.getAuthorizedClientRegistrationId(), oauthToken.getName());
return client.getAccessToken().getTokenValue();
}
If using @RegisteredOAuth2AuthorizedClient annotation on a controller method, access token refreshes automatically.
@GetMapping(value = "/testToken", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
public String testToken(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oauthClient) {
return oauthClient.getAccessToken().getTokenValue();
}
Actual Behavior
Token in service method getToken() never refreshes after access token expires. Expired access token is always returned.
If I get token using the @RegisteredOAuth2AuthorizedClient annotation in the controller, it refreshes when expired.
But, I need to access my token in my service layer class to call out to an API.
Expected Behavior
Expect the access token to be automatically refreshed when expired when getting a token in my service class method using the autowired OAuth2AuthorizedClientService.
Configuration
@Configuration
@Slf4j
public class OauthClientConfig {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
log.info(">> authorizedClientManager");
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode().refreshToken().build();
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
log.info("<< authorizedClientManager");
return authorizedClientManager;
}
}
WebSecurity configuration
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final String invalidSessionRedirectUrl = "/invalidsession";
private final String[] cookiesToClear = { "JSESSIONID", "azure.blob.storage", "azure.repository.location" };
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Override
public void configure(WebSecurity security) {
security.ignoring().antMatchers("/ui/js/**", "/ui/css/**", "/ui/images/**", "/ui/img/**", "/ui/docs/**",
"/ui/fas/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// authority check set to minimum required, based on defined role hierarchy
http.authorizeRequests(authz -> authz.antMatchers("/", invalidSessionRedirectUrl + "**", "/error**").permitAll()
.antMatchers("/ui/filesys/**").hasAuthority("ROLE_PUBLISHER").antMatchers("/actuator/**")
.hasAuthority("ROLE_ADMIN").antMatchers("/en/**", "/fr/**").hasAuthority("ROLE_GUEST").anyRequest()
.authenticated())
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo.userAuthoritiesMapper(this.userAuthoritiesMapper())))
.csrf().disable().headers().frameOptions().sameOrigin().contentTypeOptions().disable();
// concurrency controls and invalid session config
http.sessionManagement().maximumSessions(1).expiredUrl(this.invalidSessionRedirectUrl).and()
.invalidSessionUrl(this.invalidSessionRedirectUrl);
// logout config
http.logout().clearAuthentication(true).deleteCookies(cookiesToClear).invalidateHttpSession(true)
.logoutSuccessHandler(this.oidcLogoutSuccessHandler());
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(
"ROLE_ADMIN > ROLE_EDITOR\nROLE_EDITOR > ROLE_PUBLISHER\nROLE_PUBLISHER > ROLE_GUEST\nROLE_GUEST > ROLE_USER");
return roleHierarchy;
}
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
log.info(">> userAuthoritiesMapper()");
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (log.isDebugEnabled()) {
log.debug("Authority instance type: {}", authority.getClass());
}
if (OidcUserAuthority.class.isInstance(authority)) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
OidcIdToken idToken = oidcUserAuthority.getIdToken();
OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
mappedAuthorities.add(oidcUserAuthority);
if (idToken.containsClaim("roles")) {
List<String> roles = idToken.getClaimAsStringList("roles");
for (String role : roles) {
mappedAuthorities.add(new SimpleGrantedAuthority(role));
}
}
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (log.isDebugEnabled()) {
userAttributes.forEach((k, v) -> log.debug("Key: {}; Value: {}", k, v));
}
}
});
log.info("<< userAuthoritiesMapper()");
return mappedAuthorities;
};
}
private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(
this.clientRegistrationRepository);
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/" + invalidSessionRedirectUrl);
return oidcLogoutSuccessHandler;
}
@Bean
public AuthenticationEventPublisher authenticationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}
}
Oauth2 properties configuration
spring.security.oauth2.client.provider.azure.issuer-uri=https://login.microsoftonline.com/xxxxx/v2.0
spring.security.oauth2.client.provider.azure.user-name-attribute=preferred_username
spring.security.oauth2.client.registration.azure.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.azure.client-id=xxxxxx
spring.security.oauth2.client.registration.azure.client-secret=${azure.client.secret}
spring.security.oauth2.client.registration.azure.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.azure.scope=openid,email,profile,${app.config.azStorageBaseUrl}/user_impersonation,offline_access
Version
Spring Boot 2.3.9.RELEASE. Spring Security 5.3.8.RELEASE
Sample
Comment From: jzheaux
Hi, @PbALpi7xEX, sorry to hear you are having trouble.
But, I need to access my token in my service layer class to call out to an API.
Spring Security comes with support for refreshing tokens as they are passed to an API as well.
For WebClient, you can see the Spring Security WebClient sample.
For RestTemplate, you can see my answer on StackOverflow.
I'm going to close this question as answered. If you feel like there is more to discuss, it feels like this is a question that would be better suited to Stack Overflow. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or add more detail if you feel this is a genuine bug.
Comment From: PbALpi7xEX
@jzheaux Not using RestTemplate or Webclient. Passing the access token to Azure Java SDK api which uses reactor Netty underneath to calls its Azure REST apis. Found it odd access token refreshed automatically when called using RegisteredOAuth2AuthorizedClient annotation, but not when called using the OAuth2AuthorizedClientService in my service layer.
Comment From: jzheaux
OAuth2AuthorizedClientService does not refresh the token, but OAuth2AuthorizedClientManager does.
Comment From: PbALpi7xEX
Thanks @jzheaux That looks like it has done the trick. Thanks and have a good weekend.
Comment From: taninme
@PbALpi7xEX Could you please share what did you change exactly. I'm having similar issues. Thanks in advance.
Comment From: nguyent25
@PbALpi7xEX Could you please share what did you change exactly. I'm having similar issues. Thanks in advance.