Swagger 3 Required Global Request Parameter - spring-boot

I am using Spring Boot 2.5.4 with Swagger 3. I have added one Global Request Parameter as type header and required=true in Swagger Config file .
Swagger UI is correctly showing the required request header in all APIs but the problem is that it's allowing requests to be sent when the value is empty for required request header. In Swagger 2 , UI used to disable sending request until the value was filled.
Any suggestions.
#Bean
public Docket api() {
RequestParameterBuilder aParameterBuilder = new RequestParameterBuilder();
aParameterBuilder.name("x-remote-user").description("Remote User").in(ParameterType.HEADER).required(true)
.build();
List<RequestParameter> aParameters = new ArrayList<>();
aParameters.add(aParameterBuilder.build());
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.securityContexts(Arrays.asList(securityContext())).securitySchemes(Arrays.asList(apiKey())).select()
.apis(RequestHandlerSelectors.basePackage("com.xxx.controller"))
.paths(PathSelectors.ant("/api/**")).build().globalRequestParameters(aParameters);
}

I found out the solution . Posting it here if someone else is looking for it .
If we disallow empty values , then swagger UI starts blocking us from Executing API if header value is kept empty.
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.securityContexts(Arrays.asList(securityContext())).securitySchemes(Arrays.asList(apiKey())).select()
.apis(RequestHandlerSelectors.basePackage("com.xxx.controller"))
.paths(PathSelectors.ant("/api/**")).build()
.globalRequestParameters(Arrays.asList(new RequestParameterBuilder().name("x-remote-user")
.description("Remote User").in(ParameterType.HEADER).required(true)
.query(simpleParameterSpecificationBuilder -> simpleParameterSpecificationBuilder
.allowEmptyValue(false).model(modelSpecificationBuilder -> modelSpecificationBuilder
.scalarModel(ScalarType.STRING)))
.build()));
}

it maybe help you
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.operationSelector(this::selector)
.build();
}
boolean selector(OperationContext operationContext) {
String url = operationContext.requestMappingPattern();
// filter url
return true;
}
private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "Authorization header");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Collections.singletonList(new SecurityReference("Authorization", authorizationScopes));
}
private List<SecurityScheme> securitySchemeList(){
ApiKey apiKey = new ApiKey("Authorization", "Authorization token", "header");
return Collections.singletonList(apiKey);
}
private List<RequestParameter> globalRequestParameters() {
RequestParameterBuilder parameterBuilder = new RequestParameterBuilder()
.in(ParameterType.HEADER)
.name("Authorization")
.required(true)
.query(param -> param.model(model -> model.scalarModel(ScalarType.STRING)));
return Collections.singletonList(parameterBuilder.build());
}
#Bean
public Docket authorization() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.securityContexts(Collections.singletonList(securityContext()))
.securitySchemes(securitySchemeList())
.globalRequestParameters(globalRequestParameters())
.select()
.paths(PathSelectors.regex("^(?!/error).*"))
.build()
;
}

Related

Customize Spring OAuth2TokenEndpointFilter

I need to customize the Authentication Success Handler method for the Spring Authorization Server OAuth2TokenEndpointFilter to simulate a OAuth2 provider with limited capabilities.
I've tried the following:
#Bean
public OAuth2TokenEndpointFilter oauth2TokenEndpointFilter(AuthenticationManager authenticationManager) {
OAuth2TokenEndpointFilter filter = new OAuth2TokenEndpointFilter(authenticationManager);
// this is ugly but seems to be the only way to customize the token format.
filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
additionalParameters.put("expires_in", null);
additionalParameters.put("user_id", null);
final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
new OAuth2AccessTokenResponseHttpMessageConverter();
OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.refreshToken(refreshToken.getTokenValue())
.additionalParameters(additionalParameters);
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
});
return filter;
}
Unfortunately, the token still comes back with the default fields in the token.
Can anyone tell me what I'm doing wrong?

Configuring AWS Signing in Reactive Elasticsearch Configuration

