Unable to link Swagger-ui to my swagger Spring mvc project - java

I'm currently creating an API Rest using Eclipse, Spring Framework MVC, and I just added to my project swagger. I can access the json result of swagger but I need to add swagger ui.
Here are all my files creating for swagger-springmvc:
WebAppInitializer.java
public class WebAppInitializer implements WebApplicationInitializer {
private AnnotationConfigWebApplicationContext ctx = null;
#Override
public void onStartup(final ServletContext sc) throws ServletException {
System.setProperty("spring.profiles.active", "web");
// Create the 'root' Spring application context
ctx = new AnnotationConfigWebApplicationContext();
ctx.register(SpringBaseWebConfiguration.class,
SpringSwaggerConfiguration.class);
// Manages the lifecycle
sc.addListener(new ContextLoaderListener(ctx));
sc.addListener(new ContextCleanupListener());
// Spring WebMVC
ServletRegistration.Dynamic springWebMvc = sc.addServlet("ws",
new DispatcherServlet(ctx));
springWebMvc.setLoadOnStartup(1);
springWebMvc.addMapping("/ws/*");
springWebMvc.setAsyncSupported(true);
}
#PreDestroy
protected final void cleanup() {
if (ctx != null) {
ctx.close();
}
}
}
SpringSwaggerConfiguration.java
public class SpringSwaggerConfiguration {
public static final List<String> DEFAULT_INCLUDE_PATTERNS = Arrays
.asList(new String[]{"/com/sa/rnd/dark/resources/.*"});
public static final String SWAGGER_GROUP = "ApiDark";
public static final String RELATIVE_GROUP = "ApiDark";
#Autowired
private SpringSwaggerConfig springSwaggerConfig;
/**
* Adds the jackson scala module to the MappingJackson2HttpMessageConverter
* registered with spring Swagger core models are scala so we need to be
* able to convert to JSON Also registers some custom serializers needed to
* transform swagger models to swagger-ui required json format
*/
#Bean
public JacksonScalaSupport jacksonScalaSupport() {
final JacksonScalaSupport jacksonScalaSupport = new JacksonScalaSupport();
// Set to false to disable
jacksonScalaSupport.setRegisterScalaModule(true);
return jacksonScalaSupport;
}
/**
* Global swagger settings
*/
#Bean
public SwaggerGlobalSettings swaggerGlobalSettings() {
final SwaggerGlobalSettings swaggerGlobalSettings = new SwaggerGlobalSettings();
swaggerGlobalSettings.setGlobalResponseMessages(springSwaggerConfig
.defaultResponseMessages());
swaggerGlobalSettings.setIgnorableParameterTypes(springSwaggerConfig
.defaultIgnorableParameterTypes());
return swaggerGlobalSettings;
}
/**
* API Info as it appears on the swagger-ui page
*/
private ApiInfo apiInfo() {
return new ApiInfo(
"Swagger Spring MVC for Dark Api",
"Sample application demonstrating how to use swagger-springmvc in a no-XML environment.",
"http://en.wikipedia.org/wiki/Terms_of_service",
"michael#laccetti.com", "Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0.html");
}
/**
* Configure a SwaggerApiResourceListing for each swagger instance within
* your app. e.g. 1. private 2. external apis Required to be a spring bean
* as spring will call the postConstruct method to bootstrap swagger
* scanning.
*
* #return
*/
#Bean
public SwaggerApiResourceListing swaggerApiResourceListing() {
// The group name is important and should match the group set on
// ApiListingReferenceScanner
// Note that swaggerCache() is by DefaultSwaggerController to serve the
// swagger json
final SwaggerApiResourceListing swaggerApiResourceListing = new SwaggerApiResourceListing(
springSwaggerConfig.swaggerCache(), SWAGGER_GROUP);
// Set the required swagger settings
swaggerApiResourceListing
.setSwaggerGlobalSettings(swaggerGlobalSettings());
// Supply the API Info as it should appear on swagger-ui web page
swaggerApiResourceListing.setApiInfo(apiInfo());
// Use the default path provider
swaggerApiResourceListing.setSwaggerPathProvider(springSwaggerConfig
.defaultSwaggerPathProvider());
// Global authorization - see the swagger documentation
swaggerApiResourceListing.setAuthorizationTypes(authorizationTypes());
// Sets up an auth context - i.e. which controller request paths to
// apply global auth to
swaggerApiResourceListing
.setAuthorizationContext(authorizationContext());
// Every SwaggerApiResourceListing needs an ApiListingReferenceScanner
// to scan the spring request mappings
swaggerApiResourceListing
.setApiListingReferenceScanner(apiListingReferenceScanner());
return swaggerApiResourceListing;
}
#Bean
/**
* The ApiListingReferenceScanner does most of the work.
* Scans the appropriate spring RequestMappingHandlerMappings
* Applies the correct absolute paths to the generated swagger resources
*/
public ApiListingReferenceScanner apiListingReferenceScanner() {
ApiListingReferenceScanner apiListingReferenceScanner = new ApiListingReferenceScanner();
// Picks up all of the registered spring RequestMappingHandlerMappings
// for
// scanning
apiListingReferenceScanner
.setRequestMappingHandlerMapping(springSwaggerConfig
.swaggerRequestMappingHandlerMappings());
// Excludes any controllers with the supplied annotations
apiListingReferenceScanner.setExcludeAnnotations(springSwaggerConfig
.defaultExcludeAnnotations());
//
apiListingReferenceScanner
.setResourceGroupingStrategy(springSwaggerConfig
.defaultResourceGroupingStrategy());
// Path provider used to generate the appropriate uri's
apiListingReferenceScanner
.setSwaggerPathProvider(relativeSwaggerPathProvider());
// Must match the swagger group set on the SwaggerApiResourceListing
apiListingReferenceScanner.setSwaggerGroup(SWAGGER_GROUP);
// Only include paths that match the supplied regular expressions
apiListingReferenceScanner.setIncludePatterns(DEFAULT_INCLUDE_PATTERNS);
return apiListingReferenceScanner;
}
private List<AuthorizationType> authorizationTypes() {
final List<AuthorizationType> authorizationTypes = new ArrayList<>();
authorizationTypes.add(new BasicAuth());
return authorizationTypes;
}
#Bean
public AuthorizationContext authorizationContext() {
final List<Authorization> authorizations = newArrayList();
AuthorizationScope authorizationScope = new AuthorizationScope(
"global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[]{authorizationScope};
authorizations.add(new Authorization("basic", authorizationScopes));
AuthorizationContext authorizationContext = new AuthorizationContext.AuthorizationContextBuilder(
authorizations).withIncludePatterns(DEFAULT_INCLUDE_PATTERNS)
.build();
return authorizationContext;
}
// Relative path example
#Bean
public SwaggerApiResourceListing relativeSwaggerApiResourceListing() {
SwaggerApiResourceListing swaggerApiResourceListing = new SwaggerApiResourceListing(
springSwaggerConfig.swaggerCache(), RELATIVE_GROUP);
swaggerApiResourceListing
.setSwaggerGlobalSettings(swaggerGlobalSettings());
swaggerApiResourceListing
.setSwaggerPathProvider(relativeSwaggerPathProvider());
swaggerApiResourceListing
.setApiListingReferenceScanner(relativeApiListingReferenceScanner());
return swaggerApiResourceListing;
}
#Bean
public ApiListingReferenceScanner relativeApiListingReferenceScanner() {
ApiListingReferenceScanner apiListingReferenceScanner =
new ApiListingReferenceScanner();
apiListingReferenceScanner
.setRequestMappingHandlerMapping(springSwaggerConfig
.swaggerRequestMappingHandlerMappings());
apiListingReferenceScanner.setExcludeAnnotations(springSwaggerConfig
.defaultExcludeAnnotations());
apiListingReferenceScanner
.setResourceGroupingStrategy(springSwaggerConfig
.defaultResourceGroupingStrategy());
apiListingReferenceScanner
.setSwaggerPathProvider(relativeSwaggerPathProvider());
apiListingReferenceScanner.setSwaggerGroup(RELATIVE_GROUP);
apiListingReferenceScanner.setIncludePatterns(DEFAULT_INCLUDE_PATTERNS);
return apiListingReferenceScanner;
}
#Bean
public SwaggerPathProvider relativeSwaggerPathProvider() {
return new ApiRelativeSwaggerPathProvider();
}
private class ApiRelativeSwaggerPathProvider extends
DefaultSwaggerPathProvider {
#Override
public String getAppBasePath() {
return "/ApiDark/ws";
}
}
}
SpringBaseWebConfiguration.java :
#Configuration
#ComponentScan(basePackages = {"com.sa.rnd.dark.resources",
"com.mangofactory.swagger.configuration",
"com.mangofactory.swagger.controllers"})
#EnableWebMvc
public class SpringBaseWebConfiguration extends WebMvcConfigurerAdapter {
private List<HttpMessageConverter<?>> messageConverters;
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("/");
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/api-docs").setViewName("redirect:index.html");
}
/**
* The message converters for the content types we support.
*
* #return the message converters; returns the same list on subsequent calls
*/
private List<HttpMessageConverter<?>> getMessageConverters() {
if (messageConverters == null) {
messageConverters = new ArrayList<>();
final MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter = new MappingJackson2HttpMessageConverter();
final ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
false);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
mappingJacksonHttpMessageConverter.setObjectMapper(mapper);
messageConverters.add(mappingJacksonHttpMessageConverter);
}
return messageConverters;
}
#Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) {
converters.addAll(getMessageConverters());
}
#Bean
public static PropertyPlaceholderConfigurer swaggerProperties() {
final PropertyPlaceholderConfigurer swaggerProperties = new PropertyPlaceholderConfigurer();
swaggerProperties.setLocation(new ClassPathResource(
"swagger.properties"));
return swaggerProperties;
}
}
Here are my 3 files to add swagger to my project, atm I just decide to check just 1 method which is :
#Api(description = "CRUD services for containers working with WebDark",
value = "CRUD Services Containers")
#RestController
#RequestMapping(value = "/container", produces = MediaType.APPLICATION_JSON_VALUE)
public class ContainersResources {
/**
* Find all children of a container.
*
* #param containerId
* ID of the parent container
* #return ApiResponse
*/
#ApiOperation(response = ApiResponse.class,
value = "Find all children containers of one container",
notes = "Find all children containers of one container")
#RequestMapping(method = RequestMethod.GET, value = "/{containerId}")
#ResponseStatus(HttpStatus.OK)
public #ResponseBody ApiResponse<List<ContainerModel>> getContainerChildren(
#ApiParam(required = true, value = "The id of the container parent",
name = "containerId")#PathVariable("containerId") final String containerId) {
ApiResponse<List<ContainerModel>> result = new ApiResponse<>();
result.setMessage("getContainerChildren method of new Api Dark");
result.setSuccess(true);
result.setTotal(9000);
return result;
}
}
My results : I can access the following url http://localhost:8080/ApiDark/ws/api-docs but I get the json value like :
{"apiVersion":"1","swaggerVersion":"1.2","authorizations":{"basicAuth":{"type":"basicAuth"}},"info":{"title":"Swagger Spring MVC for Dark Api","description":"Sample application demonstrating how to use swagger-springmvc in a no-XML environment.","termsOfServiceUrl":"http://en.wikipedia.org/wiki/Terms_of_service","contact":"michael#laccetti.com","license":"Apache 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.html"}}
That's why I decided to add swagger-ui. I add the content of dist folder (taken from swagger-ui) to my src/main/webapp folder. And modify the content of index.html to point to my url :
<script type="text/javascript">
$(function () {
window.swaggerUi = new SwaggerUi({
url: "http://localhost:8080/ApiDark/ws/api-docs.json",
dom_id: "swagger-ui-container",
But I can't access to swagger-ui interface, I just have the json result... Need help to make it work please !

This issue seems old but just to share, with the recent version of swagger-springmvc and springmvc-ui, it has become very easy and less complicated to integrate swagger & swagger-ui with your web service to see in production REST api documentation.
#EnableSwagger annotation is added that enables swagger-springmvc out of the box. The generated swagger json Resource Listing can be accessed at /api-docs.
You may refer to following link for the steps on integrating swagger-springmvc and swagger-ui with your spring mvc project.
https://github.com/martypitt/swagger-springmvc

What do you mean by 'I can't access to swagger-ui interface'?
In my case, my browser did not load the index.html (I think it throw 404), which I had in a subfolder together with all the dist files: webapp/doc/index.html. I had to exclude the subfolder from the servlet mapping in web.xml as I shortly described here:
Java swagger with JaxRS throwing errors
My web.xml snippet looks like this finally:
<servlet-mapping>
<servlet-name>My application servlet-name</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/doc/*</url-pattern>
</servlet-mapping>
(why this works see here:http://blog.ericdaugherty.com/2010/02/excluding-content-from-url-pattern-in.html)
Hope that helps!

Related

SpringBoot-OpenAPI: How to add default header to all requests

I have created an OpenAPI spec as below.
#Bean
public OpenAPI customOpenAPI() {
final String securitySchemeName = "Authorization";
final String apiTitle = "My Service";
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
.components(
new Components()
.addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)))
.info(new Info().title(apiTitle));
}
Now, I want OpenAPI UI to add one extra header MyCustomHeader:MyValue to all the requests. How can I achieve this?
You can define OpenApiCustomiser bean as follows:
#Bean
public OpenApiCustomiser openApiCustomizer() {
return openApi ->
openApi.getPaths().values().stream()
.flatMap(pathItem -> pathItem.readOperations().stream())
.forEach(
operation ->
operation.addParametersItem(
new HeaderParameter()
.schema(new StringSchema()._default("MyValue"))
.name("MyCustomHeader")));
}
The bean adds the MyCustomHeader header to all endpoints with MyValue
as default value.

Is there a way to automatically generate the java test code using swagger codegen?

I've been playing with swagger codegen for a springboot project. I am now able to automatically generate the client code in java, ignore the generation of the data models (importing my own in the swagger-codegen-maven-plugin via importMapping).
Furthermore, I am also able to:
specify tags directly in the springboot code to help organize the openapi contract,
use the #Operation annotation to specify method names,
use the #ApiResponses annotation to detail the expected response codes and descriptions,etc.
Example below:
#RestController
#RequestMapping(value="/api/external/")
#Tag(name = "addResources", description = "The Add Resources API")
public class ControllerAddResources {
private final ServiceInterAddResources externalSources;
public ControllerAddResources(
ServiceInterAddResources externalSources){
this.externalSources = externalSources;
}
#Operation(operationId="importDataV3", summary = "Import Data V3", description = "Import a Data V3 file from source", tags = { "addResources" })
#PostMapping(path="/{source}/datav3", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public #ResponseBody String importDataV3(
#PathVariable String source,
MultipartFile file) throws ExceptionInvalidDataV3, IOException{
return externalSources.importDataV3(source, file);
}
All of this is built into the openapi contract which is then used by codegen to generate the client library (in this case, I'm using resttemplate), such as the one below:
#javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.JavaClientCodegen", date = "2020-03-05T12:15:11.537Z[Europe/Lisbon]")#Component("com.xxx.connector.api.AddResourcesApi")
public class AddResourcesApi {
private ApiClient apiClient;
public AddResourcesApi() {
this(new ApiClient());
}
#Autowired
public AddResourcesApi(ApiClient apiClient) {
this.apiClient = apiClient;
}
public ApiClient getApiClient() {
return apiClient;
}
public void setApiClient(ApiClient apiClient) {
this.apiClient = apiClient;
}
/**
* Import Data V3
* Import a Data V3 file from source
* <p><b>412</b> - default response
* <p><b>409</b> - default response
* <p><b>200</b> - default response
* #param source The source parameter
* #param file The file parameter
* #return String
* #throws RestClientException if an error occurs while attempting to invoke the API
*/
public String importDataV3(String source, File file) throws RestClientException {
Object postBody = null;
// verify the required parameter 'source' is set
if (source == null) {
throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Missing the required parameter 'source' when calling importDataV3");
}
// create path and map variables
final Map<String, Object> uriVariables = new HashMap<String, Object>();
uriVariables.put("source", source);
String path = UriComponentsBuilder.fromPath("/api/external/{source}/datav3").buildAndExpand(uriVariables).toUriString();
final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<String, String>();
final HttpHeaders headerParams = new HttpHeaders();
final MultiValueMap<String, Object> formParams = new LinkedMultiValueMap<String, Object>();
if (file != null)
formParams.add("file", new FileSystemResource(file));
final String[] accepts = {
"*/*", "application/json"
};
final List<MediaType> accept = apiClient.selectHeaderAccept(accepts);
final String[] contentTypes = {
"multipart/form-data"
};
final MediaType contentType = apiClient.selectHeaderContentType(contentTypes);
String[] authNames = new String[] { };
ParameterizedTypeReference<String> returnType = new ParameterizedTypeReference<String>() {};
return apiClient.invokeAPI(path, HttpMethod.POST, queryParams, postBody, headerParams, formParams, accept, contentType, authNames, returnType);
}
In the generated sources, codegen also includes scaffolds for junit tests, like the one below:
/**
* API tests for AddResourcesApi
*/
#Ignore
public class AddResourcesApiTest {
private final AddResourcesApi api = new AddResourcesApi();
/**
* Import Data V2
*
* Import an Data V2 file from source
*
* #throws ApiException
* if the Api call fails
*/
#Test
public void imporDataV2Test() {
String source = null;
File file = null;
String response = api.importDataV2(source, file);
// TODO: test validations
}
/**
* Import Data V3
*
* Import an Data V3 file from source [%source%]
*
* #throws ApiException
* if the Api call fails
*/
#Test
public void importDataV3Test() {
String source = null;
File file = null;
String response = api.importDataV3(source, file);
// TODO: test validations
}
}
However, the validation code is empty, as expected. Since the client is being continuously generated in a CI/CD environment and being deployed to a dependency management system that I'm running internally (Artifactory), this defeats the purpose of manually writing the tests each time I execute codegen.
Is there a way of specifying the validation code (or validation requisites) directly using java annotations (or a templating mechanism) at the springboot project level? This would allow for fully automated client library generation with testing included.

Feign Registration - Spring Cloud - Change Target without ribbon over-ride

Introduction
I would like to be able to have two different spring profiles, and depending on the profile to change to a hardcoded address for our feign builders.
Currently was have the following:
return builder.target(cls, "http://" + serviceName);
But I would actually like to do the following and over-ride the address:
return builder.target(cls, "http://our-server:8009/" + serviceName);
Why
Sometimes we don't want to run all the services within our development environment. Additionally, some of the services are only available through a zuul gateway sometimes.
So we run the same code in different situations and conditions.
Technical Details
We have the following code that we use for building our Feign Clients.
We had been using the #FeignClient annotation in the past, but lately we decided to start building our feignClients manually.
Example below:
#FeignClient(name = "ab-document-store", configuration = MultiPartSupportConfiguration.class, fallback = DocumentStoreFallback.class)
We call the feignRegistrar class with the following command:
return registerFeignClient(DocumentStoreClient.class, true);
#RequiredArgsConstructor
//#Component
#Slf4j
public class FeignRegistrar {
#Autowired
private Decoder decoder;
#Autowired
private Encoder encoder;
#Autowired
private Client client;
#Autowired
private Contract feignContract;
#Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
#Autowired
private List<RequestInterceptor> interceptors;
public <T> T register(Class<T> cls, String serviceName, boolean isDocumentStore) {
if(isDocumentStore){
encoder = new MultipartFormEncoder(new SpringEncoder(messageConverters));
}
//Client trustSSLSockets = new Client.Default(getSSLSocketFactory(), new NoopHostnameVerifier());
Feign.Builder builder = Feign.builder()
.client(client)
.encoder(encoder)
.decoder(decoder)
.contract(feignContract)
.logger(new Slf4Logger())
.logLevel(Logger.Level.HEADERS);
builder.requestInterceptor(new RequestInterceptor() {
#Override
public void apply(RequestTemplate template) {
template.header("X-Service-Name", serviceName);
}
});
for(RequestInterceptor interceptor : interceptors) {
builder.requestInterceptor(interceptor);
}
log.debug("Registering {} - as feign proxy ", serviceName);
return builder.target(cls, "http://" + serviceName);
}
public static class Slf4Logger extends Logger {
#Override
protected void log(String configKey, String format, Object... args) {
log.info("{} - {}", configKey, args);
}
}
}
Spring Cloud Property Over-ride
We have also been using property files such as application-ENV.property with entries such as the following:
ab-document-store.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
ab-document-store.ribbon.listOfServers: localhost:8025
Unfortunately, listOfServers is not enough for us. We would like to be able to assign a directory/path as well. Something like:
ab-document-store.ribbon.listOfServers: localhost:8025/ab-document-store
Otherworkaround
I have thought about sneaking in a header into all requests such as X-SERVICE-NAME using a feign interceptor. Then we could point all services to an address (e.g. localhost:9001) , and forward/proxy those requests to localhost:9001/X-SERVICE-NAME.
However, I would prefer a much easier solution such as:
ab-document-store.ribbon.listOfServers: localhost:8025/ab-document-store
But this doesn't work :(
Introduction
I found a solution for this using a proxy that detects a header.
So, I have a feign interceptor on the java-side that attaches a header x-service-name to every feign-request.
I also have a NodeJS proxy, that analyzes requests, finds x-service-name, and re-writes the requests to become: x-service-name/originalRequestPath.
This allows me to have all the microservices behind a zuul gateway but also access them using a eureka-over-ride.
Java-Feign-Interceptor
Feign.Builder builder = Feign.builder()
.client(client)
.encoder(usedEncoder)
.decoder(decoder)
.contract(feignContract)
.logger(new Slf4Logger())
.logLevel(Logger.Level.HEADERS);
builder.requestInterceptor(new RequestInterceptor() {
#Override
public void apply(RequestTemplate template) {
template.header("X-Service-Name", serviceName);
}
});
NodeJS proxy
In the example, my zuul gateway ( or another proxy ) is on localhost:9001.
I'm listening on localhost:1200 .
let enableProxyForJava = process.env.ENABLE_PROXY_FOR_JAVA;
if (enableProxyForJava != undefined && enableProxyForJava.toLowerCase() === 'true') {
var httpProxyJava = require('http-proxy');
var proxyJava = httpProxyJava.createProxy();
gutil.log( gutil.colors.green('Enabling Proxy for Java. Set your Eureka overrides to localhost:1200.') );
require('http').createServer(function(req, res) {
console.log("req.headers['x-service-name'] = " + req.headers['x-service-name']);
console.log("Before req.url:"+ req.url);
if( req.headers['x-service-name'] != undefined){
let change = req.headers['x-service-name'] +req.url;
console.log("After req.url:"+ change);
req.url = change;
}
proxyJava.web(req, res, {
target: 'http://localhost:9001/'
});
}).listen(1200);
}
Property file inside Java Application that has feign clients
mbak-microservice1.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
mbak-microservice1.ribbon.listOfServers: localhost:1200
mbak-microservice2.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
mbak-microservice2.ribbon.listOfServers: localhost:1200
mbak-document-store.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
mbak-document-store.ribbon.listOfServers: localhost:1200

How to configure OAuth2 RestTemplate for spring boot config client

How can I configure a spring boot config client microservice to fetch its configuration from an OAuth2 configServer which is #EnableResourceServer ?
I have one OAuth2 Authorization server (#EnableAuthorizationServer). There is a configServer (#EnableConfigServer) which I have configured to respond only to valid requests authorized by authorization server containing JWT tokens.
There is also a microservice client APP1 of the config server which needs to fetch its configuration upon startup from the aforementioned config server. Since the server only responds to requests containing valid access tokens (jwt tokens) I tried to inject OAuth2RestTemplate into ConfigServicePropertySourceLocator so that my config client (APP1) could fetch its config.
In order to do that I tried the partial solution which was discussed here.
This is my OAuth2 ready RestTemplate that I want to inject
#Configuration
public class OAuthConfig {
#Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oauth2ClientContext);
}}
and this is my custom property locator
public class CustomConfigServicePropertySourceLocator {
#Autowired
private ConfigurableEnvironment environment;
#Autowired
private RestTemplate restTemplate;
#Bean
public ConfigClientProperties configClientProperties() {
ConfigClientProperties client = new ConfigClientProperties(this.environment);
client.setEnabled(false);
return client;
}
#Bean
#Primary
public ConfigServicePropertySourceLocator configServicePropertySourceLocator() {
ConfigClientProperties clientProperties = configClientProperties();
ConfigServicePropertySourceLocator configServicePropertySourceLocator = new ConfigServicePropertySourceLocator(
clientProperties);
configServicePropertySourceLocator.setRestTemplate(restTemplate);
return configServicePropertySourceLocator;
}}
I Followed the instructions in Customizing the Bootstrap Configuration
I created a META_INF > spring.factories file containing
org.springframework.cloud.bootstrap.BootstrapConfiguration=com.company.mcapp.CustomConfigServicePropertySourceLocator
By debugging I can see that this custom locator will get called but my APP1 is failing to contact config server to fetch the configurations.
In initialize method of PropertySourceBootstrapConfiguration (below) I can see that the propertySourceLocators does not contain my CustomConfigServicePropertySourceLocator.
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
CompositePropertySource composite = new CompositePropertySource(
BOOTSTRAP_PROPERTY_SOURCE_NAME);
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySourceLocator locator : this.propertySourceLocators) {
PropertySource<?> source = null;
source = locator.locate(environment);
if (source == null) {
continue;
}
logger.info("Located property source: " + source);
composite.addPropertySource(source);
empty = false;
}
.
.
.
UPDATE: The issue was a silly mistake That I made. Instead of creating META-INF I created META_INF.

How to set hosted domain parameter in Google OAuth2 AccountChooser with Java config?

Currently I have working OAuth2 authentication flow with following Java config:
#Configuration
#EnableOAuth2Client
#Import(SecurityWebAppInitializer.class)
public class OAuth2SecurityConfiguration {
#Resource
private GoogleClientSecrets googleClientSecrets;
#Resource
private AccessTokenRequest accessTokenRequest;
#Bean
public AuthorizationCodeResourceDetails googleResource() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
GoogleClientSecrets.Details web = googleClientSecrets.getWeb();
details.setId((String) web.get("project_id"));
details.setClientId(web.getClientId());
details.setClientSecret(web.getClientSecret());
details.setAccessTokenUri(web.getTokenUri());
details.setUserAuthorizationUri(web.getAuthUri());
details.setTokenName((String) web.get("token_name"));
String commaSeparatedScopes = (String) web.get("scope");
details.setScope(parseScopes(commaSeparatedScopes));
details.setPreEstablishedRedirectUri(web.getRedirectUris().get(0));
details.setUseCurrentUri(false);
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
return details;
}
private List<String> parseScopes(String commaSeparatedScopes) {
List<String> scopes = newArrayList();
Collections.addAll(scopes, commaSeparatedScopes.split(","));
return scopes;
}
#Bean
public OAuth2ClientAuthenticationProcessingFilter oAuth2AuthenticationProcessingFilter(
OAuth2RestTemplate oAuth2RestTemplate, ResourceServerTokenServices resourceServerTokenServices) {
OAuth2ClientAuthenticationProcessingFilter filter =
new OAuth2ClientAuthenticationProcessingFilter("/googleLogin");
filter.setRestTemplate(oAuth2RestTemplate);
filter.setTokenServices(resourceServerTokenServices);
return filter;
}
#Bean
public UserAuthenticationConverter userTokenConverter() {
return new DefaultUserAuthenticationConverter();
}
#Bean
public AccessTokenConverter accessTokenConverter(UserAuthenticationConverter userTokenConverter) {
GoogleAccessTokenConverter accessTokenConverter = new GoogleAccessTokenConverter();
accessTokenConverter.setUserTokenConverter(userTokenConverter);
return new GoogleAccessTokenConverter();
}
#Bean
public GoogleTokenServices tokenServices(AccessTokenConverter accessTokenConverter) {
GoogleTokenServices tokenServices = new GoogleTokenServices();
GoogleClientSecrets.Details web = googleClientSecrets.getWeb();
tokenServices.setCheckTokenEndpointUrl("https://www.googleapis.com/oauth2/v1/tokeninfo");
tokenServices.setClientId(web.getClientId());
tokenServices.setClientSecret(web.getClientSecret());
tokenServices.setAccessTokenConverter(accessTokenConverter);
return tokenServices;
}
#Bean
public OAuth2RestTemplate googleRestTemplate() {
return new OAuth2RestTemplate(googleResource(), new DefaultOAuth2ClientContext(accessTokenRequest));
}
#Bean
public OAuth2ClientContextFilter oauth2ClientContextFilter() {
return new OAuth2ClientContextFilter();
}
#Bean
public LoginUrlAuthenticationEntryPoint clientAuthenticationEntryPoint() {
return new LoginUrlAuthenticationEntryPoint("/googleLogin");
}
}
and client_secret.json for my google service:
{
"web": {
"client_id": "...",
"project_id": "...",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": "...",
"redirect_uris": [
"http://localhost:8888/googleLogin",
"http://localhost:8888/googleLogin/"
],
"scope": "https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/userinfo.profile",
"token_name": "authorization_code"
}
}
The problem is I don't know where to set hosted domain parameter in java code. I was trying to find solution over the stackoverflow, but only saw solutions with JS or PHP.
I found out AuthorizationCodeAccessTokenProvider#getRedirectForAuthorization is responsible for constructing redirect URI but available parameters there are limited so the only choice is to add HD parameter to auth_uri in client_secrets.json:
"auth_uri": "https://accounts.google.com/o/oauth2/auth?hd=<domain>",
Adding the hd parameter to the URL worked. Adding ?hd=MYDOMAIN.com to the security.oauth2.client.user-authorization-uri property did the trick.
This basic config works for me in application.properties - you can do that or yaml. When the login request goes to show the google login, the domain pre-populates on the form.
If you have logged in before under a private domain hosted by google and a gmail account and previously needed to pick the correct account, this will automatically choose your account with the matching domain and send you through to the secured resource.
security.oauth2.client.client-id=123456789-abc123456789.apps.googleusercontent.com
security.oauth2.client.client-secret=yyyyyyyyyyyyyyyyy
security.oauth2.client.access-token-uri=https://www.googleapis.com/oauth2/v3/token
security.oauth2.client.user-authorization-uri=https://accounts.google.com/o/oauth2/auth?hd=MYDOMAIN.com
security.oauth2.client.authentication-scheme=query
security.oauth2.client.scope=email
security.oauth2.client.
security.oauth2.client.client-authentication-scheme=form
security.oauth2.resource.user-info-uri=https://www.googleapis.com/plus/v1/people/me
security.oauth2.resource.prefer-token-info=false

Categories

Resources