I have an auth-server + resource server in one app. I've spent a lot of time searching and debugging, but there aren't many updated pages or topics about Spring Boot 3.+ related to this. So, I had this working and wanted to add a custom secret that will be shared between my client and server. And here is where the problems started...
This is my auth+resource server config:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Value("${security.jwt.secret}")
private String jwtSecret;
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
CorsConfigurationSource corsConfigurationSource) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));
http.cors(customizer -> customizer.configurationSource(corsConfigurationSource));
return http.build();
}
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
authorize -> authorize.requestMatchers("/oauth2/authorize").permitAll().anyRequest().authenticated())
.formLogin(formLogin -> formLogin.loginPage("/login").permitAll())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
return http.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtEncoder jwtEncoder() {
byte[] keyBytes = Base64.getDecoder().decode(jwtSecret);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
OctetSequenceKey octetKey = new OctetSequenceKey.Builder(secretKeySpec)
.keyID("customKey")
.build();
JWKSet jwkSet = new JWKSet(octetKey);
JWKSource<SecurityContext> jwkSource = (jwkSelector, context) -> {
List<JWK> keys = jwkSelector.select(jwkSet);
if (keys.isEmpty()) {
System.out.println("No keys found matching selection criteria!");
} else {
System.out.println("Keys selected: " + keys.stream().map(JWK::getKeyID).collect(Collectors.joining(", ")));
}
return keys;
};
return new NimbusJwtEncoder(jwkSource);
}
@Bean
JwtDecoder jwtDecoder() {
byte[] keyBytes = Base64.getDecoder().decode(jwtSecret);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
return NimbusJwtDecoder.withSecretKey(secretKeySpec).build();
}
}
And i have in my app.properties:
security.jwt.secret=r26BoWWyTQMp/8rkD3RnRKsbHkRsmQWjTvJTfmhrQxU=
I had everything working with asymmetric way (private and public key), but I wanted to try this wat too...
Now, when logging in with the client, I always receive:
org.springframework.security.oauth2.jwt.JwtEncodingException: An error occurred while attempting to encode the Jwt: Failed to select a JWK signing key
I have fixed the issue with this:
@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
@Value("${jwt.key}")
private String jwtKey;
private final TokenService tokenService;
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
CorsConfigurationSource corsConfigurationSource) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(jwtSpec -> {
jwtSpec.decoder(jwtDecoder());
}));
http.cors(customizer -> customizer.configurationSource(corsConfigurationSource));
return http.build();
}
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/hello").authenticated()
.anyRequest().permitAll())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder())))
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers(new AntPathRequestMatcher("/h2-console/**"));
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return tokenService.jwtCustomizer();
}
@Bean
public JwtEncoder jwtEncoder() {
return tokenService.jwtEncoder();
}
@Bean
public JwtDecoder jwtDecoder() {
byte[] keyBytes = Base64.getDecoder().decode(jwtKey);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
return NimbusJwtDecoder.withSecretKey(keySpec).build();
}
}
And the TokenService class:
@Service
public class TokenService {
@Value("${jwt.key}")
private String jwtKey;
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
context.getJwsHeader().algorithm(MacAlgorithm.HS256);
Date expirationDate =
Date.from(Instant.now().plus(Duration.ofHours(5)));
Date issueDate = Date.from(Instant.now());
context.getClaims().claims(claims -> {
claims.put("exp", expirationDate);
claims.put("iat", issueDate);
claims.put("custom", "custom");
});
}
};
}
public JwtEncoder jwtEncoder() {
return parameters -> {
byte[] secretKeyBytes = Base64.getDecoder().decode(jwtKey);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, "HmacSHA256");
try {
MACSigner signer = new MACSigner(secretKeySpec);
JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder();
parameters.getClaims().getClaims().forEach((key, value) ->
claimsSetBuilder.claim(key, value instanceof Instant ? Date.from((Instant) value) : value)
);
JWTClaimsSet claimsSet = claimsSetBuilder.build();
JWSHeader header = new JWSHeader(JWSAlgorithm.HS256);
SignedJWT signedJWT = new SignedJWT(header, claimsSet);
signedJWT.sign(signer);
return Jwt.withTokenValue(signedJWT.serialize())
.header("alg", header.getAlgorithm().getName())
.subject(claimsSet.getSubject())
.issuer(claimsSet.getIssuer())
.claims(claims -> claims.putAll(claimsSet.getClaims()))
.issuedAt(claimsSet.getIssueTime().toInstant())
.expiresAt(claimsSet.getExpirationTime().toInstant())
.build();
} catch (Exception e) {
throw new IllegalStateException("Error while signing the JWT", e);
}
};
}
}