specialtyIds) throws InvalidVetDataException {
Vet vet = new Vet();
vet.setFirstName(firstName);
diff --git a/backend/src/main/java/org/springframework/samples/petclinic/security/JwtAuthenticationFilter.java b/backend/src/main/java/org/springframework/samples/petclinic/security/JwtAuthenticationFilter.java
deleted file mode 100644
index 9ad9919d..00000000
--- a/backend/src/main/java/org/springframework/samples/petclinic/security/JwtAuthenticationFilter.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package org.springframework.samples.petclinic.security;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.samples.petclinic.auth.User;
-import org.springframework.samples.petclinic.auth.UserRepository;
-import org.springframework.security.authentication.BadCredentialsException;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
-import org.springframework.stereotype.Component;
-import org.springframework.web.filter.OncePerRequestFilter;
-
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import java.io.IOException;
-
-/**
- * Parses JWT Token from Request Header and authenticated the user
- *
- * - if no authentication header is set, no authentication is done
- * - if the given jwt token is either invalid (bad format for example) or expired, authentication is denied
- *
- * Note that this is an example only. DO NOT IMPLEMENT OWN SECURITY CODE IN REAL PRODUCTION APPS !!!!!!!!!!
- *
- * @author Nils Hartmann (nils@nilshartmann.net)
- */
-@Component
-public class JwtAuthenticationFilter extends OncePerRequestFilter {
- private final Logger logger = LoggerFactory.getLogger(getClass());
-
- private final JwtTokenService jwtTokenService;
- private final UserRepository userRepository;
-
- public JwtAuthenticationFilter(JwtTokenService jwtTokenService, UserRepository userRepository) {
- this.jwtTokenService = jwtTokenService;
- this.userRepository = userRepository;
- }
-
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
- throws ServletException, IOException {
-
- try {
- logger.trace("Inside JwtAuthenticationFilter");
- authenticateIfNeeded(request);
- } catch (AuthenticationException bed) {
- logger.error("Could not authenticate: " + bed, bed);
- SecurityContextHolder.clearContext();
- response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
- return;
- }
-
- filterChain.doFilter(request, response);
- }
-
- private void authenticateIfNeeded(HttpServletRequest request) {
- final String token = getJwtFromRequest(request);
- if (token != null) {
- if (!jwtTokenService.isValidToken(token)) {
- throw new BadCredentialsException("Invalid authorization token");
- }
-
- String username = jwtTokenService.getUsernameFromToken(token);
- User user = userRepository.findByUsername(username).orElseThrow(() -> new BadCredentialsException("Invalid User in Token"));
-
- UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null,
- user.getAuthorities());
- authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
- SecurityContextHolder.getContext().setAuthentication(authentication);
- }
- }
-
- private String getJwtFromRequest(HttpServletRequest request) {
- String tokenParameter = request.getParameter("token");
- if (tokenParameter != null) {
- return tokenParameter;
- }
-
-
-
- String authHeader = request.getHeader("Authorization");
- if (authHeader == null) {
- return null;
- }
- if (!authHeader.startsWith("Bearer ")) {
- throw new BadCredentialsException(
- "Invalid 'Authorization'-Header");
- }
- return authHeader.substring(7, authHeader.length());
- }
-
-}
diff --git a/backend/src/main/java/org/springframework/samples/petclinic/security/JwtTokenService.java b/backend/src/main/java/org/springframework/samples/petclinic/security/JwtTokenService.java
index 5c268ac4..da4fb264 100644
--- a/backend/src/main/java/org/springframework/samples/petclinic/security/JwtTokenService.java
+++ b/backend/src/main/java/org/springframework/samples/petclinic/security/JwtTokenService.java
@@ -1,97 +1,49 @@
package org.springframework.samples.petclinic.security;
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.security.Keys;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.samples.petclinic.auth.User;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.jwt.JwtClaimsSet;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;
-import jakarta.annotation.PostConstruct;
-import javax.crypto.SecretKey;
-import java.nio.charset.StandardCharsets;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.stream.Collectors;
/**
- * Note that this is an example only. DO NOT IMPLEMENT OWN SECURITY CODE IN REAL PRODUCTION APPS !!!!!!!!!!
- *
- * @author Nils Hartmann (nils@nilshartmann.net)
+ * Based con code taken from Dan Vega https://github.com/danvega/jwt-username-password/blob/master/src/main/java/dev/danvega/jwt/service/TokenService.java
*/
@Service
public class JwtTokenService {
- private final Logger logger = LoggerFactory.getLogger(getClass());
- @Value("${jwt.expirationInMs:7200000}")
- private int jwtExpirationInMs;
+ private final JwtEncoder encoder;
- private final SecretKey secretKey;
-
- public JwtTokenService(@Value("${jwt.secretString:fasdfahsdufak4923674asbclbca73,f,a,dfw}") String secretString) {
- this.secretKey = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
- }
-
- /** Creates a token that will never expire and will be stable accross re-starts
- * as longs as jwt.secretString does not change.
- *
- * This token can be used for easier testing using command line tools etc.
- * YOU SHOULD NEVER DO THIS IN 'REAL' PRODUCTION APPS
- */
- @PostConstruct
- void createNeverExpiringToken() throws Exception {
- SimpleDateFormat f = new SimpleDateFormat("dd.MM.yyyy HH:mm");
- String neverExpiringManagerToken = Jwts.builder()
- .setSubject("susi")
- .setIssuedAt(f.parse("25.12.2020 10:44"))
- .setExpiration(f.parse("25.12.2044 10:44"))
- .signWith(secretKey)
- .compact();
-
- String neverExpiringUserToken = Jwts.builder()
- .setSubject("joe")
- .setIssuedAt(f.parse("25.12.2020 10:44"))
- .setExpiration(f.parse("25.12.2044 10:44"))
- .signWith(secretKey)
- .compact();
- logger.info("\n\nNever Expiring JWT Token\n\n - ROLE_MANAGER: '{}'\n As HTTP Header: 'Authorization: Bearer {}'\n\n - ROLE_USER: '{}'\n As HTTP Header: 'Authorization: Bearer {}'\n", neverExpiringManagerToken, neverExpiringManagerToken, neverExpiringUserToken, neverExpiringUserToken);
+ public JwtTokenService(JwtEncoder encoder) {
+ this.encoder = encoder;
}
- public String createTokenForUser(User user) {
-
- Date now = new Date();
- Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
-
- return Jwts.builder()
- .setSubject(user.getUsername())
- .setIssuedAt(now)
- .setExpiration(expiryDate)
- .signWith(secretKey)
- .compact();
+ public String generateToken(Authentication authentication) {
+ return generateToken(authentication.getName(),
+ authentication.getAuthorities(),
+ Instant.now().plus(1, ChronoUnit.HOURS)
+ );
}
- public String getUsernameFromToken(String token) {
- Claims claims = Jwts.parserBuilder()
- .setSigningKey(secretKey)
- .build()
- .parseClaimsJws(token)
- .getBody();
-
- return claims.getSubject();
- }
+ public String generateToken(String name, Collection extends GrantedAuthority> authorities, Instant expiresAt) {
+ String scope = authorities.stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.joining(" "));
- public boolean isValidToken(String authToken) {
- try {
- Jwts.parserBuilder()
- .setSigningKey(secretKey)
- .build()
- .parseClaimsJws(authToken);
- return true;
- } catch (Exception ex) {
- logger.info("Invalid JWT token: " + ex);
- }
+ JwtClaimsSet claims = JwtClaimsSet.builder()
+ .issuer("self")
+ .issuedAt(Instant.now())
+ .expiresAt(expiresAt)
+ .subject(name)
+ .claim("scope", scope)
+ .build();
- return false;
+ return this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
diff --git a/backend/src/main/java/org/springframework/samples/petclinic/security/LoginController.java b/backend/src/main/java/org/springframework/samples/petclinic/security/LoginController.java
index 9b9c89a4..6d339004 100644
--- a/backend/src/main/java/org/springframework/samples/petclinic/security/LoginController.java
+++ b/backend/src/main/java/org/springframework/samples/petclinic/security/LoginController.java
@@ -1,28 +1,24 @@
package org.springframework.samples.petclinic.security;
+import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
-import org.springframework.samples.petclinic.auth.User;
-import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
-import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
-import jakarta.validation.Valid;
-
/**
- * Login via Username/Password with "REST" API.
+ * Login via Username/Password via HTTP POST endpoint.
*
* - The /graphql endpoints requires a valid token. This token can be requests by invoking
* HTTP "POST /login with" sending username and password.
*
- * Note that this is an example only. DO NOT IMPLEMENT OWN SECURITY CODE IN REAL PRODUCTION APPS !!!!!!!!!!
*
* @author Nils Hartmann (nils@nilshartmann.net)
*/
@@ -31,31 +27,30 @@ public class LoginController {
private static final Logger log = LoggerFactory.getLogger(LoginController.class);
- private final AuthenticationProvider authenticationProvider;
- private final JwtTokenService jwtTokenService;
+ private final JwtTokenService tokenService;
+ private final AuthenticationManager authenticationManager;
- public LoginController(AuthenticationProvider authenticationProvider, JwtTokenService jwtTokenService) {
- this.authenticationProvider = authenticationProvider;
- this.jwtTokenService = jwtTokenService;
+ public LoginController(JwtTokenService tokenService, AuthenticationManager authenticationManager) {
+ this.tokenService = tokenService;
+ this.authenticationManager = authenticationManager;
}
@PostMapping("/login")
public ResponseEntity login(@RequestBody @Valid LoginRequest request) {
+ log.info("Authorizing '{}'", request.getUsername());
try {
- Authentication authenticate = authenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
+ Authentication authentication = authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
+ );
- if (authenticate != null && (authenticate.getPrincipal() instanceof User)) {
- User principal = (User) authenticate.getPrincipal();
- String jwtToken = jwtTokenService.createTokenForUser(principal);
+ String token = tokenService.generateToken(authentication);
- return ResponseEntity.ok()
- .body(new LoginResponse((jwtToken)));
+ return ResponseEntity.ok(new LoginResponse(token));
- }
- } catch (AuthenticationException ex) {
- log.error("Authentication failed: " + ex, ex);
+ } catch (Exception ex) {
+ log.error("could not authenticate: " + ex, ex);
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
- return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
@GetMapping("/ping")
diff --git a/backend/src/main/java/org/springframework/samples/petclinic/security/NeverExpiringTokenGenerator.java b/backend/src/main/java/org/springframework/samples/petclinic/security/NeverExpiringTokenGenerator.java
new file mode 100644
index 00000000..963bd300
--- /dev/null
+++ b/backend/src/main/java/org/springframework/samples/petclinic/security/NeverExpiringTokenGenerator.java
@@ -0,0 +1,71 @@
+package org.springframework.samples.petclinic.security;
+
+import jakarta.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.samples.petclinic.auth.UserRepository;
+import org.springframework.stereotype.Component;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+
+/**
+ * Generates a non-expiring token for testing.
+ *
+ * 👮 👮 👮 YOU SHOULD NEVER DO THIS IN PRODUCTION 👮 👮 👮
+ *
+ * @author Nils Hartmann (nils@nilshartmann.net)
+ */
+@Component
+class NeverExpiringTokenGenerator {
+
+ private static final Logger log = LoggerFactory.getLogger(NeverExpiringTokenGenerator.class);
+
+ private final UserRepository userRepository;
+ private final JwtTokenService tokenService;
+
+ NeverExpiringTokenGenerator(UserRepository userRepository, JwtTokenService tokenService) {
+ this.userRepository = userRepository;
+ this.tokenService = tokenService;
+ }
+
+ /**
+ * Creates a token that will never expire and will be stable accross re-starts
+ * as longs as the RSAKey does not change (keys from publicKey and privateKey application properties)
+ *
+ * This token can be used for easier testing using command line tools etc.
+ * 👮 👮 👮 YOU SHOULD NEVER DO THIS IN 'REAL' PRODUCTION APPS 👮 👮 👮
+ */
+ @PostConstruct
+ void createNonExpiringTokens() {
+ var somewhen = Instant.now().plus(10 * 365, ChronoUnit.DAYS);
+
+ var susi = userRepository.findByUsername("susi").orElseThrow();
+ var neverExpiringManagerToken = tokenService.generateToken(susi.getUsername(), susi.getRoles(), somewhen);
+
+ var joe = userRepository.findByUsername("joe").orElseThrow();
+ var neverExpiringUserToken = tokenService.generateToken(joe.getUsername(), joe.getRoles(), somewhen);
+ log.info("""
+
+ ===============================================================
+ 🚨 🚨 🚨 NEVER EXPIRING JWT TOKENS 🚨 🚨 🚨
+ ===============================================================
+ SCOPE_MANAGER
+ login: '{}'
+
+ {"Authorization": "Bearer {}"}
+
+ SCOPE_USER
+ login: '{}'
+
+ {"Authorization": "Bearer {}"}
+
+ ===============================================================
+ """,
+ susi.getUsername(),
+ neverExpiringManagerToken,
+ joe.getUsername(),
+ neverExpiringUserToken);
+ }
+}
diff --git a/backend/src/main/java/org/springframework/samples/petclinic/security/RSAKeyProvider.java b/backend/src/main/java/org/springframework/samples/petclinic/security/RSAKeyProvider.java
new file mode 100644
index 00000000..95ff3f09
--- /dev/null
+++ b/backend/src/main/java/org/springframework/samples/petclinic/security/RSAKeyProvider.java
@@ -0,0 +1,56 @@
+package org.springframework.samples.petclinic.security;
+
+import com.nimbusds.jose.jwk.RSAKey;
+import jakarta.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.security.KeyFactory;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import java.util.UUID;
+
+/**
+ * @author Nils Hartmann (nils@nilshartmann.net)
+ *
+ * Based on Code from Dan Vega: https://github.com/danvega/jwt-username-password/blob/master/src/main/java/dev/danvega/jwt/security/Jwks.java
+ */
+@Component
+public class RSAKeyProvider {
+ private static final Logger log = LoggerFactory.getLogger(RSAKeyProvider.class);
+
+ private final String privateKeyString;
+ private final String publicKeyString;
+
+ private RSAKey rsaKey;
+
+ public RSAKeyProvider(@Value("${publicKey}") String publicKeyString, @Value("${privateKey}") String privateKeyString) {
+ this.privateKeyString = privateKeyString;
+ this.publicKeyString = publicKeyString;
+ }
+
+ public RSAKey getRsaKey() {
+ return rsaKey;
+ }
+
+ @PostConstruct
+ private void generateRsaKey() throws Exception {
+ log.info("Generating RSA KEY......");
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyString));
+ RSAPrivateKey privateKey = (RSAPrivateKey) kf.generatePrivate(privateSpec);
+
+ X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyString));
+ RSAPublicKey publicKey = (RSAPublicKey) kf.generatePublic(keySpecX509);
+
+ this.rsaKey = new RSAKey.Builder(publicKey)
+ .privateKey(privateKey)
+ .keyID(UUID.randomUUID().toString())
+ .build();
+ }
+}
diff --git a/backend/src/main/java/org/springframework/samples/petclinic/security/SecurityConfig.java b/backend/src/main/java/org/springframework/samples/petclinic/security/SecurityConfig.java
index 99b9c292..a9c0b3e9 100644
--- a/backend/src/main/java/org/springframework/samples/petclinic/security/SecurityConfig.java
+++ b/backend/src/main/java/org/springframework/samples/petclinic/security/SecurityConfig.java
@@ -1,21 +1,30 @@
package org.springframework.samples.petclinic.security;
-import jakarta.servlet.http.HttpServletResponse;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.samples.petclinic.auth.UserRepository;
-import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+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.core.userdetails.UsernameNotFoundException;
+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.web.SecurityFilterChain;
-import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@@ -23,25 +32,27 @@
import java.util.Arrays;
/**
- * Adds JWT-based Authentication and Authorization to the server
- *
- * Note that this is an example only. DO NOT IMPLEMENT OWN SECURITY CODE IN REAL PRODUCTION APPS !!!!!!!!!!
+ * Configures security for PetClinic. Ensures that all requests to /graphql are secured.
*
* @author Nils Hartmann (nils@nilshartmann.net)
*/
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) // Enable @PreAuthorize method-level security
public class SecurityConfig {
+ private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
- private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
-
- private final JwtAuthenticationFilter jwtAuthenticationFilter;
+ private final RSAKey rsaKey;
- public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, UserRepository userRepository) {
- this.jwtAuthenticationFilter = jwtAuthenticationFilter;
+ public SecurityConfig(RSAKeyProvider RSAKeyProvider) {
+ this.rsaKey = RSAKeyProvider.getRsaKey();
}
- private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
+ @Bean
+ public AuthenticationManager authManager(UserDetailsService userDetailsService) {
+ var authProvider = new DaoAuthenticationProvider();
+ authProvider.setUserDetailsService(userDetailsService);
+ return new ProviderManager(authProvider);
+ }
@Bean
public UserDetailsService userDetailsService(UserRepository userRepository) {
@@ -54,13 +65,6 @@ public UserDetailsService userDetailsService(UserRepository userRepository) {
);
}
- @Bean
- public AuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService) {
- DaoAuthenticationProvider p = new DaoAuthenticationProvider();
- p.setUserDetailsService(userDetailsService);
- return p;
- }
-
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
@@ -72,24 +76,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// for h2 explorer
http.headers().frameOptions().sameOrigin();
- // Exception Handling
- http
- .exceptionHandling()
- .authenticationEntryPoint(
- (request, response, ex) -> {
- logger.error("Unauthorized request to '{}'- {}",
- request.getRequestURL(),
- ex.getMessage());
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
- }
- );
http.authorizeHttpRequests(authorizeHttpRequests ->
authorizeHttpRequests
.shouldFilterAllDispatcherTypes(false)
// allow login
.requestMatchers("/login/**").permitAll()
- // allow access to graphiql
+// // allow access to graphiql
.requestMatchers("/").permitAll()
.requestMatchers("/favicon.ico").permitAll()
.requestMatchers("/s.html").permitAll()
@@ -100,13 +93,27 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.anyRequest().authenticated()
);
-
- // Register JWT filter
- http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
+ http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
+ @Bean
+ public JWKSource jwkSource(RSAKeyProvider RSAKeyProvider) {
+ JWKSet jwkSet = new JWKSet(rsaKey);
+ return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
+ }
+
+ @Bean
+ JwtEncoder jwtEncoder(JWKSource jwks) {
+ return new NimbusJwtEncoder(jwks);
+ }
+
+ @Bean
+ JwtDecoder jwtDecoder() throws JOSEException {
+ return NimbusJwtDecoder.withPublicKey(rsaKey.toRSAPublicKey()).build();
+ }
+
@Bean
public CorsConfigurationSource corsConfigurationSource(Environment env) {
@@ -117,7 +124,7 @@ public CorsConfigurationSource corsConfigurationSource(Environment env) {
Arrays.stream(allowedOrigins.split(","))
.forEach(origin -> {
- logger.info("Allowing Cors for host '{}'", origin);
+ log.info("Allowing Cors for host '{}'", origin);
config.addAllowedOrigin(origin);
});
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
index 40f79587..4e1b641e 100644
--- a/backend/src/main/resources/application.properties
+++ b/backend/src/main/resources/application.properties
@@ -28,6 +28,14 @@ logging.level.org.springframework=INFO
# Spring Security
#----------------------------------------------------------------
spring.security.filter.dispatcher-types=request,error
+# Note that in real life:
+# your would NEVER CHECK IN KEYS TO GIT (esp. no private keys)
+# This is only to make the demo easier:
+# - no need to generate the keys yourself
+# - ability to provide stable, long living tokens for easier (live) demos
+# NEVER check in your keys
+privateKey=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6IqWZJBcNadJriNrR5VjouvTBENkNXEwc4IHimrKxmdUwG2RuT8gjq1kR7YN4//bY60tS35HQkILKL2c7ghfGuq4NMGT+jn4pb0Bum4O9/E5zw7rS0LsS9ZtIsiNmt/kS6HgbTHrFpruTCN0oUoYJb72MN+g43NvrFNdpehaRFXk8EU96HugCAHcMfBjBg5o7z3E9SSr864BgTClfb3EWMNKKFwClsu2NNgDLITrCGAzMxAguDhMrFUE/4wCfq0pu80C2XHq3w3nwhPghNKsR2MaBQzhgTjGqeLhfXYaiqKg2TBKesW1aJ02S45VBdvmfGQc6J0QnV6WZ/NgHxmclAgMBAAECggEATG8t0VUgokFyRvZL5SVo/SjImP9yIipklvcaXxNnjca95KNa+nGInh0e1l3SPz3c//afV8i2+A5fpucQXT9uxAykrIXS8zVQWXN14fW6g6m4DZGyhh9wsrhoq9F6+BNUFI+hwpQFVmTBYf+h0Y0RvY5FJ+9NTQxkwoQscQPRgIG8CXJQIuvSmCIvFgKG0mfSfm8vTNNLt/yjtZS8daSQH18jAixM8Qg0gL+1ycnVnqb7wUexAi+gIY1IIYn4rt2yZiS+8nm2NTEt9zXgJ29zfUD3En3S4q8uii+bkr610pU6uiLy4iBfvq48sK5JUwHhHEP/MgDyyYfXukgY94mNgQKBgQC/Ho3y4QOYoY2xp3eyiJw9Cm78wRLfXmTwBIQD2n+IuHU0FndoQvL7q1Vk52tDKZ1VffeO3MjDr6FtORlLNElZzHYmuRAJayJrcAv41GZmlRjljsZ5r1Q02Sh7cY+ZiCO3JchMcZcpRkjpIk6XVwXAu1lIBs6uDFTKIiFXkilE7wKBgQD5UvNxGGJVu0QhF9YpJKyRfXXvzhIkjHyPRaHszV/fMlztlUQhsf81Ece8+otbW6G7faS6XXDRTCSXnUsiKIGV4xsJVLspUbvQUxSgqEgTW64HuauSPoCsNwAgSwFJx+wBqQp/z8Xt8Qi/dyyjzR0EPwUxcg0MuL2/I4pZMgNdKwKBgQCpHFoyXZSXC+ybFDpACc1l3cOTdyxB5f6euwAtgLt0uPNMYczKvuLOei6XmIY66anEKjBRg19KeC/4u5t8BgoZoWeyn/JmwxuzwXN2fEsX3jondgx9Q/zYvoappDSSf/pKZu4zZheBeCWv1KGCHIiEn8JCj3N2YobsTQL/v98wlQKBgQDRyxUBs0z7aspXNmGE2VFEU1er47CshHJDdIpeYior+4rqd9GOsOJYW4/A2unsg9xBkeXM09ecpf+cfES+h2sTHrifT2b1y8rH70DKNw//CgQUiA1wz5siTl2SzspUtR9H/l/RUJnyMAH2amdrpaqm6giKpLeKXuF7NoKxrz3ZZQKBgEJGeKRrmJPH9guUPN81KxBqp/59FRW7vyMYMzBoGaW/8Vi3WHhcJpa6Mhv3JkMmqVVmVmQ6B5CuiD0dmaNlbZZkfoA452iZULG9Kml3s/JH1rBwI6nuRFi3rv9dh4vEtm2c7Ms9Mg93wUevXaQKLqQPqgndnMb1VT7SnqaFSpLi
+publicKey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuiKlmSQXDWnSa4ja0eVY6Lr0wRDZDVxMHOCB4pqysZnVMBtkbk/II6tZEe2DeP/22OtLUt+R0JCCyi9nO4IXxrquDTBk/o5+KW9AbpuDvfxOc8O60tC7EvWbSLIjZrf5Euh4G0x6xaa7kwjdKFKGCW+9jDfoONzb6xTXaXoWkRV5PBFPeh7oAgB3DHwYwYOaO89xPUkq/OuAYEwpX29xFjDSihcApbLtjTYAyyE6whgMzMQILg4TKxVBP+MAn6tKbvNAtlx6t8N58IT4ITSrEdjGgUM4YE4xqni4X12GoqioNkwSnrFtWidNkuOVQXb5nxkHOidEJ1elmfzYB8ZnJQIDAQAB
#----------------------------------------------------------------
# spring-graphql config
diff --git a/backend/src/main/resources/db/hsqldb/populateDB.sql b/backend/src/main/resources/db/hsqldb/populateDB.sql
index cdb08b4e..358779b4 100644
--- a/backend/src/main/resources/db/hsqldb/populateDB.sql
+++ b/backend/src/main/resources/db/hsqldb/populateDB.sql
@@ -4,8 +4,8 @@
INSERT INTO users (USERNAME, PASSWORD, ENABLED, FULLNAME) VALUES ('joe', '{noop}joe', true, 'Joe Hill');
INSERT INTO users (USERNAME, PASSWORD, ENABLED, FULLNAME) VALUES ('susi', '{noop}susi', true, 'Susi Smith');
-INSERT INTO roles (ID, USERNAME, ROLE) VALUES (0, 'susi', 'ROLE_MANAGER');
-INSERT INTO roles (ID, USERNAME, ROLE) VALUES (1, 'joe', 'ROLE_USER');
+INSERT INTO roles (ID, USERNAME, ROLE) VALUES (0, 'susi', 'MANAGER');
+INSERT INTO roles (ID, USERNAME, ROLE) VALUES (1, 'joe', 'USER');
diff --git a/backend/src/test/java/org/springframework/samples/petclinic/graphql/AbstractClinicGraphqlTests.java b/backend/src/test/java/org/springframework/samples/petclinic/graphql/AbstractClinicGraphqlTests.java
index a76d825c..cb6f61d9 100644
--- a/backend/src/test/java/org/springframework/samples/petclinic/graphql/AbstractClinicGraphqlTests.java
+++ b/backend/src/test/java/org/springframework/samples/petclinic/graphql/AbstractClinicGraphqlTests.java
@@ -7,14 +7,21 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.WebGraphQlTester;
import org.springframework.http.HttpHeaders;
+import org.springframework.samples.petclinic.security.JwtTokenService;
import org.springframework.test.context.ActiveProfiles;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
@SpringBootTest
@ActiveProfiles(profiles = {"hsqldb"})
@AutoConfigureMockMvc
@AutoConfigureHttpGraphQlTester
public class AbstractClinicGraphqlTests {
+ @Autowired
+ private JwtTokenService tokenService;
protected WebGraphQlTester managerRoleGraphQlTester;
protected WebGraphQlTester userRoleGraphQlTester;
protected WebGraphQlTester unauthorizedGraphqlTester;
@@ -24,19 +31,21 @@ void setupWebGraphqlTester(@Autowired WebGraphQlTester graphQlTester) {
this.unauthorizedGraphqlTester = graphQlTester;
this.userRoleGraphQlTester = graphQlTester.mutate()
- .headers(AbstractClinicGraphqlTests::withUserToken)
+ .headers(this::withUserToken)
.build();
this.managerRoleGraphQlTester = graphQlTester.mutate()
- .headers(AbstractClinicGraphqlTests::withManagerToken)
+ .headers(this::withManagerToken)
.build();
}
- private static void withManagerToken(HttpHeaders headers) {
- headers.setBearerAuth("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdXNpIiwiaWF0IjoxNjA4ODg5NDQwLCJleHAiOjIzNjYyNzE4NDB9.XG0SEtHiidGuy2A1zy_BfixVMFOv3gGbfwGqEc3F-KU");
+ private void withManagerToken(HttpHeaders headers) {
+ var token = tokenService.generateToken("susi", List.of( () -> "MANAGER"), Instant.now().plus(1, ChronoUnit.HOURS));
+ headers.setBearerAuth(token);
}
- private static void withUserToken(HttpHeaders headers) {
- headers.setBearerAuth("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2UiLCJpYXQiOjE2MDg4ODk0NDAsImV4cCI6MjM2NjI3MTg0MH0.V36ynhDffqb9LQFsckOdk6lFhcVEDhOCFxFCQDAYG0o");
+ private void withUserToken(HttpHeaders headers) {
+ var token = tokenService.generateToken("joe", List.of( () -> "USER"), Instant.now().plus(1, ChronoUnit.HOURS));
+ headers.setBearerAuth(token);
}
}
diff --git a/login.http b/login.http
new file mode 100644
index 00000000..5e89defc
--- /dev/null
+++ b/login.http
@@ -0,0 +1,14 @@
+### PING!
+GET http://localhost:9977/ping
+Authorization: Bearer eyJraWQiOiIxMTA4YzcxNS02MGIwLTQ4OGEtYjg4Yy04NGI1Njg1ZDU1MTEiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzZWxmIiwic3ViIjoic3VzaSIsImV4cCI6MTk4NzYwMDQ0MywiaWF0IjoxNjcyMjQwNDQzLCJzY29wZSI6IlJPTEVfTUFOQUdFUiJ9.hNPB-kTUEzTWEeeOOQIbN3QUeB6jFX4NNesuDGNZ9KXt3T_dfEMIBsdOkwrcPqxtkXMzugwKfSx8VV_aqFznO2NlEzlKA7ngDbjWj0T4Ozr9q1E5aVieWzt9QvI7_AoG21e5upea8T7vUO7Dy64YLFcIPL6Gvhgw0zsSyxNxLbyqQBvwpwu5EzwTWmr9plAjGwIiRbnxkbMqK1mdHuh7QemG9am6-ocfPE5SaLXk1w6Y-wWhMQTDS2JtC__tFumLunUUMqm019lqrILQFRGWqRLZRKnRDP2QEyfrSbOdhacCIPY8KOn0Zk7bstgGCyG71soM0HPnpFXthsScZY4_Zg
+
+### LOGIN
+
+POST http://localhost:9977/login
+Content-Type: application/json
+
+{
+ "username": "joe",
+ "password": "joe"
+}
+
diff --git a/readme.md b/readme.md
index 129ea4da..6b05663d 100644
--- a/readme.md
+++ b/readme.md
@@ -3,7 +3,7 @@
This PetClinic version uses the new [spring-graphql](https://github.com/spring-projects/spring-graphql) project, that has been [introduced](https://spring.io/blog/2021/07/06/hello-spring-graphql) in july 2021
and has been [finally released as 1.0.0 GA version](https://spring.io/blog/2022/05/19/spring-for-graphql-1-0-release) in May 2022.
-This version uses **Spring Boot 3.0** with **Spring for GraphQL 1.1**.
+This version uses **Spring Boot 3.0.x** with **Spring for GraphQL 1.1**.
It implements a [GraphQL API](http://graphql.org/) for the PetClinic and
provides an example Frontend for the API.