In one of our service I tried to configure AWS signing in Spring data Reactive Elasticsearch configuration.
Spring provides the configuring the webclient through webclientClientConfigurer
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.usingSsl()
.withWebClientConfigurer(
webClient -> {
return webClient.mutate().filter(new AwsSigningInterceptor()).build();
})
. // ... other options to configure if required
.build();
through which we can configure to sign the requests but however AWS signing it requires url, queryparams, headers and request body(in case of POST,POST) to generate the signed headers.
Using this I created a simple exchange filter function to sign the request but in this function I was not able to access the request body and use it.
Below is the Filter function i was trying to use
#Component
public class AwsSigningInterceptor implements ExchangeFilterFunction
{
private final AwsHeaderSigner awsHeaderSigner;
public AwsSigningInterceptor(AwsHeaderSigner awsHeaderSigner)
{
this.awsHeaderSigner = awsHeaderSigner;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next)
{
Map<String, List<String>> signingHeaders = awsHeaderSigner.createSigningHeaders(request, new byte[]{}, "es", "us-west-2"); // should pass request body bytes in place of new byte[]{}
ClientRequest.Builder requestBuilder = ClientRequest.from(request);
signingHeaders.forEach((key, value) -> requestBuilder.header(key, value.toArray(new String[0])));
return next.exchange(requestBuilder.build());
}
}
I also tried to access the request body inside ExchangeFilterFunction using below approach but once i get the request body using below approach.
ClientRequest.from(newRequest.build())
.body(
(outputMessage, context) -> {
ClientHttpRequestDecorator loggingOutputMessage =
new ClientHttpRequestDecorator(outputMessage) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
log.info("Inside write with method");
body =
DataBufferUtils.join(body)
.map(
content -> {
// Log request body using
// 'content.toString(StandardCharsets.UTF_8)'
String requestBody =
content.toString(StandardCharsets.UTF_8);
Map<String, Object> signedHeaders =
awsSigner.getSignedHeaders(
request.url().getPath(),
request.method().name(),
multimap,
requestHeadersMap,
Optional.of(
requestBody.getBytes(StandardCharsets.UTF_8)));
log.info("Signed Headers generated:{}", signedHeaders);
signedHeaders.forEach(
(key, value) -> {
newRequest.header(key, value.toString());
});
return content;
});
log.info("Before returning the body");
return super.writeWith(body);
}
#Override
public Mono<Void>
setComplete() { // This is for requests with no body (e.g. GET).
Map<String, Object> signedHeaders =
awsSigner.getSignedHeaders(
request.url().getPath(),
request.method().name(),
multimap,
requestHeadersMap,
Optional.of("".getBytes(StandardCharsets.UTF_8)));
log.info("Signed Headers generated:{}", signedHeaders);
signedHeaders.forEach(
(key, value) -> {
newRequest.header(key, value.toString());
});
return super.setComplete();
}
};
return originalBodyInserter.insert(loggingOutputMessage, context);
})
.build();
But with above approach I was not able to change the request headers as adding headers throws UnsupportedOperationException inside writewith method.
Has anyone used the spring data reactive elastic search and configured to sign with AWS signed headers?
Any help would be highly appreciated.

swagger doesn't recognize api description

I instatiate docket like this
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.config.internal"))
.paths(Predicates.or(PathSelectors.ant("/api**/**")))
.build();
}
I created a set of stub endpoints that imitate the real one for /login or /oauth.
#Api("Authentication")
#RequestMapping("/api")
public interface LoginEndpointApi {
#ApiOperation(value = "Github SSO endpoint", notes = "Endpoint for Github SSO authentication")
#ApiResponses({
#ApiResponse(code = 200, message = "HTML page of main application")
})
#GetMapping("/oauth/github")
default void oauthGithub() {
throw new UnsupportedOperationException();
}
#ApiOperation(value = "Get CSRF token", notes = "Returns current CSRF token")
#ApiResponses({
#ApiResponse(code = 200, message = "CSRF token response", response = String.class,
examples = #Example({#ExampleProperty(value = "015275eb-293d-4ce9-ba07-ff5e1c348092")}))
})
#GetMapping("/csrf-token")
default void csrfToken() {
throw new UnsupportedOperationException();
}
#ApiOperation(value = "Login endpoint", notes = "Login endpoint for authorization")
#ApiResponses({
#ApiResponse(code = 200, message = "Successful authentication")
})
#PostMapping("/login")
default void login(
#ApiParam(required = true, name = "login", value = "login body")
#RequestBody LoginRequest loginRequest) {
throw new UnsupportedOperationException();
}
}
But it doesn't recognize it. It is located in the same com.config.internal package as I described.
But the page swagger ui is empty and shows that No operations defined in spec!
What is the problem?
If you want to provide swagger documentation for your request mappings specified above you could simply describe it with .paths(Predicates.or(PathSelectors.ant("/api/**"))) path matchers. But if your path includes something more complicated like api + text without backslash separator then you should get known with
https://docs.spring.io/spring/docs/3.1.x/javadoc-api/org/springframework/util/AntPathMatcher.html

