BAEL-5534 Configure JWT Authentication for OpenAPI (#12288)

Co-authored-by: Bhaskara Navuluri <bhaskara.navuluri@hpe.com>
This commit is contained in:
Bhaskara 2022-06-02 20:50:02 +05:30 committed by GitHub
parent 06edd99b79
commit 55867b7206
11 changed files with 539 additions and 4 deletions

View File

@ -32,6 +32,14 @@
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@ -48,6 +56,11 @@
<artifactId>springdoc-openapi-data-rest</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-security</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- restdocs -->
<dependency>
<groupId>org.springframework.restdocs</groupId>
@ -131,7 +144,7 @@
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>0.2</version>
<version>1.4</version>
<executions>
<execution>
<phase>integration-test</phase>
@ -152,7 +165,7 @@
</profiles>
<properties>
<springdoc.version>1.6.4</springdoc.version>
<springdoc.version>1.6.8</springdoc.version>
<asciidoctor-plugin.version>1.5.6</asciidoctor-plugin.version>
<snippetsDirectory>${project.build.directory}/generated-snippets</snippetsDirectory>
</properties>

View File

@ -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<String> 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();
}
}

View File

@ -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 <strong>john/password</strong>")
.bearerFormat("JWT")));
//@formatter:on
}
}

View File

@ -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)));
}
}

View File

@ -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);
}
}

View File

@ -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 + "]";
}
}

View File

@ -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());
}
}

View File

@ -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-----

View File

@ -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-----

View File

@ -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
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 <strong>ROLE_USER</strong> role. If an account is not created, the user will be granted the <b>ROLE_USER</b> role.
springdoc.swagger-ui.operationsSorter=alpha
springdoc.swagger-ui.tagsSorter=alpha

View File

@ -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("<div id=\"swagger-ui\"></div>"));
}
@Test
@DisplayName("LiveTest - Check Headers")
void whenInvokeOpenApi_thenCheckHeaders()
{
assertNotNull(authenticationApi);
ResponseEntity<String> 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<String> 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<String> 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\""));
}
}