Spring Security 5.2.1 + spring-security-oauth2 + WebClient: how to use password grant-type - spring

Here is my current setup:
I'm exposing a WebClient bean with oauth2 filter:
#Configuration
class OAuthConfiguration {
#Bean("authProvider")
fun webClient(
clientRegistrationRepository: ClientRegistrationRepository,
authorizedClientRepository: OAuth2AuthorizedClientRepository,
clientHttpConnector: ClientHttpConnector
): WebClient {
val oauth = ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, authorizedClientRepository)
oauth.setDefaultClientRegistrationId("authProvider")
oauth.setDefaultOAuth2AuthorizedClient(true)
return WebClient.builder()
.baseUrl("baseUrl")
.clientConnector(clientHttpConnector)
.filter(oauth)
.build()
}
}
And I'm using it here:
fun callExternalService() {
val retrieve = webClient.get()
.uri("/uri")
.retrieve()
.bodyToMono(String::class.java)
.block()
// ...
}
My application.yml has the following structure
security:
oauth2:
client:
provider:
authProvider:
token-uri: https://authentication-uri.com
registration:
authProvider:
client-id: client-id
client-secret: client-secret
authorization-grant-type: authorization_code
scope: any
This code is failing because my internal authentication service accepts only password grant-type and I can see the response for my auth URL returning a 400 code. Once I change authorization-grant-type: authorization_code to authorization-grant-type: password, spring ignores all the logic of authentication, it does not try to authenticate.
Does anyone know how to implement authorization-grant-type: password?

Related

Service to Service secure communication among services run behind spring cloud gateway

I have couple of microservices running behind the Spring cloud gateway and all microservices run behind are protected by OAuth2 protocol using spring authorization server. Everything is working fine but few of the microservices would like to exchange data with each other but these resource servers are protected.
I have created the registered client (for each microservice with their own client id & client secret) at authorization server and as well as respective OAuth2 client configuration at each service. I have configured this setup with Webclient to fetch data from other services needed as follows
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
#Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("server");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
....
....
application.yml (similar config. for resource-client2)
spring:
security:
oauth2:
client:
registration:
server:
scope: ROLE_RESOURCE_CLIENT1
client-id: resource-client1
client-secret: '{noop}secret1'
client-authentication-method: basic
authorization-grant-type: client_credentials
client-name: resource-client1-client-credentials
provider:
server:
token-uri: http://localhost:9001/oauth2/token
resourceserver:
jwt:
issuer-uri: http://localhost:9001
and at authorization server, the client configuration is as follows
private final static RegisteredClient resource1ServiceClient = RegisteredClient.withId("2")
.clientId("resource-client1")
.clientSecret("{noop}secret1")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
private final static RegisteredClient resource2ServiceClient = RegisteredClient.withId("3")
.clientId("resource-client2")
.clientSecret("{noop}secret2")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
when I'm trying to do rest call using webclient from resource client1 to resource client2, I'm getting NPE error because the spring authorization server is not providing the access token.
error is
java.lang.NullPointerException: Cannot invoke "org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse.getAccessToken()" because "tokenResponse" is null
at org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider.authorize(ClientCredentialsOAuth2AuthorizedClientProvider.java:87) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ Request to GET http://localhost:8082/hello2 [DefaultWebClient]
Original Stack Trace:
at org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider.authorize(ClientCredentialsOAuth2AuthorizedClientProvider.java:87) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1]
at org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider.authorize(DelegatingOAuth2AuthorizedClientProvider.java:71) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1]
at org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager.authorize(DefaultOAuth2AuthorizedClientManager.java:176) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1]
at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$authorizeClient$22(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:485) ~[spring-security-oauth2-client-6.0.1.jar:6.0.1]
at reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:67) ~[reactor-core-3.5.1.jar:3.5.1]
Not sure what went wrong with my setup or missing the logic to get it done. using latest spring version & spring authorization server version 1.0.0 . kindly help

Throw OAuth2AuthenticationException when access oidc-client-registration-endpoint

I'm trying to register client use oidc-client-registration-endpoint /connect/register.
Here is my client defined:
spring:
security:
oauth2:
client:
registration:
dev-client:
provider: auth-server
client-id: dev-client
client-secret: dev-client-password
client-name: dev-client
scope: client.create,client.read
authorization-grant-type: client_credentials
redirect-uri: http://client.localhost:9090/dev-client
provider:
auth-server:
issuer-uri: http://auth.localhost:8080
Here is my client side Controller code:
#GetMapping("/register-client")
public String registerClient(#RegisteredOAuth2AuthorizedClient("dev-client") OAuth2AuthorizedClient client) throws IOException {
var clientMetaData = new OIDCClientMetadata();
clientMetaData.setRedirectionURI(URI.create("http://client.localhost:9090/authorized"));
return new OIDCClientRegistrationRequest(
URI.create("http://auth.localhost:9090/connect/register"),
clientMetaData,
new BearerAccessToken(client.getAccessToken().getTokenValue())
).toHTTPRequest().send().getContent();
}
When I call client API to access AS oidc-client-registration-endpoint, it will throw the OAuth2AuthenticationException in
org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider#checkScope, because the client has two scope.
Is that means I must define two client (one for create, another for read)?

