Spring security - using WebClient access a resource that is protected via Oauth2 "Password" grant type - spring-security

How can I access with WebClient a resource that is protected via the Oauth2 'Password' grant type?
Connecting with Oauth2 'client-credentials' works. In this case I need the password grant type.
I get this error:
401 Unauthorized from GET http://localhost:8086/test2 at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:198) ~[spring-webflux-5.3.19.jar:5.3.19]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ 401 from GET http://localhost:8086/test2
I configured the auth server via Keycloack with Access Type 'public'. I checked accessing a token via Postman. You can find more details via this post.
The Websecurity config (working for grant-type client-credentials):
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("*").permitAll();
}
}
The webclient is created as a Bean. It works for the client-credentials grant type.
#Configuration
public class WebClientOAuth2Config {
#Bean("method2")
WebClient webClientGrantPassword( #Qualifier("authclientmgr2") OAuth2AuthorizedClientManager authorizedClientManager2) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager2);
oauth2Client2.setDefaultClientRegistrationId("businesspartners");
return WebClient.builder().apply(oauth2Client2.oauth2Configuration()).build();
}
#Bean("authclientmgr2")
public OAuth2AuthorizedClientManager authorizedClientManager2(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
The controller accessing the resource server:
#RestController
public class Test2Controller {
#Autowired
private #Qualifier("method2") WebClient webClient2;
#GetMapping("/test2")
public String test2() {
return webClient2.get().uri("http://localhost:8086/test2")
.attributes(clientRegistrationId("businesspartners"))
.retrieve().bodyToMono(String.class).block();
}
}
The application.yml config is:
server:
port: 8081
spring:
security:
oauth2:
client:
registration:
businesspartners:
client-id: myclient2
authorization-grant-type: password
client-name: johan
client-secret: password
provider:
businesspartners:
issuer-uri: http://localhost:28080/auth/realms/realm2
token-uri: http://localhost:28080/auth/realms/realm2/protocol/openid-connect/token
The maven dependencies include:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Not sure whether it's possible to do it using application.yml but here is how you can configure it in code
private ServerOAuth2AuthorizedClientExchangeFilterFunction oauth(
String clientRegistrationId, SecurityConfig config) {
var clientRegistration = ClientRegistration
.withRegistrationId(clientRegistrationId)
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.tokenUri(config.getTokenUri() + "/token")
.clientId(config.getClientId())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.build();
var authRepository = new InMemoryReactiveClientRegistrationRepository(clientRegistration);
var authClientService = new InMemoryReactiveOAuth2AuthorizedClientService(authRepository);
var authClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
authRepository, authClientService);
var clientAuthProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
authClientManager.setAuthorizedClientProvider(clientAuthProvider);
authClientManager.setContextAttributesMapper(authorizeRequest -> Mono.just(
Map.of(
OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, config.getUsername(),
OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, config.getPassword()
)
));
var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authClientManager);
oauth.setDefaultClientRegistrationId(clientRegistrationId);
return oauth;
}
and then use in in WebClient
WebClient webClient = WebClient.builder()
.filter(oauth("businesspartners", securityConfig))
.build();
where SecurityConfig is defined like
#lombok.Value
#lombok.Builder
static class SecurityConfig {
String tokenUri;
String clientId;
String username;
String password;
}
Here is a full test using WireMock
#Slf4j
#SpringBootTest(webEnvironment = NONE)
#AutoConfigureWireMock(port = 0) // random port
class WebClientTest {
#Value("${wiremock.server.port}")
private int wireMockPort;
#Test
void authClientTest() {
String authResponse = """
{
"token_type": "Bearer",
"expires_in": 3599,
"ext_expires_in": 3599,
"access_token": "token",
"refresh_token": "token"
}""";
stubFor(post(urlPathMatching("/token"))
.withRequestBody(
containing("client_id=myclient2")
.and(containing("grant_type=password"))
.and(containing("password=password"))
.and(containing("username=username"))
)
.withHeader(HttpHeaders.CONTENT_TYPE, containing(MediaType.APPLICATION_FORM_URLENCODED.toString()))
.willReturn(aResponse()
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withStatus(200)
.withBody(authResponse)
)
);
stubFor(get(urlPathMatching("/test"))
.willReturn(aResponse()
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withStatus(200)
.withBody("{}")
)
);
SecurityConfig config = SecurityConfig.builder()
.tokenUri("http://localhost:" + wireMockPort)
.clientId("myclient2")
.username("username")
.password("password")
.build();
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:" + wireMockPort)
.filter(oauth("test", config))
.build();
Mono<String> request = webClient.get()
.uri("/test")
.retrieve()
.bodyToMono(String.class);
StepVerifier.create(request)
.assertNext(res -> log.info("response: {}", res))
.verifyComplete();
}
private ServerOAuth2AuthorizedClientExchangeFilterFunction oauth(
String clientRegistrationId, SecurityConfig config) {
var clientRegistration = ClientRegistration
.withRegistrationId(clientRegistrationId)
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.tokenUri(config.getTokenUri() + "/token")
.clientId(config.getClientId())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.build();
var authRepository = new InMemoryReactiveClientRegistrationRepository(clientRegistration);
var authClientService = new InMemoryReactiveOAuth2AuthorizedClientService(authRepository);
var authClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
authRepository, authClientService);
var clientAuthProvider = new PasswordReactiveOAuth2AuthorizedClientProvider();
authClientManager.setAuthorizedClientProvider(clientAuthProvider);
authClientManager.setContextAttributesMapper(authorizeRequest -> Mono.just(
Map.of(
OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, config.getUsername(),
OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, config.getPassword()
)
));
var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authClientManager);
oauth.setDefaultClientRegistrationId(clientRegistrationId);
return oauth;
}
#lombok.Value
#lombok.Builder
static class SecurityConfig {
String tokenUri;
String clientId;
String username;
String password;
}
}