Spring boot + Swagger UI how to tell endpoint to require bearer token

I'm using Spring Boot to build a REST API. I've added Swagger-ui to handle documentation. I'm having a problem implementation the client authentication flow into swagger, the problem being I can get swagger-ui to authorise a supplied client-id(username) and client-secret(password) via basic auth, but swagger UI doesn't appear to be then applying to resulting access token to endpoint calls.
To confirm, my authorisation process;
- Use basic auth to send base64 encoded username/password & grant_type=client_credentials to /oauth/token. Spring returns an access_token
- On future API calls, use the supplied access_token as the bearer token
I think that the problem may be because I need to place something on each method in my controllers to tell swagger that the endpoint requires authentication and what type, but I can't find any clear documentation on how to do this, and I don't know if I need to apply any further changes to my swagger config.
Here's an example of a controller (with most methods removed to reduce size);
#Api(value="Currencies", description="Retrieve, create, update and delete currencies", tags = "Currencies")
#RestController
#RequestMapping("/currency")
public class CurrencyController {
private CurrencyService currencyService;
public CurrencyController(#Autowired CurrencyService currencyService) {
this.currencyService = currencyService;
}
/**
* Deletes the requested currency
* #param currencyId the Id of the currency to delete
* #return 200 OK if delete successful
*/
#ApiOperation(value = "Deletes a currency item", response = ResponseEntity.class)
#RequestMapping(value="/{currencyId}", method=RequestMethod.DELETE)
public ResponseEntity<?> deleteCurrency(#PathVariable("currencyId") Long currencyId) {
try {
currencyService.deleteCurrencyById(currencyId);
} catch (EntityNotFoundException e) {
return new ErrorResponse("Unable to delete, currency with Id " + currencyId + " not found!").response(HttpStatus.NOT_FOUND);
}
return new ResponseEntity(HttpStatus.OK);
}
/**
* Returns a single currency by it's Id
* #param currencyId the currency Id to return
* #return the found currency item or an error
*/
#ApiOperation(value = "Returns a currency item", response = CurrencyResponse.class)
#RequestMapping(value="/{currencyId}", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<RestResponse> getCurrency(#PathVariable("currencyId") Long currencyId) {
Currency currency = null;
try {
currency = currencyService.findById(currencyId);
} catch (EntityNotFoundException e) {
return new ErrorResponse("Currency with Id " + currencyId + " could not be found!").response(HttpStatus.NOT_FOUND);
}
return new CurrencyResponse(currency).response(HttpStatus.OK);
}
/**
* Returns a list of all currencies available in the system
* #return Rest response of all currencies
*/
#ApiOperation(value = "Returns a list of all currencies ordered by priority", response = CurrencyListResponse.class)
#RequestMapping(value="", method=RequestMethod.GET, produces="application/json")
public ResponseEntity<RestResponse> getCurrencies() {
return new CurrencyListResponse(currencyService.getAllCurrencies()).response(HttpStatus.OK);
}
}
Here is my current swagger config;
#Configuration
#EnableSwagger2
public class SwaggerConfig extends WebMvcConfigurationSupport {
#Bean
public SecurityConfiguration security() {
return SecurityConfigurationBuilder.builder()
.clientId("12345")
.clientSecret("12345")
.scopeSeparator(" ")
.useBasicAuthenticationWithAccessCodeGrant(true)
.build();
}
#Bean
public Docket productApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.xompare.moo.controllers"))
.build()
.securitySchemes(Arrays.asList(securityScheme()))
.securityContexts(Arrays.asList(securityContext()))
.apiInfo(metaData());
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(Arrays.asList(new SecurityReference("spring_oauth", scopes())))
.forPaths(PathSelectors.regex("/.*"))
.build();
}
private AuthorizationScope[] scopes() {
AuthorizationScope[] scopes = {
new AuthorizationScope("read", "for read operations"),
new AuthorizationScope("write", "for write operations") };
return scopes;
}
public SecurityScheme securityScheme() {
GrantType grantType = new ClientCredentialsGrant("http://localhost:8080/oauth/token");
SecurityScheme oauth = new OAuthBuilder().name("spring_oauth")
.grantTypes(Arrays.asList(grantType))
.scopes(Arrays.asList(scopes()))
.build();
return oauth;
}
#Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
Authentication via spring works perfectly at this point, my only problem is getting it working with Swagger UI.
I think that you need to add "Bearer " in front of your key, just like it is shown at this post:
Spring Boot & Swagger UI. Set JWT token
I managed to resolve this by reverting from swagger-ui version 2.8.0 to 2.7.0 after reading the contents of this link which suggested it was a problem with version 2.8.0
https://github.com/springfox/springfox/issues/1961