Code verifier not sent with spring reactive security

I've been trying to set up twitter oauth2 PKCE authentification with spring boot. The issue is that the code_verifier parameter is not being picked up by spring security. Do I have to configure a bean so that the code verifier gets picked up? Is there a way to customize a ReactiveOAuth2AccessTokenResponseClient to customize to body send to the token endpoint ?
Here is my spring security config, :
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http.authorizeExchange()
.anyExchange().authenticated()
.and().oauth2Login().and().build();
}
security:
oauth2:
client:
registration:
twitter:
client-id: xxx
client-secret: xxx
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/twitter
provider:
twitter:
authorization-uri: https://twitter.com/i/oauth2/authorize?response_type=code&client_id=xxx&redirect_uri=http://localhost:8080/login/oauth2/code/twitter&scope=tweet.read%20users.read%20follows.read%20follows.write&code_challenge=challenge&code_challenge_method=plain
token-uri: https://api.twitter.com/2/oauth2/token
user-info-uri: https://api.twitter.com/2/users/me
user-name-attribute: data
For people still looking for an answer: You can customize the token request body by overriding the WebClientReactiveAuthorizationCodeTokenResponseClient bean and use the setParameter method. Here is an example:
#Bean
public WebClientReactiveAuthorizationCodeTokenResponseClient webClientReactiveAuthorizationCodeTokenResponseClient() {
WebClientReactiveAuthorizationCodeTokenResponseClient webClientReactiveAuthorizationCodeTokenResponseClient =
new WebClientReactiveAuthorizationCodeTokenResponseClient();
webClientReactiveAuthorizationCodeTokenResponseClient.setParametersConverter(source -> {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap();
parameters.add("grant_type", source.getGrantType().getValue());
//...
return parameters;
});
return webClientReactiveAuthorizationCodeTokenResponseClient;
}

Spring Cloud Gateway redirects to Keycloak login page although Bearer token is set