Related

How to emulate Oauth2RestTemplate getAccessToken with WebClient

I have a Oauth2 client app which other apps are using to get access token against Keycloak (Authorization server). Internally i was using Oauth2RestTemplate but as its marked deprecated some links have suggested using WebClient. I have done the Webclient configuration as per spring docs for password grant OAuth2 client, but have not found a way to get access token from it. Any leads here would be appreciated
`Web Client Configuration:
#Configuration
public class WebClientConfig {
#Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("keycloak");
return WebClient.builder().apply(oauth2Client.oauth2Configuration()).build();
}
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager (
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters,
// map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
}
Now what i am looking is to emulate Oauth2RestTemplate.getAccessToken() sort of behaviour with WebClient,
where user can generate access token using password grant.
Please let me know if my understanding is wrong.
`

Feign and Spring Security 5 - Client Credentials

I am trying to invoke some backend system which is secured by a client_credentials grant type from a Feign client application.
The access token from the backend system can be retrieved with the following curl structure (just as an example):
curl --location --request POST '[SERVER URL]/oauth/grant' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: WebSessionID=172.22.72.1.1558614080219404; b8d49fdc74b7190aacd4ac9b22e85db8=2f0e4c4dbf6d4269fd3349f61c151223' \
--data-raw 'grant_type=client_credentials' \
--data-raw 'client_id=[CLIENT_ID]' \
--data-raw 'client_secret=[CLIENT_SECRET]'
{"accessToken":"V29C90D1917528E9C29795EF52EC2462D091F9DC106FAFD829D0FA537B78147E20","tokenType":"Bearer","expiresSeconds":7200}
This accessToken should then be set in a header to subsequent business calls to the backend system.
So now my question is, how to implement this using Feign and Spring Boot Security 5.
After some research I come to this solution (which doesn't work):
Define my client in the application.yml:
spring:
security:
oauth2:
client:
registration:
backend:
client-id:[CLIENT_ID]
client-secret: [CLIENT_SECRET]
authorization-grant-type: client_credentials
provider:
backend:
token-uri: [SERVER URL]/oauth/grant
Create a OAuth2AuthorizedClientManager Bean to be able to authorize (or re-authorize) an OAuth 2.0 client:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
return authorizedClientManager;
}
Create a Feign Request Interceptor that uses the OAuth2AuthorizedClientManager:
public class OAuthRequestInterceptor implements RequestInterceptor {
private OAuth2AuthorizedClientManager manager;
public OAuthRequestInterceptor(OAuth2AuthorizedClientManager manager) {
this.manager = manager;
}
#Override
public void apply(RequestTemplate requestTemplate) {
OAuth2AuthorizedClient client = this.manager.authorize(OAuth2AuthorizeRequest.withClientRegistrationId("backend").principal(createPrincipal()).build());
String accessToken = client.getAccessToken().getTokenValue();
requestTemplate.header(HttpHeaders.AUTHORIZATION, "Bearer" + accessToken);
}
private Authentication createPrincipal() {
return new Authentication() {
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptySet();
}
#Override
public Object getCredentials() {
return null;
}
#Override
public Object getDetails() {
return null;
}
#Override
public Object getPrincipal() {
return this;
}
#Override
public boolean isAuthenticated() {
return false;
}
#Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
#Override
public String getName() {
return "backend";
}
};
}
}
Create a FeignConfig that uses the Interceptor:
public class FeignClientConfig {
#Bean
public OAuthRequestInterceptor repositoryClientOAuth2Interceptor(OAuth2AuthorizedClientManager manager) {
return new OAuthRequestInterceptor(manager);
}
}
And this is my Feign client:
#FeignClient(name = "BackendRepository", configuration = FeignClientConfig.class, url = "${BACKEND_URL}")
public interface BackendRepository {
#GetMapping(path = "/healthChecks", produces = MediaType.APPLICATION_JSON_VALUE)
public Info healthCheck();
}
When running this code, I get the error:
org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse] and content type [text/html;charset=utf-8]
Debugging the code it looks like the DefaultClientCredentialsTokenResponseClient is requesting the auth endpoint using Basic Authentication. Although I never set this up.
Any advise what I can do? Maybe there is a completely different approach to do this.
For this to work with Spring Security 5 and Feign you need to have
a working Spring Security config
a Feign interceptor
a Feign configuration using that interceptor
Working Spring Security Config
Here we will register a generic internal-api client for your oauth2 client credentials. This is where you specify the client-id,client-secret, scopes and grant type.
All basic Spring Security 5 stuff. This also involves setting up a provider (here I am using a custom OpenID Connect provider called "yourprovider"
spring:
security:
oauth2:
client:
registration:
internal-api:
provider: yourprovider
client-id: x
client-secret: y
scope:
- ROLE_ADMIN
authorization-grant-type: client_credentials
provider:
yourprovider:
issuer-uri: yourprovider.issuer-uri
resourceserver:
jwt:
issuer-uri: yourprovider.issuer-uri
Next you need your feign config. This will use a OAuth2FeignRequestInterceptor
public class ServiceToServiceFeignConfiguration extends AbstractFeignConfiguration {
#Bean
public OAuth2FeignRequestInterceptor requestInterceptor() {
return new OAuth2FeignRequestInterceptor(
OAuth2AuthorizeRequest.withClientRegistrationId("internal-api")
.principal(new AnonymousAuthenticationToken("feignClient", "feignClient", createAuthorityList("ROLE_ANONYMOUS")))
.build());
}
}
And a RequestInterceptor that looks like this :
The OAuth2AuthorizedClientManager is a bean that you can configure in your Configuration
public OAuth2AuthorizedClientManager authorizedClientManager(final ClientRegistrationRepository clientRegistrationRepository, final OAuth2AuthorizedClientService authorizedClientService) {
return new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
}
The OAuth2AuthorizeRequest is provided by the Feign Configuration above.
The oAuth2AuthorizedClientManager can authorize the oAuth2AuthorizeRequest, get you the access token, and provide it as an Authorization header to the underlying service
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
#Inject
private OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
private OAuth2AuthorizeRequest oAuth2AuthorizeRequest;
OAuth2FeignRequestInterceptor(OAuth2AuthorizeRequest oAuth2AuthorizeRequest) {
this.oAuth2AuthorizeRequest = oAuth2AuthorizeRequest;
}
#Override
public void apply(RequestTemplate template) {
template.header(AUTHORIZATION,getAuthorizationToken());
}
private String getAuthorizationToken() {
final OAuth2AccessToken accessToken = oAuth2AuthorizedClientManager.authorize(oAuth2AuthorizeRequest).getAccessToken();
return String.format("%s %s", accessToken.getTokenType().getValue(), accessToken.getTokenValue());
}
}
I am quite experienced with Feign and OAuth2 and it took me some good hours to find how to do that.
First, let's say that my app is based on latest Spring libraries, so I am using the following dependencies (managed version for spring-cloud-starter-openfeign is 3.0.0)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
In my application.properties I have the following
security.oauth2.client.access-token-uri=https://api.twitter.com/oauth2/token
security.oauth2.client.client-id=my-secret-twitter-id
security.oauth2.client.client-secret=my-secret-twitter-secret
security.oauth2.client.grant-type=client_credentials
And finally my configuration beans
package es.spanishkangaroo.ttanalyzer.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import feign.RequestInterceptor;
#Configuration
public class FeignClientConfiguration {
#Bean
#ConfigurationProperties(prefix = "security.oauth2.client")
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
return new ClientCredentialsResourceDetails();
}
#Bean
public RequestInterceptor oauth2FeignRequestInterceptor(){
return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails());
}
#Bean
public OAuth2RestTemplate clientCredentialsRestTemplate() {
return new OAuth2RestTemplate(clientCredentialsResourceDetails());
}
}
So then the Feign client is as simple as
package es.spanishkangaroo.ttanalyzer.api;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import es.clovelly.ttanalyzer.model.Trends;
#FeignClient(name = "twitterClient", url = "https://api.twitter.com/1.1/")
public interface TwitterClient {
#GetMapping("/trends/place.json")
Trends[] getTrendsById(#RequestParam Long id);
}
As you may have noticed, the code is automatically getting a token (a bearer token) before the client call. If you are using a non-expiring bearer token you can just use something like
#Bean
public OAuth2ClientContext oAuth2ClientContext() {
DefaultOAuth2ClientContext context = new DefaultOAuth2ClientContext();
context.setAccessToken(bearerToken);
return context;
}
I tried you're approach. Unfortunatelly without success. But this one worked for me: Spring cloud Feign OAuth2 request interceptor is not working. Looks like I use a lot of depredations now, but at least it does work.

swagger UI for Spring Boot API : How to add "audience" in request body for authorising "client credentials" flow

I have generated swagger UI documentation from my spring boot API, the API is secured using oauth2 client credentials grant from auth0.
The problem is that:
In the swagger configuration, I am unable to set the "audience" request body parameter while authorisation.
Thus, swagger ui is not authenticating the API.
I am following this documentation:
https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api
pom.xml:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
SwaggerConfig.Java:
#Configuration
#EnableSwagger2
public class SwaggerConfig {
String token_endpoint = "xxxx";
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("xxxx.controller"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo())
.useDefaultResponseMessages(false)
.securitySchemes(Arrays.asList(securityScheme()))
.securityContexts(Arrays.asList(securityContext()));
}
private ApiInfo apiInfo() {
return new ApiInfo(
"xxxx API",
"Some description of API.",
"xxxx",
"Terms of service",
new Contact("xx", "xxxx", "xxxx"),
"License of API", "xxxx", Collections.emptyList());
}
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
#Bean
public SecurityConfiguration security() {
return SecurityConfigurationBuilder.builder()
.appName("xxxx")
.clientId("")
.clientSecret("")
.build();
}
private SecurityScheme securityScheme() {
GrantType grantType = new ClientCredentialsGrant(token_endpoint);
SecurityScheme oauth = new OAuthBuilder().name("spring_oauth")
.grantTypes(Arrays.asList(grantType))
.build();
return oauth;
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.forPaths(PathSelectors.any())
.build();
}
}
The response is as 403 Forbidden and this is because, I am not able to provide "audience" in the request body during authorization:
"error_description": "Non-global clients are not allowed access to APIv1"
authorization in swagger ui for "application/client credentials grant type

