We are using spring security oauth2 to obtain token using client credentials grant type. We are not using the application.properties file for specifying the client credentials, instead we are supplying them programmatically.
ClientRegistration clientRegistration = ClientRegistration
.withRegistrationId("test")
.clientId("testclientid")
.clientSecret("testclientsecret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.tokenUri("http://test.tokenuri.com")
.build();
ReactiveClientRegistrationRepository reactiveClientRegistrationRepository = new InMemoryReactiveClientRegistrationRepository(clientRegistration);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
reactiveClientRegistrationRepository,
new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
oauth.setDefaultClientRegistrationId("test");
this.webClient = webClientFactory.getBuilder()
.filter(oauth)
.build();
The code is working fine, but we see a warning that UnAuthenticatedServerOAuth2AuthorizedClientRepository is deprecated.
The api docs for UnAuthenticatedServerOAuth2AuthorizedClientRepository recommend to use AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager instead, but AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager does not implement the same interface as UnAuthenticatedServerOAuth2AuthorizedClientRepository. What is the recommendation on replacing the deprecated UnAuthenticatedServerOAuth2AuthorizedClientRepository in this case?
I found https://github.com/spring-projects/spring-security/issues/8016 but the issue does not give much detail.
With the help of #Jokers answer, I managed to solve this problem in the following way. I put the credentials in appliction.properties and seperated the RegistrationRepository for that.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.web.reactive.function.client.WebClient;
#Configuration
public class MyClientRequestConfig {
#Bean
ReactiveClientRegistrationRepository getRegistration(
#Value("${spring.security.oauth2.client.provider.myprovider.token-uri}") String token_uri,
#Value("${spring.security.oauth2.client.registration.myprovider.client-id}") String client_id,
#Value("${spring.security.oauth2.client.registration.myprovider.client-secret}") String client_secret
) {
ClientRegistration registration = ClientRegistration
.withRegistrationId("myprovider")
.tokenUri(token_uri)
.clientId(client_id)
.clientSecret(client_secret)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
return new InMemoryReactiveClientRegistrationRepository(registration);
}
#Bean(name = "myprovider")
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth.setDefaultClientRegistrationId("myprovider");
return WebClient.builder()
.filter(oauth)
.build();
}
}
Allthough kotlin code (conversion should be easy to java), we ended up with something like that:
val clientRegistryRepo = InMemoryReactiveClientRegistrationRepository(ClientRegistration
.withRegistrationId("test")
.tokenUri("http://test.tokenuri.com")
.clientId("testClientId")
.clientSecret("testclientsecret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build())
val clientService = InMemoryReactiveOAuth2AuthorizedClientService(clientRegistryRepo)
val authorizedClientManager =
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistryRepo, clientService)
val oauthFilter = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
oauthFilter.setDefaultClientRegistrationId("test")
WebClient.builder()
.filter(oauthFilter)
.build()
Related
Planning to use elasticsearch 8.1 version and use 'org.springframework.boot:spring-boot-starter-data-elasticsearch' in
our project.
Repository.save() throws following exception.
java.lang.NullPointerException: null
at java.base/java.util.Objects.requireNonNull(Objects.java:221)
at org.elasticsearch.action.DocWriteResponse.(DocWriteResponse.java:116)
at org.elasticsearch.action.index.IndexResponse.(IndexResponse.java:43)
The same code with Elasticsearch 7.15.2 works fine.
I see the supported matrix here https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#preface.requirements
Where to see the road map of Spring boot elasticsearch data plugin? When Do we get the plugin support for the 8.1 version of
Elasticsearch?
Thanks in advance
Adding the following headers resolved the issue.
Accept: "application/vnd.elasticsearch+json;compatible-with=7"
Content-Type: "application/vnd.elasticsearch+json;compatible-with=7"
Its better to change your code which is used for creating client
package com.search.elasticsearchapp.config;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.message.BasicHeader;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.http.HttpHeaders;
#Configuration
#EnableElasticsearchRepositories(basePackages = "*")
public class TestClient {
#Value("${elasticsearch.host}")
private String host;
#Value("${elasticsearch.port}")
private int port;
#Value("${elasticsearch.protocol}")
private String protocol;
#Value("${elasticsearch.username}")
private String userName;
#Value("${elasticsearch.password}")
private String password;
#Bean(destroyMethod = "close")
public RestHighLevelClient restClient() {
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(userName, password));
RestClientBuilder builder = RestClient.builder(new HttpHost(host, port, protocol))
.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider))
.setDefaultHeaders(compatibilityHeaders());
return new RestHighLevelClient(builder);
}
private Header[] compatibilityHeaders() {
return new Header[]{new BasicHeader(HttpHeaders.ACCEPT, "application/vnd.elasticsearch+json;compatible-with=7"), new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.elasticsearch+json;compatible-with=7")};
}
}
the code that i wrote:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
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.OAuth2RestOperations;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
#EnableOAuth2Client
#Configuration
public class OAuth2SecurityConfig {
private static Logger logger = LoggerFactory.getLogger(OAuth2SecurityConfig.class);
#Value("${tokenUrl}")
private String tokenUrl;
#Value("${oauthClientId}")
private String oauthClientId;
#Value("${oauthClientPassword}")
private String oauthClientPassword;
#Value("${oauthGrantType}")
private String oauthGrantType;
#Bean
protected OAuth2ProtectedResourceDetails resourceDetails() {
logger.info("inside resourceDetails");
ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setAccessTokenUri(tokenUrl);
resourceDetails.setClientId(oauthClientId);
resourceDetails.setClientSecret(oauthClientPassword);
resourceDetails.setGrantType(oauthGrantType);
logger.info("resourceDetails: "+ resourceDetails.toString());
logger.info("resourceDetails: "+ resourceDetails.getAccessTokenUri());
return resourceDetails;
}
#Bean
public OAuth2RestOperations restTemplateToken() {
logger.info("inside restTemplateToken");
AccessTokenRequest request = new DefaultAccessTokenRequest();
return new OAuth2RestTemplate(resourceDetails(), new DefaultOAuth2ClientContext(request));
}
}
The file where I am accessing the tokens I have given this line to fetch it after autowiring OAuth2RestOperations :
#Autowired
private OAuth2RestOperations restTemplateToken;
inside Method....
OAuth2AccessToken accessToken = restTemplateToken.getAccessToken();
When i execute it I am getting error like :
error="access_denied", error_description="Error requesting access token."
Not sure why it is failing. Please help.
Thanks.
We're working on a Spring Boot application that is an OIDC client. The identity provider (IdP) is a third-party service and fully OpenID Connect and OAuth 2.0 compliant (as far as we can tell). As it's built with high security in mind, its UserInfo endpoint returns a signed JWT (instead of a regular one).
It seems that Spring Security does not support it. The authentication flow ends with an error message (displayed in an HTML page generated by our Spring application):
[invalid_user_info_response] An error occurred while attempting to
retrieve the UserInfo Resource: Could not extract response: no
suitable HttpMessageConverter found for response type
[java.util.Map] and content type
[application/jwt;charset=UTF-8]
My questions:
Is it correct that Spring does not currently support UserInfo endpoints returning signed JWTs?
If so, how can we add support for signed JWTs (including verfication of the signature)?
Our analysis has shown that the DefaultOAuth2UserService requests (Accept: application/json) and expects a JSON response from the IdP. However, being configured for high security, the IdP returns a signed JWT with the content type application/jwt. The response looks like the example on jwt.io. As the RestTemplate has no message converter capable of handling the content type application/jwt, the authenticaton fails.
Our sample app is as simple as it gets:
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
DemoApplication.java
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
application.yml
server:
port: 8081
spring:
security:
oauth2:
client:
registration:
demo:
client-id: our-client-id
client-secret: our-client-secret
clientAuthenticationMethod: post
provider: our-idp
scope:
- profile
- email
provider:
our-idp:
issuer-uri: https://login.idp.com:443/idp/oauth2
HomeController.java
package demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class HomeController {
#GetMapping("/")
String hello() { return "hello"; }
}
After more analysis, it seems Spring Boot does not support UserInfo endpoints returning signed JWTs. It's obviously an unusual setup (but still within the OAuth 2.0 / OIDC specification). What I haven't mentioned so far is that the JWT is signed with the client secret.
While Spring Boot doesn't support it, it can be added. The solution consists of:
A user service supporting signed JWTs (as a replacment for DefaultOAuth2UserService)
A HttpMessageConverter supporting JWTs (used in the user service's RestTemplate)
A JwtDecoder using the client secret
A security configuration that puts the pieces together
Note that we have changed from OAuth 2.0 to OIDC in the mean-time, thus our application.yml now includes the openid scope.
spring:
security:
oauth2:
client:
registration:
demo:
client-id: our-client-id
client-secret: our-client-secret
clientAuthenticationMethod: post
provider: our-idp
scope:
- profile
- email
provider:
our-idp:
issuer-uri: https://login.idp.com:443/idp/oauth2
The security configuration is:
package demoapp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(oidcUserService());
}
#Bean
OidcUserService oidcUserService() {
OidcUserService userService = new OidcUserService();
userService.setOauth2UserService(new ValidatingOAuth2UserService(jwtDecoderUsingClientSecret("demo")));
return userService;
}
JwtDecoder jwtDecoderUsingClientSecret(String registrationId) {
ClientRegistration registration = clientRegistrationRepository.findByRegistrationId(registrationId);
SecretKeySpec key = new SecretKeySpec(registration.getClientSecret().getBytes(StandardCharsets.UTF_8), "HS256");
return NimbusJwtDecoder.withSecretKey(key).build();
}
}
If you are using OAuth 2.0 instead of OIDC (i.e. you don't use the scope 'openid'), the configuration is simpler:
package demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(new ValidatingOAuth2UserService(jwtDecoderUsingClientSecret("demo")));
}
JwtDecoder jwtDecoderUsingClientSecret(String registrationId) {
ClientRegistration registration = clientRegistrationRepository.findByRegistrationId(registrationId);
SecretKeySpec key = new SecretKeySpec(registration.getClientSecret().getBytes(StandardCharsets.UTF_8), "HS256");
return NimbusJwtDecoder.withSecretKey(key).build();
}
}
The ValidatingOAuth2UserService class is - for the most part - a copy of DefaultOAuth2UserService:
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package demo;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequestEntityConverter;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* An implementation of an {#link OAuth2UserService} that supports standard OAuth 2.0 Provider's.
* <p>
* This provider supports <i>UserInfo</i> endpoints returning user details
* in signed JWTs (content-type {#code application/jwt}).
* </p>
* <p>
* For standard OAuth 2.0 Provider's, the attribute name used to access the user's name
* from the UserInfo response is required and therefore must be available via
* {#link ClientRegistration.ProviderDetails.UserInfoEndpoint#getUserNameAttributeName() UserInfoEndpoint.getUserNameAttributeName()}.
* <p>
* <b>NOTE:</b> Attribute names are <b>not</b> standardized between providers and therefore will vary.
* Please consult the provider's API documentation for the set of supported user attribute names.
*
* #see org.springframework.security.oauth2.client.userinfo.OAuth2UserService
* #see OAuth2UserRequest
* #see OAuth2User
* #see DefaultOAuth2User
*/
public class ValidatingOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";
private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
private RestOperations restOperations;
private JwtDecoder jwtDecoder;
public ValidatingOAuth2UserService(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
restTemplate.getMessageConverters().add(new JwtHttpMessageConverter());
this.restOperations = restTemplate;
}
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
Assert.notNull(userRequest, "userRequest cannot be null");
if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_USER_INFO_URI_ERROR_CODE,
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: " +
userRequest.getClientRegistration().getRegistrationId(),
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
if (!StringUtils.hasText(userNameAttributeName)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: " +
userRequest.getClientRegistration().getRegistrationId(),
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
ResponseEntity<String> response;
try {
response = this.restOperations.exchange(request, String.class);
} catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
StringBuilder errorDetails = new StringBuilder();
errorDetails.append("Error details: [");
errorDetails.append("UserInfo Uri: ").append(
userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri());
errorDetails.append(", Error Code: ").append(oauth2Error.getErrorCode());
if (oauth2Error.getDescription() != null) {
errorDetails.append(", Error Description: ").append(oauth2Error.getDescription());
}
errorDetails.append("]");
oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
Jwt jwt = decodeAndValidateJwt(response.getBody());
Map<String, Object> userAttributes = jwt.getClaims();
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(userAttributes));
OAuth2AccessToken token = userRequest.getAccessToken();
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}
private Jwt decodeAndValidateJwt(String token) {
return jwtDecoder.decode(token);
}
/**
* Sets the {#link Converter} used for converting the {#link OAuth2UserRequest}
* to a {#link RequestEntity} representation of the UserInfo Request.
*
* #since 5.1
* #param requestEntityConverter the {#link Converter} used for converting to a {#link RequestEntity} representation of the UserInfo Request
*/
public final void setRequestEntityConverter(Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}
/**
* Sets the {#link RestOperations} used when requesting the UserInfo resource.
*
* <p>
* <b>NOTE:</b> At a minimum, the supplied {#code restOperations} must be configured with the following:
* <ol>
* <li>{#link ResponseErrorHandler} - {#link OAuth2ErrorResponseErrorHandler}</li>
* </ol>
*
* #since 5.1
* #param restOperations the {#link RestOperations} used when requesting the UserInfo resource
*/
public final void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
}
And finally the JwtHttpMessageConverter class:
package demo;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
/**
* Message converter for reading JWTs transmitted with content type {#code application/jwt}.
* <p>
* The JWT is returned as a string and not validated.
* </p>
*/
public class JwtHttpMessageConverter extends AbstractGenericHttpMessageConverter<String> {
public JwtHttpMessageConverter() {
super(MediaType.valueOf("application/jwt"));
}
#Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return getBodyAsString(inputMessage.getBody());
}
#Override
public String read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return readInternal(null, inputMessage);
}
private String getBodyAsString(InputStream bodyStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] chunk = new byte[64];
int len;
while ((len = bodyStream.read(chunk)) != -1) {
buffer.write(chunk, 0, len);
}
return buffer.toString(StandardCharsets.US_ASCII);
}
#Override
protected void writeInternal(String stringObjectMap, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
throw new UnsupportedOperationException();
}
}
Thanks #Codo. You saved my day. I did not have to use JwtHttpMessageConverter class though. I added the below in SecurityConfig class. I had to use JwksUri and SignatureAlgorithm.RS512 in my situation.
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.
authorizeRequests().
anyRequest().authenticated().
and().
oauth2Login().userInfoEndpoint().oidcUserService(oidcUserService());
return http.build();
}
OidcUserService oidcUserService() {
logger.info("OidcUserService bean");
OidcUserService userService = new OidcUserService();
//userService.setOauth2UserService(new ExampleOAuth2UserService(jwtDecoderUsingClientSecret("web-client")));
userService.setOauth2UserService(new ExampleOAuth2UserService(jwtDecoder()));
return userService;
}
public JwtDecoder jwtDecoder() {
String jwksUri = Config.getJwksUri();
System.out.println("jwtDecoder jwksUri="+jwksUri);
return NimbusJwtDecoder.withJwkSetUri(jwksUri).jwsAlgorithm(SignatureAlgorithm.RS512).build();
}
public class ExampleOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
...............
...............
private JwtDecoder jwtDecoder;
public ExampleAuth2UserService(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
this.restOperations = restTemplate;
}
I am developing a Service Account application to obtain Token for accessing downstream services. However, I am facing an issue with compilation. Eclipse prompts an error "The type com.google.api.client.auth.oauth2.Credential$Builder cannot be resolved. It is indirectly referenced from required .class files". Code snippet below:
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson.JacksonFactory;
public class AppMain {
public static void main(String[] args) {
// TODO Auto-generated method stub
final HttpTransport TRANSPORT = new NetHttpTransport();
final JsonFactory JSON_FACTORY = new JacksonFactory();
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(TRANSPORT)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId("SOMETHING#developer.gserviceaccount.com")
.setServiceAccountScopes(BigqueryScopes.BIGQUERY)
.setServiceAccountPrivateKeyFromP12File(new File("key.p12"))
.build();
}
}
Please help. Thanks in advance.
I resolved this error with overriding google-api-client dependency to this -
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>1.22.0</version>
</dependency>
I resolved above issue and getting token by adding one more jar in library "google-oauth-client-1.22.0.jar". Below is code snippet
GoogleCredential googleCredentials = GoogleCredential.fromStream(serviceAccount);
GoogleCredential scoped = googleCredentials.createScoped(
Arrays.asList(
"https://www.googleapis.com/auth/firebase.messaging"
)
);
scoped.refreshToken();
String token = scoped.getAccessToken();
Good day,
I was recently testing Oauth2.0 Installed application authentication for bigquery through java api.
I used
this command line sample
as a source since i want to make it for the users eaiser to give access (it would be ugly to ask them to copy the code)
If i use SetAccessType(Offline) in GoogleAuthorizationCodeflow it says, that it automatically refreshes the access token for me.
In related I'd like to ask, if i authorize a com.google.api.services.bigquery.Bigquery instance with
com.google.api.services.bigquery.Bigquery.Builder.Builder(HttpTransport transport, JsonFactory jsonFactory, HttpRequestInitializer httpRequestInitializer)
using
credentials that have a refresh token, do i have to reauthorize it or it will update the credentials inside itself?
If it does not give me a valid connection to bigquery after one hour (token expiration time)
then which is the best method to reauthorize the client?
Shall i use credentials.RefreshToken() and then just Build another bigquery instance whith the new access token, or there is a better way?
(I may also want to change the credentials Refreshlistener, but the problem is that if i instantinate the credential with GoogleAuthorizationCodeflow.createAndStoreCredential(response, Clientid) after that i cannot get the Refreshlistener. How can i get it, so i may use that class to automatically reauthorize my Bigquery Client < maybe this is the better way is it?)
Where does the codeflow store the refreshed access token (if it refreshes it automatically)?
If i declared it like this:
new GoogleAuthorizationCodeFlow.Builder(
transport, jsonFactory, clientSecrets, scopes).setAccessType("offline")
.setApprovalPrompt("auto").setCredentialStore(new MemoryCredentialStore()).build();
Will it store my refreshed access token always in the MemoryCredentialStore?
So if i use .getCredentialStore().load(userId, credential) it will load the refreshed access token from the memorystore?
Is there a way to increase or decrease the time while an access token is valid? Since for testing purposes i really want to do so.
p.s: I was also looking into this source code of google-api-java-client
and google-oauth-java-client but i still couldn't find the sollution.
Most importantly I was looking into: class Credential at code:
public void intercept(HttpRequest request) throws IOException {
lock.lock();...
but I couldnt figure it out actually when this method is called and I eventually got lost in the problem.
Looking forward to your answer:
Attila
There's a lot of questions here. You will want to use the GoogleCredential class to gain a new access token from an existing refresh token, that you well need to store (using whatever method you want, such as MemoryCredentialStore).
The access token only lasts for an hour and there is not way to change this.
Here is an example using the installed flow:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson.JacksonFactory;
import com.google.api.services.bigquery.Bigquery;
import com.google.api.services.bigquery.Bigquery.Datasets;
import com.google.api.services.bigquery.BigqueryScopes;
import com.google.api.services.bigquery.model.DatasetList;
class BigQueryInstalledAuthDemo {
// Change this to your current project ID
private static final String PROJECT_NUMBER = "XXXXXXXXXX";
// Load Client ID/secret from client_secrets.json file.
private static final String CLIENTSECRETS_LOCATION = "client_secrets.json";
static GoogleClientSecrets clientSecrets = loadClientSecrets();
private static final String REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob";
// Objects for handling HTTP transport and JSON formatting of API calls
private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
private static GoogleAuthorizationCodeFlow flow = null;
// BigQuery Client
static Bigquery bigquery;
public static void main(String[] args) throws IOException {
// Attempt to Load existing Refresh Token
String storedRefreshToken = loadRefreshToken();
// Check to see if the an existing refresh token was loaded.
// If so, create a credential and call refreshToken() to get a new
// access token.
if (storedRefreshToken != null) {
// Request a new Access token using the refresh token.
GoogleCredential credential = createCredentialWithRefreshToken(
HTTP_TRANSPORT, JSON_FACTORY, new TokenResponse().setRefreshToken(storedRefreshToken));
credential.refreshToken();
bigquery = buildService(credential);
// If there is no refresh token (or token.properties file), start the OAuth
// authorization flow.
} else {
String authorizeUrl = new GoogleAuthorizationCodeRequestUrl(
clientSecrets,
REDIRECT_URI,
Collections.singleton(BigqueryScopes.BIGQUERY)).setState("").build();
System.out.println("Paste this URL into a web browser to authorize BigQuery Access:\n" + authorizeUrl);
System.out.println("... and type the code you received here: ");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String authorizationCode = in.readLine();
// Exchange the auth code for an access token and refesh token
Credential credential = exchangeCode(authorizationCode);
// Store the refresh token for future use.
storeRefreshToken(credential.getRefreshToken());
bigquery = buildService(credential);
}
// Make API calls using your client.
listDatasets(bigquery, PROJECT_NUMBER);
}
/**
* Builds an authorized BigQuery API client.
*/
private static Bigquery buildService(Credential credential) {
return new Bigquery.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).build();
}
/**
* Build an authorization flow and store it as a static class attribute.
*/
static GoogleAuthorizationCodeFlow getFlow() {
if (flow == null) {
flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT,
JSON_FACTORY,
clientSecrets,
Collections.singleton(BigqueryScopes.BIGQUERY))
.setAccessType("offline").setApprovalPrompt("force").build();
}
return flow;
}
/**
* Exchange the authorization code for OAuth 2.0 credentials.
*/
static Credential exchangeCode(String authorizationCode) throws IOException {
GoogleAuthorizationCodeFlow flow = getFlow();
GoogleTokenResponse response =
flow.newTokenRequest(authorizationCode).setRedirectUri(REDIRECT_URI).execute();
return flow.createAndStoreCredential(response, null);
}
/**
* No need to go through OAuth dance, get an access token using the
* existing refresh token.
*/
public static GoogleCredential createCredentialWithRefreshToken(HttpTransport transport,
JsonFactory jsonFactory, TokenResponse tokenResponse) {
return new GoogleCredential.Builder().setTransport(transport)
.setJsonFactory(jsonFactory)
.setClientSecrets(clientSecrets)
.build()
.setFromTokenResponse(tokenResponse);
}
/**
* Helper to load client ID/Secret from file.
*/
private static GoogleClientSecrets loadClientSecrets() {
try {
GoogleClientSecrets clientSecrets =
GoogleClientSecrets.load(new JacksonFactory(),
BigQueryInstalledAuthDemo.class.getResourceAsStream(CLIENTSECRETS_LOCATION));
return clientSecrets;
} catch (Exception e) {
System.out.println("Could not load clientsecrets.json");
e.printStackTrace();
}
return clientSecrets;
}
/**
* Helper to store a new refresh token in token.properties file.
*/
private static void storeRefreshToken(String refresh_token) {
Properties properties = new Properties();
properties.setProperty("refreshtoken", refresh_token);
System.out.println(properties.get("refreshtoken"));
try {
properties.store(new FileOutputStream("token.properties"), null);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Helper to load refresh token from the token.properties file.
*/
private static String loadRefreshToken(){
Properties properties = new Properties();
try {
properties.load(new FileInputStream("token.properties"));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return (String) properties.get("refreshtoken");
}
/**
*
* List available Datasets.
*/
public static void listDatasets(Bigquery bigquery, String projectId)
throws IOException {
Datasets.List datasetRequest = bigquery.datasets().list(projectId);
DatasetList datasetList = datasetRequest.execute();
if (datasetList.getDatasets() != null) {
List<DatasetList.Datasets> datasets = datasetList.getDatasets();
System.out.println("Available datasets\n----------------");
for (com.google.api.services.bigquery.model.DatasetList.Datasets dataset : datasets) {
System.out.format("%s\n", dataset.getDatasetReference().getDatasetId());
}
}
}
}
An alternative to authorization via storing and using a refresh token to acquire a new access token in your installed application is to use a server to server service account authorization flow. In this case, your application will need to be able to securely store and use a unique private key. Here is an example of this type of flow using the Google Java API Client:
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson.JacksonFactory;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.services.bigquery.Bigquery;
import com.google.api.services.bigquery.Bigquery.Datasets;
import com.google.api.services.bigquery.model.DatasetList;
import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
public class JavaCommandLineServiceAccounts {
private static final String SCOPE = "https://www.googleapis.com/auth/bigquery";
private static final HttpTransport TRANSPORT = new NetHttpTransport();
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
private static Bigquery bigquery;
public static void main(String[] args) throws IOException, GeneralSecurityException {
GoogleCredential credential = new GoogleCredential.Builder().setTransport(TRANSPORT)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId("XXXXXXX#developer.gserviceaccount.com")
.setServiceAccountScopes(SCOPE)
.setServiceAccountPrivateKeyFromP12File(new File("my_file.p12"))
.build();
bigquery = new Bigquery.Builder(TRANSPORT, JSON_FACTORY, credential)
.setApplicationName("BigQuery-Service-Accounts/0.1")
.setHttpRequestInitializer(credential).build();
Datasets.List datasetRequest = bigquery.datasets().list("publicdata");
DatasetList datasetList = datasetRequest.execute();
System.out.format("%s\n", datasetList.toPrettyString());
}
}