I am using a setup with Keycloak as Identity Provider, Spring Cloud Gateway as API Gateway and multiple Microservices.
I can receive a JWT via my Gateway (redirecting to Keycloak) via http://localhost:8050/auth/realms/dev/protocol/openid-connect/token.
I can use the JWT to access a resource directly located at the Keycloak server (e.g. http://localhost:8080/auth/admin/realms/dev/users).
But when I want to use the Gateway to relay me to the same resource (http://localhost:8050/auth/admin/realms/dev/users) I get the Keycloak Login form as response.
My conclusion is that there must me a misconfiguration in my Spring Cloud Gateway application.
This is the Security Configuration in the Gateway:
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository) {
// Authenticate through configured OpenID Provider
http.oauth2Login();
// Also logout at the OpenID Connect provider
http.logout(logout -> logout.logoutSuccessHandler(
new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
//Exclude /auth from authentication
http.authorizeExchange().pathMatchers("/auth/realms/ahearo/protocol/openid-connect/token").permitAll();
// Require authentication for all requests
http.authorizeExchange().anyExchange().authenticated();
// Allow showing /home within a frame
http.headers().frameOptions().mode(Mode.SAMEORIGIN);
// Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
http.csrf().disable();
return http.build();
}
}
This is my application.yaml in the Gateway:
spring:
application:
name: gw-service
cloud:
gateway:
default-filters:
- TokenRelay
discovery:
locator:
lower-case-service-id: true
enabled: true
routes:
- id: auth
uri: http://localhost:8080
predicates:
- Path=/auth/**
security:
oauth2:
client:
registration:
keycloak:
client-id: 'api-gw'
client-secret: 'not-relevant-but-correct'
authorizationGrantType: authorization_code
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
scope: openid,profile,email,resource.read
provider:
keycloak:
issuerUri: http://localhost:8080/auth/realms/dev
user-name-attribute: preferred_username
server:
port: 8050
eureka:
client:
service-url:
default-zone: http://localhost:8761/eureka
register-with-eureka: true
fetch-registry: true
How can I make the Gateway able to know that the user is authenticated (using the JWT) and not redirect me to the login page?
If you want to make requests to Spring Gateway with access token you need to make it a resource server. Add the following:
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
application.yml
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://.../auth/realms/...
SecurityConfiguration.java
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository) {
// Authenticate through configured OpenID Provider
http.oauth2Login();
// Also logout at the OpenID Connect provider
http.logout(logout -> logout.logoutSuccessHandler(
new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
// Require authentication for all requests
http.authorizeExchange().anyExchange().authenticated();
http.oauth2ResourceServer().jwt();
// Allow showing /home within a frame
http.headers().frameOptions().mode(Mode.SAMEORIGIN);
// Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
http.csrf().disable();
return http.build();
}
I bypassed the problem by communicating directly with Keycloak without relaying requests to it via Spring Cloud Gateway.
That's actually not a workaround but actually best practice/totally ok as far as I understand.
This code is for Client_credentials grant_type. if you use other grant type you need to add client_id and client_secret in request parameters.
public class MyFilter2 extends OncePerRequestFilter {
private final ObjectMapper mapper = new ObjectMapper();
#Value("${auth.server.uri}")
private String authServerUri;
#Value("${client_id}")
private String clientId;
#Value("${client_secret}")
private String clientSecret;
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws IOException {
try {
String token = httpServletRequest.getHeader("Authorization");
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type","application/x-www-form-urlencoded");
headers.set("Authorization",token);
final HttpEntity finalRequest = new HttpEntity("{}", headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.postForEntity(authServerUri,finalRequest,String.class);
if (!HttpStatus.OK.equals(response.getStatusCode())) {
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", HttpStatus.UNAUTHORIZED.value());
errorDetails.put("message", "Invalid or empty token");
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(httpServletResponse.getWriter(), errorDetails);
} else {
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}catch(HttpClientErrorException he) {
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", HttpStatus.UNAUTHORIZED.value());
errorDetails.put("message", "Invalid or empty token");
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(httpServletResponse.getWriter(), errorDetails);
}catch (Exception exception) {
}
}

How can I retrieve the azure AD JWT access token from Spring?

I'm trying to retrieve the azure JWT access token from my Spring Boot application from another application by querying a /token endpoint, but the token I receive is seemingly incorrect.
The project has a Spring Boot backend and an Eclipse rcp frontend. I'm attempting to retrieve the access token from the eclipse frontend. For this, I have the controller below:
#Autowired
private OAuth2AuthorizedClientService authorizedClientService;
#GetMapping("/token")
public String user(OAuth2AuthenticationToken authentication) {
OAuth2AuthorizedClient authorizedClient = this.authorizedClientService
.loadAuthorizedClient(authentication.getAuthorizedClientRegistrationId(), authentication.getName());
return authorizedClient.getAccessToken().getTokenValue();
}
Which returns a token with the following format:
PAQABAAAAAABeAFzDwllzTYGDLh_qYbH8hgtbYMB8x7YLamQyQPk_MEXyd9Ckc5epDFQMv3RxjmMie0JDr5uN82U4RFLgU3fnDBxGolo4XVwzLEsTZDmUK_r0YG6ZwLbbQI_ch_Xn8xCxhsFq-AoRbEESDqK3GmK4eXwCYoT0G8_XfZjHTvCNTOMqUb2Q-CD2EalIKf0zSZ5184qrvlXfdNeT_BJdH_tqaodn80Bp2UL2hdnOCDZuWRqKl_2fi4v-eOOKJCcjOqY6SreVEeoKkIvVdayGE8F6qCxFehmlA0sX9sVW34FIVYVo4lDRsTkm-WN2KJwxJmalNcxg0k2ObDnIeC1ulPPpiPq-O_LK9bVA4HEZ63cJi9ZwQHwLPUhOO6TquoCOroHSy5KPoFkX3N796hM1i0NpaaY4MeAx17CSYeZ9P06jvYD7UMTV3OwWt-OVrDm5z_AvbOvyHRf9wjh31H6oLoc-iu_NCspT6NzC2UZQSHBtKdydEcP6sNkRp073jrZEg8UtcVT6HzddIBk2P0tVeIiSyU3SfLETbzJE67xtJVip3ai9aLN28c0qt3rDBaVGDAXjXhqrh5D3NiXdQjS6YTAKy0bVmNk9Yr9o2CGBA2wFjE8OZ6_Hb3k8_13KMJHafx0gAA
Dependencies from pom.xml
Built using spring boot with the following relevant dependencies:
spring-boot-starter-web v2.2.4
azure-active-directory-spring-boot-starter v2.2.1
spring-security-oauth2-client v5.2.1
spring-security-oauth2-jose v5.2.1
spring-security-oauth2-resource-server v5.2.1
Config from application.yml
We support multiple authorization servers, here is the fully configured azure client:
spring:
security:
oauth2:
client:
azure:
client-id: XXX
client-secret: XXX
client-name: Microsoft
scope: openid, https://graph.microsoft.com/user.read, profile
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/azure
client-authentication-method: basic
authentication-method: post
provider:
authorization-uri: https://login.microsoftonline.com/XXX/oauth2/authorize
token-uri: https://login.microsoftonline.com/XXX/oauth2/token
user-info-uri: https://login.microsoftonline.com/XXX/openid/userinfo
jwt-set-uri: https://login.microsoftonline.com/dXXX/discovery/keys
azure:
activedirectory:
tenant-id: XXX
active-directory-groups: XXX
allow-telemetry: false
websecurityconfig.java
#Configuration
#EnableConfigurationProperties
#EnableWebSecurity
#Order(1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
[...]
.anyRequest().authenticated();
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
http.oauth2Login()
.userInfoEndpoint()
.oidcUserService(oidcUserService)
.and()
.authorizationEndpoint();
}
[...]
}
This is how I ended up obtaining the open id token from Azure
#GetMapping("/token")
public String user(OAuth2AuthenticationToken authentication) {
DefaultOidcUser user = (DefaultOidcUser) authentication.getPrincipal();
return user.getIdToken().getTokenValue();
}

Resources