Spring Boot OAuth2 - GoogleApi- Could not obtain user details from token

I am working on a project which has requirements for Gmail Authentication and also which can be extended.
I was following this tutorial here which had examples for Facebook and GitHub authentication. So I tried for Gmail and I am getting this error which I am not able to resolve and getting new exceptions when trying to resolve. Kindly, help me out as I believe this is the point where the code is least affected by my additions. With this much of config and code, it is working for github and fb but not for google.
SocialApplication.java
#SpringBootApplication
#RestController
#EnableOAuth2Client
#EnableAuthorizationServer
#Order(6)
public class SocialApplication extends WebSecurityConfigurerAdapter {
#Autowired
OAuth2ClientContext oauth2ClientContext;
#RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
Map<String, String> map = new LinkedHashMap<>();
map.put("name", principal.getName());
return map;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/**").authorizeRequests().antMatchers("/", "/login**", "/webjars/**").permitAll().anyRequest()
.authenticated().and().exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/")).and().logout()
.logoutSuccessUrl("/").permitAll().and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
// #formatter:on
}
#Configuration
#EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/me").authorizeRequests().anyRequest().authenticated();
// #formatter:on
}
}
public static void main(String[] args) {
SpringApplication.run(SocialApplication.class, args);
}
#Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
#Bean
#ConfigurationProperties("github")
ClientResources github() {
return new ClientResources();
}
#Bean
#ConfigurationProperties("facebook")
ClientResources facebook() {
return new ClientResources();
}
**#Bean
#ConfigurationProperties("gmail")
ClientResources gmail(){return new ClientResources();}**
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(facebook(), "/login/facebook"));
filters.add(ssoFilter(github(), "/login/github"));
**filters.add(ssoFilter(gmail(), "/login/gmail"));**
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter oAuth2ClientAuthenticationFilter = new OAuth2ClientAuthenticationProcessingFilter(
path);
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
oAuth2ClientAuthenticationFilter.setRestTemplate(oAuth2RestTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(client.getResource().getUserInfoUri(),
client.getClient().getClientId());
tokenServices.setRestTemplate(oAuth2RestTemplate);
oAuth2ClientAuthenticationFilter.setTokenServices(tokenServices);
return oAuth2ClientAuthenticationFilter;
}
}
class ClientResources {
private OAuth2ProtectedResourceDetails client = new AuthorizationCodeResourceDetails();
private ResourceServerProperties resource = new ResourceServerProperties();
public OAuth2ProtectedResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
index.html
<div>
With Facebook: click here
</div>
<div>
With Github: click here
</div>
**<div>
With Gmail: click here
</div>**
application.yml // skipped the contents for github and fb to save space
gmail:
client:
client_id: 7xxxxxxxx-1spjexxxxxxxc.apps.googleusercontent.com
scope: https://www.googleapis.com/auth/userinfo.profile
client_secret: Xxxxxxx-I*****zx
userAuthorizationUri: https://accounts.google.com/o/oauth2/auth
accessTokenUri: https://accounts.google.com/o/oauth2/token
auth_provider_x509_cert_url:https://www.googleapis.com/oauth2/v1/certs
LOG
org.springframework.security.authentication.BadCredentialsException:
Could not obtain user details from token at
org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication(OAuth2ClientAuthenticationProcessingFilter.java:122)
~[spring-security-oauth2-2.0.10.RELEASE.jar:na] at
org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212)
[spring-security-web-4.1.1.RELEASE.jar:4.1.1.RELEASE] at
org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:112)
[spring-web-4.3.2.RELEASE.jar:4.3.2.RELEASE] at
org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:200)
[spring-security-web-4.1.1.RELEASE.jar:4.1.1.RELEASE] at
org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:112)
[spring-web-4.3.2.RELEASE.jar:4.3.2.RELEASE] at
org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:200)
[spring-security-web-4.1.1.RELEASE.jar:4.1.1.RELEASE] at
org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:112)
[spring-web-4.3.2.RELEASE.jar:4.3.2.RELEASE] at
org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:73)
[spring-web-4.3.2.RELEASE.jar:4.3.2.RELEASE]
**Caused by:
org.springframework.security.oauth2.common.exceptions.InvalidTokenException:
ya***********dCCnRbsve3
at
org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices.loadAuthentication(UserInfoTokenServices.java:91)
~[spring-boot-autoconfigure-1.4.0.RELEASE.jar:1.4.0.RELEASE] at
org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication(OAuth2ClientAuthenticationProcessingFilter.java:112)
~[spring-security-oauth2-2.0.10.RELEASE.jar:na] ... 66 common frames
omitted
And on Google API Console.
Redirect URL I have given as : localhost:8080/login/gmail
In your application.yml confirguration, I couldn't find the userinfo url defined?
I have the following google config working for me:
google:
client:
clientId: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
clientSecret: YYYYYYYYYYYYYYYYYY
accessTokenUri: https://accounts.google.com/o/oauth2/token
userAuthorizationUri: https://accounts.google.com/o/oauth2/auth
clientAuthenticationScheme: form
scope: profile email
resource:
userInfoUri: https://www.googleapis.com/userinfo/v2/me

Spring Boot OAuth 2.0 UserDetails user not found

I am new to Spring Boot, and I am trying to configure OAuth 2.0. The problem I am having at this moment is that I keep getting the following message when I attempt to request for an access token:
{
"error": "invalid_grant",
"error_description": "Bad credentials"
}
The error message in the Spring Boot console says that the user cannot be found.
: Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider
: User 'stromero' not found
: Returning cached instance of singleton bean 'authenticationAuditListener'
I have implemented a custom user that has already been saved to a database using JPA, I am unable to figure why Spring Security cannot find this user, it may an issue with my logic or configuration. If someone with more experience can look at my code and perhaps guide me to the right direction, that would be greatly appreciated.
This is the HTTP Request:
POST /oauth/token HTTP/1.1
Host: localhost:8181
Authorization: Basic YnJvd3NlcjpzZWNyZXQ=
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
username=stromero&password=password&client_id=browser&client_secret=secret&grant_type=password
These are the classes that I used to implement my custom user and OAuth 2.0
#Repository
public interface UserRepository extends CrudRepository<CustomUser, String> {
public CustomUser findByUsername(String name);
}
Below is the custom user I have created
#Entity
#Table (name = "custom_user")
public class CustomUser {
#Id
#Column(name = "id", nullable = false, updatable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name = "username", unique=true, nullable = false)
private String username;
#Column(name = "password", nullable = false)
private String password;
#ElementCollection
private List<String> roles = new ArrayList<>();
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Below is a customdetails service that reads the user information from the database and returns it as a UserDetails Object
#Service
#Transactional(readOnly = true)
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
CustomUser customUser = userRepository.findByUsername(s);
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
return new User(
customUser .getUsername(),
customUser .getPassword().toLowerCase(),
enabled,
accountNonExpired,
credentialsNonExpired,
accountNonLocked,
getAuthorities(customUser.getRoles()));
}
public Collection<? extends GrantedAuthority> getAuthorities(List<String> roles) {
List<GrantedAuthority> authList = getGrantedAuthorities(roles);
return authList;
}
public static List<GrantedAuthority> getGrantedAuthorities(List<String> roles) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
}
The class below is a data structure that holds both the UserDetailsService and ClientDetailsService
public class ClientAndUserDetailsService implements UserDetailsService,
ClientDetailsService {
private final ClientDetailsService clients;
private final UserDetailsService users;
private final ClientDetailsUserDetailsService clientDetailsWrapper;
public ClientAndUserDetailsService(ClientDetailsService clients,
UserDetailsService users) {
super();
this.clients = clients;
this.users = users;
clientDetailsWrapper = new ClientDetailsUserDetailsService(this.clients);
}
#Override
public ClientDetails loadClientByClientId(String clientId)
throws ClientRegistrationException {
return clients.loadClientByClientId(clientId);
}
#Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = null;
try{
user = users.loadUserByUsername(username);
}catch(UsernameNotFoundException e){
user = clientDetailsWrapper.loadUserByUsername(username);
}
return user;
}
}
The class below is my configuration for OAuth 2.0 using Spring Boot
#Configuration
public class OAuth2SecurityConfiguration {
#Configuration
#EnableWebSecurity
protected static class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
protected void registerAuthentication(
final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
#Configuration
#EnableResourceServer
protected static class ResourceServer extends
ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/oauth/token").anonymous();
// Require all GET requests to have client "read" scope
http.authorizeRequests().antMatchers(HttpMethod.GET, "/**")
.access("#oauth2.hasScope('read')");
// Require all POST requests to have client "write" scope
http.authorizeRequests().antMatchers(HttpMethod.POST,"/**")
.access("#oauth2.hasScope('write')");
}
}
#Configuration
#EnableAuthorizationServer
#Order(Ordered.LOWEST_PRECEDENCE - 100)
protected static class AuthorizationServer extends
AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
private ClientAndUserDetailsService combinedService;
public AuthorizationServer() throws Exception {
ClientDetailsService clientDetailsService = new InMemoryClientDetailsServiceBuilder()
.withClient("browser")
.secret("secret")
.authorizedGrantTypes("password")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read","write")
.resourceIds("message")
.accessTokenValiditySeconds(7200)
.and()
.build();
// Create a series of hard-coded users.
UserDetailsService userDetailsService = new CustomUserDetailsService();
combinedService = new ClientAndUserDetailsService(clientDetailsService, userDetailsService);
}
#Bean
public ClientDetailsService clientDetailsService() throws Exception {
return combinedService;
}
#Bean
public UserDetailsService userDetailsService() {
return combinedService;
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.withClientDetails(clientDetailsService());
}
}
}
Below is my pom.xml file
<properties>
<tomcat.version>8.0.8</tomcat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- Postgres JDBC Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.2-1002-jdbc4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Hibernate validator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>17.0</version>
</dependency>
</dependencies>
Yeah, I had the same issue... wanted to use JPA's UserDetailsService but the same problem - user couldn't be found... got resolved it eventually, thanks to Dave Syer's OAuth2 samples on GitHub.
The problem seem to be in authenticationManager instance autowired in #EnableAuthorizationServer AuthorizationServer class. AuthenticationManager is autowired there and seems to initialize with default DAOAuthenticationProvider, and for some reason it doesn't use custom JPA UserDetailsService we initialize authenticationManager with in WebSecurityConfiguration.
In Dave Syer samples authenticationManager is exposed as a bean in WebSecurityConfiguration:
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
then in AuthorizationServer we autowire authenticationManager as follows:
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
Once I did it I finally managed to get my user authenticated against my customer JPA user repository.
I faced the same issue and spent hours investigating the case. As a workaround, if you are using Spring Boot version 1.1.8.RELEASE, downgrade it to 1.0.2.RELEASE. Things went fine that way but I did not investigate yet the reasons of compatibility issue with Spring Boot version 1.1.8.RELEASE.
InitializeUserDetailsBeanManagerConfigurer has default order as
static final int DEFAULT_ORDER = Ordered.LOWEST_PRECEDENCE - 5000;
So it Initializee DaoAuthenticationProvider before custom one.
#Order(-5001)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { ... }

Resources