From 24cdf2b67221252c7aaf5930a8d283fe7d101394 Mon Sep 17 00:00:00 2001 From: Bhaskara Navuluri Date: Tue, 31 May 2022 20:24:47 +0530 Subject: [PATCH 1/3] BAEL-5534 Configure JWT Authentication for OpenAPI --- .../spring-boot-springdoc/pom.xml | 17 ++- .../com/baeldung/jwt/AuthenticationApi.java | 86 ++++++++++++ .../baeldung/jwt/OpenAPI30Configuration.java | 52 ++++++++ .../baeldung/jwt/SecurityConfiguration.java | 124 ++++++++++++++++++ .../jwt/SecurityTokenApplication.java | 17 +++ .../src/main/java/com/baeldung/jwt/User.java | 56 ++++++++ .../main/java/com/baeldung/jwt/UserApi.java | 50 +++++++ .../src/main/resources/app.key | 28 ++++ .../src/main/resources/app.pub | 9 ++ .../src/main/resources/application.properties | 15 ++- .../jwt/OpenApiJwtIntegrationTest.java | 89 +++++++++++++ 11 files changed, 539 insertions(+), 4 deletions(-) create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/AuthenticationApi.java create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/OpenAPI30Configuration.java create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityConfiguration.java create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityTokenApplication.java create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/User.java create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/UserApi.java create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/resources/app.key create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/resources/app.pub create mode 100644 spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/jwt/OpenApiJwtIntegrationTest.java diff --git a/spring-boot-modules/spring-boot-springdoc/pom.xml b/spring-boot-modules/spring-boot-springdoc/pom.xml index e7d4a35d97..88e616119d 100644 --- a/spring-boot-modules/spring-boot-springdoc/pom.xml +++ b/spring-boot-modules/spring-boot-springdoc/pom.xml @@ -32,6 +32,14 @@ com.h2database h2 + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + org.springframework.boot spring-boot-starter-test @@ -48,6 +56,11 @@ springdoc-openapi-data-rest ${springdoc.version} + + org.springdoc + springdoc-openapi-security + ${springdoc.version} + org.springframework.restdocs @@ -131,7 +144,7 @@ org.springdoc springdoc-openapi-maven-plugin - 0.2 + 1.4 integration-test @@ -152,7 +165,7 @@ - 1.6.4 + 1.6.8 1.5.6 ${project.build.directory}/generated-snippets diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/AuthenticationApi.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/AuthenticationApi.java new file mode 100644 index 0000000000..c53774129a --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/AuthenticationApi.java @@ -0,0 +1,86 @@ +package com.baeldung.jwt; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtEncoderParameters; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Instant; +import java.util.stream.Collectors; + +/** + * REST APIs that contains all the operations that can be performed for authentication like login, registration etc + */ +@RequestMapping("/api/auth") +@RestController +@Tag(name = "Authentication", description = "The Authentication API. Contains operations like change password, forgot password, login, logout, etc.") +public class AuthenticationApi { + + private final UserDetailsService userDetailsService; + private final JwtEncoder encoder; + + public AuthenticationApi(UserDetailsService userDetailsService, JwtEncoder encoder) { + this.userDetailsService = userDetailsService; + this.encoder = encoder; + } + + /** + * API to Login + * + * @param user The login entity that contains username and password + * @return Returns the JWT token + * @see com.baeldung.jwt.User + */ + @Operation(summary = "User Authentication", description = "Authenticate the user and return a JWT token if the user is valid.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @io.swagger.v3.oas.annotations.media.Content(mediaType = "application/json", examples = @io.swagger.v3.oas.annotations.media.ExampleObject(value = "{\n" + " \"username\": \"jane\",\n" + + " \"password\": \"password\"\n" + "}", summary = "User Authentication Example"))) + @PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity login(@RequestBody User user) { + UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername()); + if (user.getPassword().equalsIgnoreCase(userDetails.getPassword())) { + String token = generateToken(userDetails); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set("X-AUTH-TOKEN", token); + return ResponseEntity.ok().headers(httpHeaders).contentType(MediaType.APPLICATION_JSON).body("{\"token\":\"" + token + "\"}"); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).contentType(MediaType.APPLICATION_JSON).body("Invalid username or password"); + } + } + + /** + * Generates the JWT token with claims + * + * @param userDetails The user details + * @return Returns the JWT token + */ + private String generateToken(UserDetails userDetails) { + Instant now = Instant.now(); + long expiry = 36000L; + // @formatter:off + String scope = userDetails.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(" ")); + JwtClaimsSet claims = JwtClaimsSet.builder() + .issuer("self") + .issuedAt(now) + .expiresAt(now.plusSeconds(expiry)) + .subject(userDetails.getUsername()) + .claim("scope", scope) + .build(); + // @formatter:on + return this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); + } + +} diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/OpenAPI30Configuration.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/OpenAPI30Configuration.java new file mode 100644 index 0000000000..53f0b735fe --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/OpenAPI30Configuration.java @@ -0,0 +1,52 @@ +package com.baeldung.jwt; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; +import io.swagger.v3.oas.annotations.servers.Server; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +//@formatter:off +@OpenAPIDefinition( + info = @Info(title = "User API", version = "${api.version}", + contact = @Contact(name = "Baeldung", email = "user-apis@baeldung.com", url = "https://www.baeldung.com"), + license = @License(name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0"), termsOfService = "${tos.uri}", + description = "${api.description}"), + servers = { + @Server(url = "http://localhost:8080", description = "Development"), + @Server(url = "${api.server.url}", description = "Production")}) +//@formatter:on +public class OpenAPI30Configuration { + + /** + * Configure the OpenAPI components. + * + * @return Returns fully configure OpenAPI object + * @see OpenAPI + */ + @Bean + public OpenAPI customizeOpenAPI() { + //@formatter:off + final String securitySchemeName = "bearerAuth"; + return new OpenAPI() + .addSecurityItem(new SecurityRequirement() + .addList(securitySchemeName)) + .components(new Components() + .addSecuritySchemes(securitySchemeName, new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .description( + "Provide the JWT token. JWT token can be obtained from the Login API. For testing, use the credentials john/password") + .bearerFormat("JWT"))); + //@formatter:on + + } +} diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityConfiguration.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityConfiguration.java new file mode 100644 index 0000000000..6b42a8f1bb --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityConfiguration.java @@ -0,0 +1,124 @@ +package com.baeldung.jwt; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; +import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +import org.springframework.security.core.userdetails.User; + +/** + * This class is inspired from + * https://github.com/spring-projects/spring-security-samples/blob/5.7.x/servlet/spring-boot/java/jwt/login/src/main/java/example/RestConfig.java + */ +@EnableWebSecurity +@Configuration +public class SecurityConfiguration { + + @Value("${jwt.public.key}") + RSAPublicKey publicKey; + + @Value("${jwt.private.key}") + RSAPrivateKey privateKey; + + /** + * This bean is used to configure the JWT token. Configure the URLs that should not be protected by the JWT token. + * + * @param http the HttpSecurity object + * @return the HttpSecurity object + * @throws Exception if an error occurs + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + //@formatter:off + return http + .authorizeHttpRequests(authorizeRequests -> authorizeRequests + .antMatchers("/api/auth/**", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", + "/swagger-ui/index.html") + .permitAll() + .anyRequest() + .authenticated()) + .cors().disable() + .csrf().disable() + .formLogin().disable() + .httpBasic().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint()) + .accessDeniedHandler(new BearerTokenAccessDeniedHandler()) + .and()) + .build(); + //@formatter:on + } + + /** + * For demonstration/example, we use the InMemoryUserDetailsManager. + * + * @return Returns the UserDetailsService with pre-configure credentials. + * @see InMemoryUserDetailsManager + */ + @Bean + UserDetailsService allUsers() { + // @formatter:off + InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); + manager + .createUser(User.builder() + .passwordEncoder(password -> password) + .username("john") + .password("password") + .authorities("USER") + .roles("USER").build()); + manager + .createUser(User.builder() + .passwordEncoder(password -> password) + .username("jane") + .password("password") + .authorities("USER") + .roles("USER").build()); + return manager; + // @formatter:on + } + + /** + * This bean is used to decode the JWT token. + * + * @return Returns the JwtDecoder bean to decode JWT tokens. + */ + @Bean + JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withPublicKey(this.publicKey).build(); + } + + /** + * This bean is used to encode the JWT token. + * + * @return Returns the JwtEncoder bean to encode JWT tokens. + */ + @Bean + JwtEncoder jwtEncoder() { + JWK jwk = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey).build(); + return new NimbusJwtEncoder(new ImmutableJWKSet<>(new JWKSet(jwk))); + } +} + diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityTokenApplication.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityTokenApplication.java new file mode 100644 index 0000000000..4c0c4f01d8 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityTokenApplication.java @@ -0,0 +1,17 @@ +package com.baeldung.jwt; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SecurityTokenApplication { + + /** + * The bootstrap method + * @param args + */ + public static void main(String[] args) { + SpringApplication.run(SecurityTokenApplication.class, args); + } + +} diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/User.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/User.java new file mode 100644 index 0000000000..43427c609f --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/User.java @@ -0,0 +1,56 @@ +package com.baeldung.jwt; + + +import java.io.Serializable; + +public class User implements Serializable { + + private static final long serialVersionUID = 3317686311392412458L; + private String username; + private String password; + private String role; + private String email; + + public User(String username, String password, String role) { + this.username = username; + this.password = password; + this.role = role; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Override + public String toString() { + return "User [username=" + username + ", password=" + password + ", role=" + role + "]"; + } +} diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/UserApi.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/UserApi.java new file mode 100644 index 0000000000..d2d17978ba --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/UserApi.java @@ -0,0 +1,50 @@ +package com.baeldung.jwt; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.text.MessageFormat; + +/** + * REST APIs that contain all the operations that can be performed on a User + */ +@RequestMapping("/api/user") +@RestController +@Tag(name = "User", description = "The User API. Contains all the operations that can be performed on a user.") +public class UserApi { + + private final UserDetailsService userDetailsService; + + public UserApi(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + /** + * API to get the current user. Returns the user details for the provided JWT token + * @param authentication The authentication object that contains the JWT token + * @return Returns the user details for the provided JWT token + */ + @Operation(summary = "Get user details", description = "Get the user details. The operation returns the details of the user that is associated " + "with the provided JWT token.") + @GetMapping + public UserDetails getUser(Authentication authentication) { + return userDetailsService.loadUserByUsername(authentication.getName()); + } + + /** + * API to delete the current user. + * @param authentication The authentication object that contains the JWT token + * @return Returns a success message on deletion of the user + */ + @Operation(summary = "Delete user details", description = "Delete user details. The operation deletes the details of the user that is " + "associated with the provided JWT token.") + @DeleteMapping + public String deleteUser(Authentication authentication) { + return MessageFormat.format("User {0} deleted successfully", authentication.getName()); + } +} diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/resources/app.key b/spring-boot-modules/spring-boot-springdoc/src/main/resources/app.key new file mode 100644 index 0000000000..53510079ac --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/resources/app.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA +iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM +g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK +LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF +oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc +3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn ++jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE +E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek +lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG +mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7 +62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0 +bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA ++Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH +Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA +8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd +I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY +QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d +rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk +HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA +Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN +HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a +FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF +snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H +c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM +TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR +47jndeyIaMTNETEmOnms+as17g== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/resources/app.pub b/spring-boot-modules/spring-boot-springdoc/src/main/resources/app.pub new file mode 100644 index 0000000000..0b2ee7b336 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/resources/app.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd +7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv +c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6 +iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2 +kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o +RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj +KwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/resources/application.properties b/spring-boot-modules/spring-boot-springdoc/src/main/resources/application.properties index 733e716e76..a668601a7d 100644 --- a/spring-boot-modules/spring-boot-springdoc/src/main/resources/application.properties +++ b/spring-boot-modules/spring-boot-springdoc/src/main/resources/application.properties @@ -1,6 +1,5 @@ # custom path for swagger-ui springdoc.swagger-ui.path=/swagger-ui-custom.html -springdoc.swagger-ui.operationsSorter=method # custom path for api docs springdoc.api-docs.path=/api-docs @@ -10,4 +9,16 @@ spring.datasource.url=jdbc:h2:mem:springdoc ## for com.baeldung.restdocopenapi ## springdoc.version=@springdoc.version@ -spring.jpa.hibernate.ddl-auto=none \ No newline at end of file +spring.jpa.hibernate.ddl-auto=none + +## for com.baeldung.jwt ## +jwt.private.key=classpath:app.key +jwt.public.key=classpath:app.pub + + +api.version=1.0-SNAPSHOT +tos.uri=terms-of-service +api.server.url=https://www.baeldung.com +api.description=The User API is used to create, update, and delete users. Users can be created with or without an associated account. If an account is created, the user will be granted the ROLE_USER role. If an account is not created, the user will be granted the ROLE_USER role. +springdoc.swagger-ui.operationsSorter=alpha +springdoc.swagger-ui.tagsSorter=alpha \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/jwt/OpenApiJwtIntegrationTest.java b/spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/jwt/OpenApiJwtIntegrationTest.java new file mode 100644 index 0000000000..1ea88d14fa --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/jwt/OpenApiJwtIntegrationTest.java @@ -0,0 +1,89 @@ +package com.baeldung.jwt; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DisplayName("OpenAPI JWT Live Tests") +class OpenApiJwtIntegrationTest +{ + @LocalServerPort + private int port; + @Autowired + private AuthenticationApi authenticationApi; + @Autowired + private TestRestTemplate restTemplate; + + @Test + @DisplayName("LiveTest - Render Swagger UI") + void whenInvokeSwagger_thenRenderIndexPage() + { + assertNotNull(authenticationApi); + + String response = this.restTemplate.getForObject("http://localhost:" + port + "/swagger-ui.html", String.class); + + assertNotNull(response); + assertTrue(response.contains("Swagger UI")); + assertTrue(response.contains("
")); + } + + @Test + @DisplayName("LiveTest - Check Headers") + void whenInvokeOpenApi_thenCheckHeaders() + { + assertNotNull(authenticationApi); + + ResponseEntity response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", + String.class); + + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getHeaders().get("Content-Type")); + assertEquals(1, response.getHeaders().get("Content-Type").size()); + assertEquals("application/json", response.getHeaders().get("Content-Type").get(0)); + } + + @Test + @DisplayName("LiveTest - Verify OpenAPI Document") + void whenInvokeOpenApi_thenVerifyOpenApiDoc() + { + assertNotNull(authenticationApi); + + ResponseEntity response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", + String.class); + + assertNotNull(response); + assertNotNull(response.getBody()); + assertTrue(response.getBody().contains("\"openapi\":")); + assertTrue(response.getBody().contains("User API")); + assertTrue(response.getBody().contains("\"post\"")); + } + + @Test + @DisplayName("LiveTest - Verify OpenAPI Security Section") + void whenInvokeOpenApi_thenCheckSecurityConfig() + { + assertNotNull(authenticationApi); + + ResponseEntity response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", + String.class); + + assertNotNull(response); + assertNotNull(response.getBody()); + assertTrue(response.getBody().contains("\"securitySchemes\"")); + assertTrue(response.getBody().contains("\"bearerFormat\":\"JWT\"")); + assertTrue(response.getBody().contains("\"scheme\":\"bearer\"")); + } + +} + From a26d45e5c0d481a38d533cb056b7ea274bb0febd Mon Sep 17 00:00:00 2001 From: Bhaskara Navuluri Date: Mon, 6 Jun 2022 20:35:38 +0530 Subject: [PATCH 2/3] Fixed the Integration Build Pipeline --- .../spring-boot-springdoc/pom.xml | 6 +- .../baeldung/jwt/SecurityConfiguration.java | 4 +- .../jwt/SecurityTokenApplication.java | 4 +- .../jwt/OpenApiJwtIntegrationTest.java | 8 +- .../MongoAuthApplicationIntegrationTest.java | 118 ------------------ 5 files changed, 11 insertions(+), 129 deletions(-) delete mode 100644 spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java diff --git a/spring-boot-modules/spring-boot-springdoc/pom.xml b/spring-boot-modules/spring-boot-springdoc/pom.xml index 88e616119d..18b1774920 100644 --- a/spring-boot-modules/spring-boot-springdoc/pom.xml +++ b/spring-boot-modules/spring-boot-springdoc/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 spring-boot-springdoc 0.0.1-SNAPSHOT @@ -112,6 +110,8 @@ application.properties data.sql schema.sql + app.key + app.pub diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityConfiguration.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityConfiguration.java index 6b42a8f1bb..216740f979 100644 --- a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityConfiguration.java +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityConfiguration.java @@ -52,8 +52,8 @@ public class SecurityConfiguration { //@formatter:off return http .authorizeHttpRequests(authorizeRequests -> authorizeRequests - .antMatchers("/api/auth/**", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", - "/swagger-ui/index.html") + .antMatchers("/api/auth/**", "/swagger-ui-custom.html" ,"/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", + "/swagger-ui/index.html","/api-docs/**") .permitAll() .anyRequest() .authenticated()) diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityTokenApplication.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityTokenApplication.java index 4c0c4f01d8..ba396e1703 100644 --- a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityTokenApplication.java +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/jwt/SecurityTokenApplication.java @@ -11,7 +11,7 @@ public class SecurityTokenApplication { * @param args */ public static void main(String[] args) { - SpringApplication.run(SecurityTokenApplication.class, args); + SpringApplication.run(SecurityTokenApplication.class); } - } + diff --git a/spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/jwt/OpenApiJwtIntegrationTest.java b/spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/jwt/OpenApiJwtIntegrationTest.java index 1ea88d14fa..fecd4101eb 100644 --- a/spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/jwt/OpenApiJwtIntegrationTest.java +++ b/spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/jwt/OpenApiJwtIntegrationTest.java @@ -30,7 +30,7 @@ class OpenApiJwtIntegrationTest { assertNotNull(authenticationApi); - String response = this.restTemplate.getForObject("http://localhost:" + port + "/swagger-ui.html", String.class); + String response = this.restTemplate.getForObject("http://localhost:" + port + "/swagger-ui/index.html", String.class); assertNotNull(response); assertTrue(response.contains("Swagger UI")); @@ -43,7 +43,7 @@ class OpenApiJwtIntegrationTest { assertNotNull(authenticationApi); - ResponseEntity response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", + ResponseEntity response = this.restTemplate.getForEntity("http://localhost:" + port + "/api-docs", String.class); assertNotNull(response); @@ -59,7 +59,7 @@ class OpenApiJwtIntegrationTest { assertNotNull(authenticationApi); - ResponseEntity response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", + ResponseEntity response = this.restTemplate.getForEntity("http://localhost:" + port + "/api-docs", String.class); assertNotNull(response); @@ -75,7 +75,7 @@ class OpenApiJwtIntegrationTest { assertNotNull(authenticationApi); - ResponseEntity response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", + ResponseEntity response = this.restTemplate.getForEntity("http://localhost:" + port + "/api-docs", String.class); assertNotNull(response); diff --git a/spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java b/spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java deleted file mode 100644 index b7994cad9e..0000000000 --- a/spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.baeldung.mongoauth; - -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.util.Collections; -import java.util.HashSet; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import com.baeldung.mongoauth.domain.Role; -import com.baeldung.mongoauth.domain.User; -import com.baeldung.mongoauth.domain.UserRole; - -@SpringBootTest(classes = { MongoAuthApplication.class }) -@AutoConfigureMockMvc -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -class MongoAuthApplicationIntegrationTest { - - @Autowired - private WebApplicationContext context; - - @Autowired - private MongoTemplate mongoTemplate; - - @Autowired - private BCryptPasswordEncoder bCryptPasswordEncoder; - - private MockMvc mvc; - - private static final String USER_NAME = "user@gmail.com"; - private static final String ADMIN_NAME = "admin@gmail.com"; - private static final String PASSWORD = "password"; - - @BeforeEach - public void setup() { - - setUp(); - - mvc = MockMvcBuilders.webAppContextSetup(context) - .apply(springSecurity()) - .build(); - } - - private void setUp() { - Role roleUser = new Role(); - roleUser.setName("ROLE_USER"); - mongoTemplate.save(roleUser); - - User user = new User(); - user.setUsername(USER_NAME); - user.setPassword(bCryptPasswordEncoder.encode(PASSWORD)); - - UserRole userRole = new UserRole(); - userRole.setRole(roleUser); - user.setUserRoles(new HashSet<>(Collections.singletonList(userRole))); - mongoTemplate.save(user); - - User admin = new User(); - admin.setUsername(ADMIN_NAME); - admin.setPassword(bCryptPasswordEncoder.encode(PASSWORD)); - - Role roleAdmin = new Role(); - roleAdmin.setName("ROLE_ADMIN"); - mongoTemplate.save(roleAdmin); - - UserRole adminRole = new UserRole(); - adminRole.setRole(roleAdmin); - admin.setUserRoles(new HashSet<>(Collections.singletonList(adminRole))); - mongoTemplate.save(admin); - } - - @Test - void givenUserCredentials_whenInvokeUserAuthorizedEndPoint_thenReturn200() throws Exception { - mvc.perform(get("/user").with(httpBasic(USER_NAME, PASSWORD))) - .andExpect(status().isOk()); - } - - @Test - void givenUserNotExists_whenInvokeEndPoint_thenReturn401() throws Exception { - mvc.perform(get("/user").with(httpBasic("not_existing_user", "password"))) - .andExpect(status().isUnauthorized()); - } - - @Test - void givenUserExistsAndWrongPassword_whenInvokeEndPoint_thenReturn401() throws Exception { - mvc.perform(get("/user").with(httpBasic(USER_NAME, "wrong_password"))) - .andExpect(status().isUnauthorized()); - } - - @Test - void givenUserCredentials_whenInvokeAdminAuthorizedEndPoint_thenReturn403() throws Exception { - mvc.perform(get("/admin").with(httpBasic(USER_NAME, PASSWORD))) - .andExpect(status().isForbidden()); - } - - @Test - void givenAdminCredentials_whenInvokeAdminAuthorizedEndPoint_thenReturn200() throws Exception { - mvc.perform(get("/admin").with(httpBasic(ADMIN_NAME, PASSWORD))) - .andExpect(status().isOk()); - - mvc.perform(get("/user").with(httpBasic(ADMIN_NAME, PASSWORD))) - .andExpect(status().isOk()); - } - -} From 502f947cdb05e7e707554e33c6d320fddd88affb Mon Sep 17 00:00:00 2001 From: Bhaskara Navuluri Date: Tue, 7 Jun 2022 12:16:50 +0530 Subject: [PATCH 3/3] Restored the deleted test file --- .../MongoAuthApplicationIntegrationTest.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java diff --git a/spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java b/spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java new file mode 100644 index 0000000000..fc9fd1ef30 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-3/src/test/java/com/baeldung/mongoauth/MongoAuthApplicationIntegrationTest.java @@ -0,0 +1,118 @@ +package com.baeldung.mongoauth; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Collections; +import java.util.HashSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.baeldung.mongoauth.domain.Role; +import com.baeldung.mongoauth.domain.User; +import com.baeldung.mongoauth.domain.UserRole; + +@SpringBootTest(classes = { MongoAuthApplication.class }) +@AutoConfigureMockMvc +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class MongoAuthApplicationIntegrationTest { + + @Autowired + private WebApplicationContext context; + + @Autowired + private MongoTemplate mongoTemplate; + + @Autowired + private BCryptPasswordEncoder bCryptPasswordEncoder; + + private MockMvc mvc; + + private static final String USER_NAME = "user@gmail.com"; + private static final String ADMIN_NAME = "admin@gmail.com"; + private static final String PASSWORD = "password"; + + @BeforeEach + public void setup() { + + setUp(); + + mvc = MockMvcBuilders.webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + private void setUp() { + Role roleUser = new Role(); + roleUser.setName("ROLE_USER"); + mongoTemplate.save(roleUser); + + User user = new User(); + user.setUsername(USER_NAME); + user.setPassword(bCryptPasswordEncoder.encode(PASSWORD)); + + UserRole userRole = new UserRole(); + userRole.setRole(roleUser); + user.setUserRoles(new HashSet<>(Collections.singletonList(userRole))); + mongoTemplate.save(user); + + User admin = new User(); + admin.setUsername(ADMIN_NAME); + admin.setPassword(bCryptPasswordEncoder.encode(PASSWORD)); + + Role roleAdmin = new Role(); + roleAdmin.setName("ROLE_ADMIN"); + mongoTemplate.save(roleAdmin); + + UserRole adminRole = new UserRole(); + adminRole.setRole(roleAdmin); + admin.setUserRoles(new HashSet<>(Collections.singletonList(adminRole))); + mongoTemplate.save(admin); + } + + @Test + void givenUserCredentials_whenInvokeUserAuthorizedEndPoint_thenReturn200() throws Exception { + mvc.perform(get("/user").with(httpBasic(USER_NAME, PASSWORD))) + .andExpect(status().isOk()); + } + + @Test + void givenUserNotExists_whenInvokeEndPoint_thenReturn401() throws Exception { + mvc.perform(get("/user").with(httpBasic("not_existing_user", "password"))) + .andExpect(status().isUnauthorized()); + } + + @Test + void givenUserExistsAndWrongPassword_whenInvokeEndPoint_thenReturn401() throws Exception { + mvc.perform(get("/user").with(httpBasic(USER_NAME, "wrong_password"))) + .andExpect(status().isUnauthorized()); + } + + @Test + void givenUserCredentials_whenInvokeAdminAuthorizedEndPoint_thenReturn403() throws Exception { + mvc.perform(get("/admin").with(httpBasic(USER_NAME, PASSWORD))) + .andExpect(status().isForbidden()); + } + + @Test + void givenAdminCredentials_whenInvokeAdminAuthorizedEndPoint_thenReturn200() throws Exception { + mvc.perform(get("/admin").with(httpBasic(ADMIN_NAME, PASSWORD))) + .andExpect(status().isOk()); + + mvc.perform(get("/user").with(httpBasic(ADMIN_NAME, PASSWORD))) + .andExpect(status().isOk()); + } + +}