I want to switch to the Micronaut framework from Spring Boot 2. And I am struggling with the Swagger settings.
In Spring Boot 2 project I have the following dependencies:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
and SwaggerConfig.class:
#Configuration
#EnableSwagger2
public class SwaggerConfig {
#Bean
public Docket swagger() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(getApiInfo())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build();
}
private ApiInfo getApiInfo() {
return new ApiInfo("test",
"",
"",
"",
new Contact("", "https://test.test", ""),
"",
"");
}
}
And it works perfectly starting up swagger-ui along with the Spring Boot 2 application.
Which dependencies should I add to maven and which classes should I create to obtain the same result for the Micronaut project?
Assuming the application is already created, add the following to you pom.xml
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger.version}</version>
<scope>compile</scope>
</dependency>
where the property swagger.version is set to 2.0.3
add the following to you annotationProcessorPaths in the maven-compiler-plugin
<path>
<groupId>io.micronaut.configuration</groupId>
<artifactId>micronaut-openapi</artifactId>
<version>${micronaut.version}</version>
</path>
Then add the following to your micronaut router section.
micronaut:
router:
static-resources:
swagger:
paths: classpath:META-INF/swagger
mapping: ${application.api.swagger.path}/**
This will expose your swagger/oas yml file that is generated during compile, provided you use the configuration below. You can of course change the ${application.api.swagger.path} to just be /api-docs/swagger or something to your liking.
As described in the docs, you can also do the following --features=swagger-java to add the above dependecies when you initially create the project.
If you want to render the api-specification from the application itself, then you need to add some more code. The following example is probably more fleshed out than it needs to be, but for my purpose the application serves as a central renderer for swagger/oas specifications.
First add a controller for you swagger needs, and annotate the controller with #Hidden to make sure it doesn't get processed by the annotation processor.
#Hidden
#Controller("/api")
public class SwaggerController {
#Inject
SwaggerConfig config;
#View("swagger/index")
#Get
public SwaggerConfig index() {
return config;
}
}
Then add the following configuration class, which binds the properties from below
#ConfigurationProperties(SwaggerConfig.PREFIX)
public class SwaggerConfig {
public static final String PREFIX = "application.api.swagger";
private String version;
private String layout;
private boolean deepLinking;
private List<URIConfig> urls;
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getLayout() {
return layout;
}
public void setLayout(String layout) {
this.layout = layout;
}
public boolean isDeepLinking() {
return deepLinking;
}
public void setDeepLinking(boolean deepLinking) {
this.deepLinking = deepLinking;
}
public List<URIConfig> getUrls() {
return urls;
}
public void setUrls(List<URIConfig> urls) {
this.urls = urls;
}
#ConfigurationProperties(URIConfig.PREFIX)
public static class URIConfig {
static final String PREFIX = "urls";
private String name;
private String url;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
}
The above config class binds the following configuration from application.yml, but needs to be placed before the micronaut specific configuration.
application:
api:
swagger:
path: /api-docs/swagger
version: 3.19.4
layout: StandaloneLayout
deepLinking: true
urls:
- name: ubw-rest
url: /api-docs/swagger/ubw-rest-0.1.yml
When that is done, add the following handlebars/mustache dependency to the pom
<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>
<version>4.1.0</version>
<scope>runtime</scope>
</dependency>
Under the resources folder, create a folder named swagger, and then create an index.hbs file containing the following.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger-ui</title>
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist#{{version}}/favicon-32x32.png">
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist#{{version}}/swagger-ui.css">
<script src="https://unpkg.com/swagger-ui-dist#{{version}}/swagger-ui-standalone-preset.js"></script>
<script src="https://unpkg.com/swagger-ui-dist#{{version}}/swagger-ui-bundle.js"></script>
</head>
<body>
<div id="swagger-ui"></div>
<script>
window.onload = function() {
var ui = SwaggerUIBundle({
urls: [{{#each urls}}
{
name: "{{name}}",
url: "{{url}}"
}{{#unless #last}},{{/unless}}{{/each}}
],
dom_id: '#swagger-ui',
deepLinking: {{deepLinking}},
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "{{layout}}"
});
window.ui = ui
}
</script>
</body>
</html>
Finally, in the application main class, add the #OpenApiDefinition annotation to enable the annotation processor to scan the entire appliaction.
#OpenAPIDefinition(
info = #Info(
title = "swagger-server",
version = "0.1",
description = "My API",
license = #License(name = "Apache 2.0")
)
)
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class);
}
}
A word of advice regarding the annotation processor as it stands in micronaut 1.0.0 is that public fields on an object will not be exposed, so you need to have getters/setters if you want to see the schema for the input or return values.
If you'd like to try out a running example of the above, I have a repo with the swagger server configuration located here https://github.com/frehov/micronaut-swagger-server which includes the ability to make a post with a list of url and name pairs to be rendered by Swagger.
Micronaut able to automatically generate Swagger YAML definitions from our controller and methods based on these annotations. Add swagger-annotations dependency pom.xml.
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.0.5</version>
</dependency>
Add #OpenAPIDefinition annotation to application's main class.
#OpenAPIDefinition(
info = #Info(
title = "my app",
version = "1.0",
description = "my api",
contact = #Contact(url = "http://something.com", name = "something", email = "something")
)
)
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class);
}
}
Micronaut generates a Swagger file at target/classes/META-INF/swagger/my-app-1.0.yml. We can expose it outside the application using an HTTP endpoint. Here's the appropriate configuration provided inside the application.yml file.
micronaut:
router:
static-resources:
swagger:
paths: classpath:META-INF/swagger
mapping: /swagger/**
Accesspath http://localhost:8080/swagger/my-app-1.0.yml.
In comparison to Spring Boot, we don't have projects like Swagger
SpringFox for Micronaut, so we need to copy the content to an online
editor in order to see the graphical representation of our Swagger
YAML.
Paste your content of my-app-1.0.yml to https://editor.swagger.io/ and add server to access Swagger UI.
Read Micronaut Doc- https://micronaut-projects.github.io/micronaut-openapi/latest/guide/index.html
Related
I have a AWS Cognito user pool issuing tokens to my frontend application. The frontend application then uses the tokens to talk to my backend service.
This flow is working as intended. I am validating the tokens that hit my backend service using org.springframework.security:spring-security-oauth2-resource-server:6.0.1 which is configured to point back to Cognito
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://cognito-idp.us-east-1.amazonaws.com/my_pool_endpoint
I have a simple SecurityConfig
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity(useAuthorizationManager = true)
public class SecurityConfig {
#Bean
SecurityWebFilterChain securityWebFilterChain(final ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/v3/api-docs/**")
.permitAll()
.anyExchange()
.authenticated()
.and()
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt)
.build();
}
So far everything is looking good.
But how do I gather additional information from incoming tokens, for example things such as email and username are not included in the token response from Cognito. An example of a decoded token looks like:
{
"sub": "00000000000000000000",
"cognito:groups": [
"00000000000000000000"
],
"iss": "https://cognito-idp.us-east-1.amazonaws.com/00000000000000000000",
"version": 2,
"client_id": "00000000000000000000",
"origin_jti": "00000000000000000000",
"token_use": "access",
"scope": "openid profile email",
"auth_time": 1676066347,
"exp": 1676186814,
"iat": 1676143614,
"jti": "00000000000000000000",
"username": "google_00000000000000000000"
}
When I need extra information from the token, I'm calling https://my-congito-pool.auth.us-east-1.amazoncognito.com/oauth2/userInfo and passing the JWT as the Bearer token, which works and returns the information I'm looking for such as email, picture, username etc.
My question is I don't think doing this manually every time I want additional information is the 'correct' way of handling it.
Should I be using something like a UserDetailsService to perform this once and transforming the incoming JWT into my own User which holds this information?
If so, how do I do this using ReactiveSpringSecurity?
It looks like Cognito allows to enrich ID tokens, but not access tokens. That's sad, most competitors allow it and it makes spring resource-servers configurations much easier.
I can think of two solutions:
configure your resource-server with access-token introspection (with http.oauth2ResourceServer().opaqueToken()), using your /oauth2/userInfo as introspection endpoint and the JWT access-token as "opaque" token
require clients to add the ID token in a dedicated header (let's say X-ID-Token) in addition to the access token (provided in the Authorization header as usual). Then in the authentication converter, retrieve and decode this additional header and build an Authentication of your own with both access and ID tokens strings and claims
I will only develop the second solution for two reasons:
the first has the usual performance cost of token introspection (a call is made from the resource-server to the authorization-server before each request is processed)
the second permits to add any data from as many headers as we need to the Authentication instance for authentication and authorization (not only ID token as we demo here) with very little performance impact
Spoiler: here is what I got:
with valid access and ID tokens
with just the access-token
Isn't it exactly what you are looking for: an Authentication instance with the roles from the access token and the email from the ID token (or Unauthorized if authorization data is missing / invalid / incomplete)?
Detailed Security Configuration
Here is the security configuration for a reactive app. For Servlets, main lines are the same, only the tooling to statically access the request context is quite different. You can refer to this tutorial I just added to my collection for details.
#Configuration
#EnableReactiveMethodSecurity
#EnableWebFluxSecurity
public class SecurityConfig {
static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
public static Mono<ServerHttpRequest> getServerHttpRequest() {
return Mono.deferContextual(Mono::just)
.map(contextView -> contextView.get(ServerWebExchange.class).getRequest());
}
public static Mono<String> getIdTokenHeader() {
return getServerHttpRequest().map(req -> {
final var headers = req.getHeaders().getOrEmpty(ID_TOKEN_HEADER_NAME).stream()
.filter(StringUtils::hasLength).toList();
if (headers.size() == 0) {
throw new MissingIdTokenException();
}
if (headers.size() > 1) {
throw new MultiValuedIdTokenException();
}
return headers.get(0);
});
}
#Bean
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) {
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(accessToken -> getIdTokenHeader()
.flatMap(idTokenString -> jwtDecoder.decode(idTokenString).doOnError(JwtException.class, e -> {
throw new InvalidIdTokenException();
}).map(idToken -> {
final var idClaims = idToken.getClaims();
#SuppressWarnings("unchecked")
final var authorities = ((List<String>) accessToken.getClaims().getOrDefault("cognito:groups",
List.of())).stream().map(SimpleGrantedAuthority::new).toList();
return new MyAuth(authorities, accessToken.getTokenValue(), idTokenString, accessToken.getClaims(),
idClaims);
})));
http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance()).csrf().disable();
http.authorizeExchange().anyExchange().authenticated();
return http.build();
}
public static class MyAuth extends AbstractAuthenticationToken {
private static final long serialVersionUID = 9115947200114995708L;
// Save access and ID tokens strings just in case we need to call another
// micro-service on behalf of the user who initiated the request and as so,
// position "Authorization" and "X-ID-Token" headers
private final String accessTokenString;
private final String idTokenString;
private final Map<String, Object> accessClaims;
private final Map<String, Object> idClaims;
public MyAuth(Collection<? extends GrantedAuthority> authorities, String accessTokenString,
String idTokenString, Map<String, Object> accessClaims, Map<String, Object> idClaims) {
super(authorities);
this.accessTokenString = accessTokenString;
this.accessClaims = Collections.unmodifiableMap(accessClaims);
this.idTokenString = idTokenString;
this.idClaims = Collections.unmodifiableMap(idClaims);
// Minimal security checks: assert that issuer and subject claims are the same
// in access and ID tokens.
if (!Objects.equals(accessClaims.get(IdTokenClaimNames.ISS), idClaims.get(IdTokenClaimNames.ISS))
|| !Objects.equals(accessClaims.get(StandardClaimNames.SUB), idClaims.get(IdTokenClaimNames.SUB))) {
throw new InvalidIdTokenException();
}
// You could also make assertions on ID token audience, but this will require
// adding a custom property for expected ID tokens audience.
// You can't just check for audience equality with already validated access
// token one.
this.setAuthenticated(true);
}
#Override
public String getCredentials() {
return accessTokenString;
}
#Override
public String getPrincipal() {
return (String) accessClaims.get(StandardClaimNames.SUB);
}
public String getAccessTokenString() {
return accessTokenString;
}
public String getIdTokenString() {
return idTokenString;
}
public Map<String, Object> getAccessClaims() {
return accessClaims;
}
public Map<String, Object> getIdClaims() {
return idClaims;
}
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is missing")
static class MissingIdTokenException extends RuntimeException {
private static final long serialVersionUID = -4894061353773464761L;
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is not unique")
static class MultiValuedIdTokenException extends RuntimeException {
private static final long serialVersionUID = 1654993007508549674L;
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is not valid")
static class InvalidIdTokenException extends RuntimeException {
private static final long serialVersionUID = -6233252290377524340L;
}
}
Now, each time an authorization succeeds (isAuthenticated() is true), you'll have a MyAuth instance in the security context and it contains both the access and ID tokens claims!
Sample Controller
#RestController
public class GreetingController {
#GetMapping("/greet")
#PreAuthorize("isAuthenticated()")
Mono<String> greet(MyAuth auth) {
return Mono.just("Hello %s! You are granted with %s".formatted(
auth.getIdClaims().get("email"),
auth.getAuthorities()));
}
}
You may also build your #PreAuthorize expressions based on it. Something like:
#RequiredArgsConstructor
#RestController
#RequestMapping("/something/protected")
#PreAuthorize("isAuthenticated()")
public class ProtectedResourceController {
private final SomeResourceRepository resourceRepo;
#GetMapping("/{resourceId}")
#PreAuthorize("#auth.idClaims['email'] == #resource.email")
ResourceDto getProtectedResource(MyAuth auth, #RequestParam("resourceId") SomeResource resource) {
...
}
}
EDIT: Code reusability & spring-addons starters
I maintain wrappers around spring-boot-starter-oauth2-resource-server. It is very thin and opensource. If you don't want to use it, you should have a look at how it is done to get inspiration from it:
inspect resources to find out what it takes to build your own spring-boot starters
inspect beans to pick ideas for creating your own configurable ones
browse to dependencies like OpenidClaimSet and OAuthentication which could be of inspiration
Here is what the sample above becomes with "my" starter for reactive resource-servers with JWT decoders:
#Configuration
#EnableReactiveMethodSecurity
#EnableWebFluxSecurity
public class SecurityConfig {
static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
#Bean
OAuth2AuthenticationFactory authenticationFactory(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter,
ReactiveJwtDecoder jwtDecoder) {
return (accessBearerString, accessClaims) -> ServerHttpRequestSupport.getUniqueHeader(ID_TOKEN_HEADER_NAME)
.flatMap(idTokenString -> jwtDecoder.decode(idTokenString).doOnError(JwtException.class, e -> {
throw new InvalidHeaderException(ID_TOKEN_HEADER_NAME);
}).map(idToken -> new MyAuth(
authoritiesConverter.convert(accessClaims),
accessBearerString,
new OpenidClaimSet(accessClaims),
idTokenString,
new OpenidClaimSet(idToken.getClaims()))));
}
#Data
#EqualsAndHashCode(callSuper = true)
public static class MyAuth extends OAuthentication<OpenidClaimSet> {
private static final long serialVersionUID = 1734079415899000362L;
private final String idTokenString;
private final OpenidClaimSet idClaims;
public MyAuth(Collection<? extends GrantedAuthority> authorities, String accessTokenString,
OpenidClaimSet accessClaims, String idTokenString, OpenidClaimSet idClaims) {
super(accessClaims, authorities, accessTokenString);
this.idTokenString = idTokenString;
this.idClaims = idClaims;
}
}
}
Update #Controller (pay attention to the direct accessor to email claim):
#RestController
public class GreetingController {
#GetMapping("/greet")
#PreAuthorize("isAuthenticated()")
Mono<String> greet(MyAuth auth) {
return Mono.just("Hello %s! You are granted with %s".formatted(
auth.getIdClaims().getEmail(),
auth.getAuthorities()));
}
}
This are the configuration properties (with different claims used as authorities source depending on the authorization-server configured in the profile):
server:
error.include-message: always
spring:
lifecycle.timeout-per-shutdown-phase: 30s
security.oauth2.resourceserver.jwt.issuer-uri: https://localhost:8443/realms/master
com:
c4-soft:
springaddons:
security:
issuers:
- location: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}
authorities:
claims:
- realm_access.roles
- resource_access.spring-addons-public.roles
- resource_access.spring-addons-confidential.roles
caze: upper
prefix: ROLE_
cors:
- path: /greet
---
spring.config.activate.on-profile: cognito
spring.security.oauth2.resourceserver.jwt.issuer-uri: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl
com.c4-soft.springaddons.security.issuers:
- location: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}
authorities:
claims:
- cognito:groups
caze: upper
prefix: ROLE_
---
spring.config.activate.on-profile: auth0
com.c4-soft.springaddons.security.issuers:
- location: https://dev-ch4mpy.eu.auth0.com/
authorities:
claims:
- roles
- permissions
caze: upper
prefix: ROLE_
Unit-tests with mocked identity for the #Controller above can be as simple as:
#WebFluxTest(controllers = GreetingController.class)
#AutoConfigureAddonsWebSecurity
#Import(SecurityConfig.class)
class GreetingControllerTest {
#Autowired
WebTestClientSupport api;
#Test
#WithMyAuth(authorities = { "AUTHOR" }, idClaims = #OpenIdClaims(email = "ch4mp#c4-soft.com"))
void givenUserIsAuthenticated_whenGreet_thenOk() throws Exception {
api.get("/greet").expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello ch4mp#c4-soft.com! You are granted with [AUTHOR]");
}
#Test
void givenRequestIsAnonymous_whenGreet_thenUnauthorized() throws Exception {
api.get("/greet").expectStatus().isUnauthorized();
}
}
With annotation definition (to build the custom Authentication implementation and set it in the security context):
#Target({ ElementType.METHOD, ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#Documented
#WithSecurityContext(factory = WithMyAuth.MyAuthFactory.class)
public #interface WithMyAuth {
#AliasFor("authorities")
String[] value() default {};
#AliasFor("value")
String[] authorities() default {};
OpenIdClaims accessClaims() default #OpenIdClaims();
OpenIdClaims idClaims() default #OpenIdClaims();
String accessTokenString() default "machin.truc.chose";
String idTokenString() default "machin.bidule.chose";
#AliasFor(annotation = WithSecurityContext.class)
TestExecutionEvent setupBefore()
default TestExecutionEvent.TEST_METHOD;
#Target({ ElementType.METHOD, ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
public static #interface Proxy {
String onBehalfOf();
String[] can() default {};
}
public static final class MyAuthFactory extends AbstractAnnotatedAuthenticationBuilder<WithMyAuth, MyAuth> {
#Override
public MyAuth authentication(WithMyAuth annotation) {
final var accessClaims = new OpenidClaimSet(super.claims(annotation.accessClaims()));
final var idClaims = new OpenidClaimSet(super.claims(annotation.idClaims()));
return new MyAuth(super.authorities(annotation.authorities()), annotation.accessTokenString(), accessClaims, annotation.idTokenString(), idClaims);
}
}
}
And this is the pom body:
<properties>
<java.version>17</java.version>
<spring-addons.version>6.0.13</spring-addons.version>
</properties>
<dependencies>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webflux-jwt-resource-server</artifactId>
<version>${spring-addons.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webflux-jwt-test</artifactId>
<version>${spring-addons.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
I'm trying to test a quarkus rest-endpoint which is secured with #RolesAllowed
...
#GET
#Path("{id}")
#Produces(MediaType.APPLICATION_OCTET_STREAM)
#RolesAllowed({ "APPLICATION_USER"})
public Response getFile(#PathParam(value = "id") String documentId, #Context UriInfo uriInfo)
...
The test case
#QuarkusTest
class DocumentResourceTest {
#Test
public void testDocumentEndpoint() {
String documentId = "someId";
given()
.when().get("/documents/" + documentId)
.then()
.statusCode(200);
}
}
How can i mock an authenticated user with role 'APPLICATION_USER' for my test case ?
You can inject a SecurityIdentity which you can then stub out with the relevant role using Mockito:
#QuarkusTest
public class DocumentResourceTest {
#InjectMock
SecurityIdentity identity;
#BeforeEach
public void setup() {
Mockito.when(identity.hasRole("APPLICATION_USER")).thenReturn(true);
}
#Test
public void testDocumentEndpoint() {
String documentId = "someId";
given()
.when().get("/documents/" + documentId)
.then()
.statusCode(200);
}
}
You can of course move the stubbing call to your individual tests if you want to test a variety of different roles.
Note that you'll need to add the quarkus-junit5-mockito dependency for this to work:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
A more convinient way to mock the security is to use Quarkus' security testing features:
https://quarkus.io/guides/security-testing#testing-security
Including
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security</artifactId>
<scope>test</scope>
</dependency>
allows you to write
#Test
#TestSecurity(authorizationEnabled = false)
void someTestMethod() {
...
}
#Test
#TestSecurity(user = "testUser", roles = {"admin", "user"})
void otherTestMethod() {
...
}
In addition to the accepted answer, there is also this guide which explains how to deal with integration tests: https://quarkus.io/guides/security-oauth2#integration-testing
The first sentence there is:
If you don’t want to use a real OAuth2 authorization server for your integration tests, you can use the Properties based security extension for your test, or mock an authorization server using Wiremock.
So I think the property based security extension could also work for you: https://quarkus.io/guides/security-properties
I wanna change my swagger-ui path from localhost:8080/swagger-ui.html to
localhost:8080/myapi/swagger-ui.html in springboot
redirect is helpless to me
In the application.properties of Spring Boot
springdoc.swagger-ui.path=/swagger-ui-custom.html
in your case it will be
springdoc.swagger-ui.path=/myapi/swagger-ui.html
if for some reason you don't want redirect to /swagger-ui.html you can set itself contents as home view, setting an index.html at resources/static/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to another awesome Microservice</title>
</head>
<body>
<script>
document.body.innerHTML = '<object type="text/html" data="/swagger-ui.html" style="overflow:hidden;overflow-x:hidden;overflow-y:hidden;height:100%;width:100%;position:absolute;top:0px;left:0px;right:0px;bottom:0px"></object>';
</script>
</body>
</html>
then accessing to your http://localhost:8080/ you will see your swagger docs.
finally you can customize path and .html file using:
registry.addViewController("/swagger").setViewName("forward:/index.html");
like suggests this answer
You can modify springfox properties in application.properties
For example, to edit the base-url
springfox.documentation.swagger-ui.base-url=documentation
For e.g. setting it to /documentation will put swagger-ui at /documentation/swagger-ui/index.html
If you want to add, for example, documentation prefix - You can do like this for path http://localhost:8080/documentation/swagger-ui.html:
kotlin
#Configuration
#EnableSwagger2
#ConfigurationPropertiesScan("your.package.config")
#Import(value = [BeanValidatorPluginsConfiguration::class])
class SwaggerConfiguration(
private val swaggerContactProp: SwaggerContactProp, private val swaggerProp: SwaggerProp
) : WebMvcConfigurationSupport() {
// https://springfox.github.io/springfox/docs/current/
#Bean
fun api(): Docket = Docket(DocumentationType.SWAGGER_2)
.groupName("Cards")
.apiInfo(getApiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("your.controllers.folder"))
.paths(PathSelectors.any())
.build()
private fun getApiInfo(): ApiInfo {
val contact = Contact(swaggerContactProp.name, swaggerContactProp.url, swaggerContactProp.mail)
return ApiInfoBuilder()
.title(swaggerProp.title)
.description(swaggerProp.description)
.version(swaggerProp.version)
.contact(contact)
.build()
}
override fun addViewControllers(registry: ViewControllerRegistry) {
with(registry) {
addRedirectViewController("/documentation/v2/api-docs", "/v2/api-docs").setKeepQueryParams(true)
addRedirectViewController(
"/documentation/swagger-resources/configuration/ui", "/swagger-resources/configuration/ui"
)
addRedirectViewController(
"/documentation/swagger-resources/configuration/security", "/swagger-resources/configuration/security"
)
addRedirectViewController("/documentation/swagger-resources", "/swagger-resources")
}
}
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/documentation/**").addResourceLocations("classpath:/META-INF/resources/")
}
}
#ConfigurationProperties(prefix = "swagger")
#ConstructorBinding
data class SwaggerProp(val title: String, val description: String, val version: String)
#ConfigurationProperties(prefix = "swagger.contact")
#ConstructorBinding
data class SwaggerContactProp(val mail: String, val url: String, val name: String)
and in applicatiom.yml:
swagger:
title: Cards
version: 1.0
description: Documentation for API
contact:
mail: email#gmail.com
url: some-url.com
name: COLABA card
Also don't forget to add in build.gradle.kts:
implementation("io.springfox:springfox-swagger2:$swagger")
implementation("io.springfox:springfox-swagger-ui:$swagger")
implementation("io.springfox:springfox-bean-validators:$swagger")
I've found several possible solutions for myself. Maybe it will be helpful for somebody else.
Set springdoc.swagger-ui.path directly
The straightforward way is to set property springdoc.swagger-ui.path=/custom/path. It will work perfectly if you can hardcode swagger path in your application.
Override springdoc.swagger-ui.path property
You can change default swagger-ui path programmatically using ApplicationListener<ApplicationPreparedEvent>. The idea is simple - override springdoc.swagger-ui.path=/custom/path before your Spring Boot application starts.
#Component
public class SwaggerConfiguration implements ApplicationListener<ApplicationPreparedEvent> {
#Override
public void onApplicationEvent(final ApplicationPreparedEvent event) {
ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
Properties props = new Properties();
props.put("springdoc.swagger-ui.path", swaggerPath());
environment.getPropertySources()
.addFirst(new PropertiesPropertySource("programmatically", props));
}
private String swaggerPath() {
return "/swagger/path"; //todo: implement your logic here.
}
}
In this case, you must register the listener before your application start:
#SpringBootApplication
#OpenAPIDefinition(info = #Info(title = "APIs", version = "0.0.1", description = "APIs v0.0.1"))
public class App {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(App.class);
application.addListeners(new SwaggerConfiguration());
application.run(args);
}
}
Redirect using controller
You can also register your own controller and make a simple redirect as suggested there.
Redirect code for Spring WebFlux applications:
#RestController
public class SwaggerEndpoint {
#GetMapping("/custom/path")
public Mono<Void> api(ServerHttpResponse response) {
response.setStatusCode(HttpStatus.PERMANENT_REDIRECT);
response.getHeaders().setLocation(URI.create("/swagger-ui.html"));
return response.setComplete();
}
}
The problem with such an approach - your server will still respond if you call it by address "/swagger-ui.html".
You can use this code, it worked for me
package com.swagger.api.redirect;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class SwaggerApiReDirector implements WebMvcConfigurer {
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController("/documentation/v2/api-docs", "/v2/api-docs");
registry.addRedirectViewController("/documentation/configuration/ui", "/configuration/ui");
registry.addRedirectViewController("/documentation/configuration/security", "/configuration/security");
registry.addRedirectViewController("/documentation/swagger-resources", "/swagger-resources");
registry.addRedirectViewController("/documentation/swagger-resources/configuration/ui", "/swagger-resources/configuration/ui");
registry.addRedirectViewController("/documentation", "/documentation/swagger-ui.html");
registry.addRedirectViewController("/documentation/", "/documentation/swagger-ui.html");
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("/documentation/**").addResourceLocations("classpath:/META-INF/resources/");
}
}
If you are using spring boot then please update
application.properties file and write here
server.servlet.context-path=/myapi
it will redirect you as you want.
I have a restful service calling an external service using Spring Cloud Feign client
#FeignClient(name = "external-service", configuration = FeignClientConfig.class)
public interface ServiceClient {
#RequestMapping(value = "/test/payments", method = RequestMethod.POST)
public void addPayment(#Valid #RequestBody AddPaymentRequest addPaymentRequest);
#RequestMapping(value = "/test/payments/{paymentId}", method = RequestMethod.PUT)
public ChangePaymentStatusResponse updatePaymentStatus(#PathVariable("paymentId") String paymentId,
#Valid #RequestBody PaymentStatusUpdateRequest paymentStatusUpdateRequest);
}
I noticed the following failure 3-4 times in the last 3 months in my log file:
json.ERROR_RESPONSE_BODY:Connection refused executing POST
http://external-service/external/payments json.message:Send Payment
Add Payment Failure For other reason: {ERROR_RESPONSE_BODY=Connection
refused executing POST http://external-service/external/payments,
EVENT=ADD_PAYMENT_FAILURE, TRANSACTION_ID=XXXXXXX} {}
json.EVENT:ADD_PAYMENT_FAILURE
json.stack_trace:feign.RetryableException: Connection refused
executing POST http://external-service/external/payments at
feign.FeignException.errorExecuting(FeignException.java:67) at
feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:104)
at
feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:76)
at
feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103)
Is it possible to add Spring Retry on a Feign client.
What I wanted to annotate the addPayment operation with
#Retryable(value = {feign.RetryableException.class }, maxAttempts = 3, backoff = #Backoff(delay = 2000, multiplier=2))
But this is not possible, what other options do I have?
You can add a Retryer in the FeignClientConfig
#Configuration
public class FeignClientConfig {
#Bean
public Retryer retryer() {
return new Custom();
}
}
class Custom implements Retryer {
private final int maxAttempts;
private final long backoff;
int attempt;
public Custom() {
this(2000, 3);
}
public Custom(long backoff, int maxAttempts) {
this.backoff = backoff;
this.maxAttempts = maxAttempts;
this.attempt = 1;
}
public void continueOrPropagate(RetryableException e) {
if (attempt++ >= maxAttempts) {
throw e;
}
try {
Thread.sleep(backoff);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
#Override
public Retryer clone() {
return new Custom(backoff, maxAttempts);
}
}
Updated with sample Retryer example config based on the Retryer.Default.
If you are using ribbon you can set properties, you can use below properties for retry:
myapp.ribbon.MaxAutoRetries=5
myapp.ribbon.MaxAutoRetriesNextServer=5
myapp.ribbon.OkToRetryOnAllOperations=true
Note: "myapp" is your service id.
Checkout this Github implementation for working example
Just new a contructor Default
#Configuration
public class FeignClientConfig {
#Bean
public Retryer retryer() {
return new Retryer.Default(100, 2000, 3);
}
}
Adding this if it can help someone. I was getting connection reset using feign, as some unknown process was running on that port.
Try changing the port. Refer this to find the process running on a port
I prepared a blog post about using Spring Retry with Feign Client methods. You may consider checking the Post. All steps have been explained in the post.
This is my config. Test OK in spring boot 2.2.0.RELEASE
spring cloud Hoxton.M3.
feign.hystrix.enabled=true
MY-SPRING-API.ribbon.MaxAutoRetries=2
MY-SPRING-API.ribbon.MaxAutoRetriesNextServer=2
MY-SPRING-API.ribbon.OkToRetryOnAllOperations=true
MY-SPRING-API.ribbon.retryableStatusCodes=404,500
feign.client.config.PythonPatentClient.connectTimeout=500
feign.client.config.PythonPatentClient.readTimeout=500
hystrix.command.PythonPatentClient#timeTest(String).execution.isolation.thread.timeoutInMilliseconds=5000
java code is :
#FeignClient(name = "MY-SPRING-API",configuration = {PythonPatentConfig.class},fallbackFactory = FallBack.class)
public interface PythonPatentClient
#RequestLine("GET /test?q={q}")
void timeTest(#Param("appNo") String q);
Controller is :
#RequestMapping(value = "/test",method = {RequestMethod.POST,RequestMethod.GET})
public Object test() throws InterruptedException {
log.info("========important print enter test========");
TimeUnit.SECONDS.sleep(10L);
pom.xml additon add:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
optional:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
#EnableRetry
#SpringBootApplication
public class ApiApplication
this is document :
https://docs.spring.io/spring-cloud-netflix/docs/2.2.10.RELEASE/reference/html/#retrying-failed-requests
https://github.com/spring-projects/spring-retry
https://github.com/spring-cloud/spring-cloud-netflix/
I resolved that by creating a wrapper on top of ServiceClient
#Configuration
public class ServiceClient {
#Autowired
ServiceFeignClient serviceFeignClient;
#Retryable(value = { ClientReprocessException.class }, maxAttemptsExpression = "#{${retryMaxAttempts}}", backoff = #Backoff(delayExpression = "#{${retryDelayTime}}"))
public void addPayment( AddPaymentRequest addPaymentRequest){
return serviceFeignClient.addPayment(addPaymentRequest);
}
}
I want to be able to use a test properties files and only override a few properties. Having to override every single property will get ugly fast.
This is the code I am using to test my ability to mock properties and use existing properties in a test case
#RunWith(SpringRunner.class)
#SpringBootTest(classes = MyApp.class)
#TestPropertySource(
locations = { "classpath:myapp-test.properties" },
properties = { "test.key = testValue" })
public class EnvironmentMockedPropertiesTest {
#Autowired private Environment env;
// #MockBean private Environment env;
#Test public void testExistingProperty() {
// some.property=someValue
final String keyActual = "some.property";
final String expected = "someValue";
final String actual = env.getProperty(keyActual);
assertEquals(expected, actual);
}
#Test public void testMockedProperty() {
final String keyMocked = "mocked.test.key";
final String expected = "mockedTestValue";
when(env.getProperty(keyMocked)).thenReturn(expected);
final String actual = env.getProperty(keyMocked);
assertEquals(expected, actual);
}
#Test public void testOverriddenProperty() {
final String expected = "testValue";
final String actual = env.getProperty("test.key");
assertEquals(expected, actual);
}
}
What I find is:
#Autowired private Environment env;
testExistingProperty() and testOverriddenProperty() pass
testMockedProperty() fails
#MockBean private Environment env;
testMockedProperty() passes
testExistingProperty() and testOverriddenProperty() fail
Is there a way to achieve what I am aiming for?
Dependencies:
<spring.boot.version>1.4.3.RELEASE</spring.boot.version>
...
<!-- Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Starter for testing Spring Boot applications with libraries including JUnit,
Hamcrest and Mockito -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
</dependency>
Ok i have made this work, you need to use Mockito to accompish what you are looking for:
Maven Dependency
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.6.4</version>
</dependency>
Test Class Set up
import static org.mockito.Mockito.*;
import static org.springframework.test.util.AopTestUtils.getTargetObject;
#RunWith(SpringRunner.class)
#SpringBootTest(classes = MyApp.class)
#TestPropertySource(
locations = { "classpath:myapp-test.properties" },
properties = { "test.key = testValue" })
public class AnswerTest {
// This will be only for injecting, we will not be using this object in tests.
#Autowired
private Environment env;
// This is the reference that will be used in tests.
private Environment envSpied;
// Map of properties that you intend to mock
private Map<String, String> mockedProperties;
#PostConstruct
public void postConstruct(){
mockedProperties = new HashMap<String, String>();
mockedProperties.put("mocked.test.key_1", "mocked.test.value_1");
mockedProperties.put("mocked.test.key_2", "mocked.test.value_2");
mockedProperties.put("mocked.test.key_3", "mocked.test.value_3");
// We use the Spy feature of mockito which enabled partial mocking
envSpied = Mockito.spy((Environment) getTargetObject(env));
// We mock certain retrieval of certain properties
// based on the logic contained in the implementation of Answer class
doAnswer(new CustomAnswer()).when(envSpied).getProperty(Mockito.anyString());
}
Test case
// Testing for both mocked and real properties in same test method
#Test public void shouldReturnAdequateProperty() {
String mockedValue = envSpied.getProperty("mocked.test.key_3");
String realValue = envSpied.getProperty("test.key");
assertEquals(mockedValue, "mocked.test.value_3");
assertEquals(realValue, "testValue");
}
Implementation of Mockito's Answer interface
// Here we define what should mockito do:
// a) return mocked property if the key is a mock
// b) invoke real method on Environment otherwise
private class CustomAnswer implements Answer<String>{
#Override
public String answer(InvocationOnMock invocationOnMock) throws Throwable {
Object[] arguments = invocationOnMock.getArguments();
String parameterKey = (String) arguments[0];
String mockedValue = mockedProperties.get(parameterKey);
if(mockedValue != null){
return mockedValue;
}
return (String) invocationOnMock.callRealMethod();
}
}
}
Try it out, and let me know if all is clear here.