Protecting API visibility in Swagger and Oauth2 Token Storage in Browser Session/Local Storage

I have created Spring Boot Swagger API using the below configuration. I have created OAUth2 and integrated within my Swagger Configurations. Everything works fine, I am able to see the Authorize button at the top right. All Apis are protected using Oauth2, the user will be able to get the details of the API only after successful authentication.
Now the two issues which I am facing is
After Successful Authentication through OAuth2 (Authorize button), I am able to hit all the services and gets all the response, but when I reload the page the access token which I got after successful authentication is going off.Is there any way to store the access token within the browser session/local storage
Right now all users even without authentication will be able to see all the Apis available in my project when they hit the swagger url. Is there any way in which I can hide those Rest Apis and shows it only after successful
authentication. An Example is given below
Before Authentication
After Authentication
My Spring Boot Swagger Configuration is given below
#Configuration
#EnableSwagger2
public class SwaggerConfig {
#Bean
public Docket userApi() {
List < ResponseMessage > list = new java.util.ArrayList < > ();
list.add(new ResponseMessageBuilder().code(500).message("500 message")
.responseModel(new ModelRef("Result")).build());
list.add(new ResponseMessageBuilder().code(401).message("Unauthorized")
.responseModel(new ModelRef("Result")).build());
list.add(new ResponseMessageBuilder().code(406).message("Not Acceptable")
.responseModel(new ModelRef("Result")).build());
return new Docket(DocumentationType.SWAGGER_2)
.groupName("otrms-reports-api")
.apiInfo(apiInfo())
.select().apis(RequestHandlerSelectors.basePackage("com.otrms.reports"))
.paths(PathSelectors.any())
.build()
.securitySchemes(newArrayList(oauth()))
.securityContexts(newArrayList(securityContext()))
.globalResponseMessage(RequestMethod.GET, list)
.globalResponseMessage(RequestMethod.POST, list);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("OTRMS")
.description("OTRMS API for Reports")
.termsOfServiceUrl("http://otrms.com")
.contact(contact())
.license("Apache License Version 2.0")
.licenseUrl("http://otrms.com/service/LICENSE")
.version("1.0")
.build();
}
private Contact contact() {
return new Contact("OTRMS", "http://otrms.com", "admin#otrms.com");
}
#Bean
SecurityContext securityContext() {
AuthorizationScope readScope = new AuthorizationScope("read:report", "read your report");
AuthorizationScope[] scopes = new AuthorizationScope[1];
scopes[0] = readScope;
SecurityReference securityReference = SecurityReference.builder()
.reference("report_auth")
.scopes(scopes)
.build();
return SecurityContext.builder()
.securityReferences(newArrayList(securityReference))
.forPaths(ant("/api/pet.*"))
.build();
}
#Bean
SecurityScheme oauth() {
return new OAuthBuilder()
.name("report_auth")
.grantTypes(grantTypes())
.scopes(scopes())
.build();
}
#Bean
SecurityScheme apiKey() {
return new ApiKey("header");
}
List < AuthorizationScope > scopes() {
List < AuthorizationScope > scopes = Lists. < AuthorizationScope > newArrayList();
scopes.add(new AuthorizationScope("resource-access", "Get Resource Access"));
return scopes;
}
List < GrantType > grantTypes() {
GrantType grantType = new ImplicitGrantBuilder()
.loginEndpoint(new LoginEndpoint("http://otrms.com/auth/oauth/authorize"))
.build();
return newArrayList(grantType);
}
#Bean
public SecurityConfiguration securityInfo() {
return new SecurityConfiguration("swaggerClient", "", "reports", "reportstore", "123", ApiKeyVehicle.HEADER, "", " ");
}
}

Resources