diff --git a/spring-addons-oauth2-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/annotations/WithJwt.java b/spring-addons-oauth2-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/annotations/WithJwt.java
index 52dade242..970e5b50a 100644
--- a/spring-addons-oauth2-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/annotations/WithJwt.java
+++ b/spring-addons-oauth2-test/src/main/java/com/c4_soft/springaddons/security/oauth2/test/annotations/WithJwt.java
@@ -50,8 +50,8 @@
import reactor.core.publisher.Mono;
/**
- * Annotation to setup test {@link SecurityContext} with an {@link Authentication} instantiated by the (Reactive)JwtAuthenticaionConverter
- * in the security conf. Usage on tests decorated with @AutoConfigureAddonsSecurity or @AutoConfigureAddonsWebSecurity::
+ * Annotation to setup test {@link SecurityContext} with an {@link Authentication} instantiated by the (Reactive)JwtAuthenticaionConverter in the security conf.
+ * Usage on tests decorated with @AutoConfigureAddonsSecurity or @AutoConfigureAddonsWebSecurity::
*
*
* @Test
@@ -61,8 +61,8 @@
* }
*
*
- * For usage with @ParameterizedTest, you'll need a {@link MethodSource @MethodSource} in a test running with
- * @TestInstance(Lifecycle.PER_CLASS). Authentication instance should be injected in the test with @ParameterizedAuthentication.
+ * For usage with @ParameterizedTest, you'll need a {@link MethodSource @MethodSource} in a test running with @TestInstance(Lifecycle.PER_CLASS).
+ * Authentication instance should be injected in the test with @ParameterizedAuthentication.
*
*
* @Autowired
@@ -79,8 +79,8 @@
* }
*
*
- * If using spring-addons-oauth2-test without spring-addons-starter-oidc-test, you should explicitly import
- * @Import(AuthenticationFactoriesTestConf.class) (otherwise, the @Addons...Test will pull this configuration for you)
+ * If using spring-addons-oauth2-test without spring-addons-starter-oidc-test, you should explicitly import @Import(AuthenticationFactoriesTestConf.class)
+ * (otherwise, the @Addons...Test will pull this configuration for you)
*
* @author Jérôme Wacongne <ch4mp@c4-soft.com>
*/
@@ -90,135 +90,150 @@
@Documented
@WithSecurityContext(factory = WithJwt.AuthenticationFactory.class)
public @interface WithJwt {
- @AliasFor("file")
- String value() default "";
-
- @AliasFor("value")
- String file() default "";
-
- String json() default "";
-
- String bearerString() default AuthenticationFactory.DEFAULT_BEARER;
-
- String headers() default AuthenticationFactory.DEFAULT_HEADERS;
-
- @RequiredArgsConstructor
- public static final class AuthenticationFactory implements WithSecurityContextFactory {
- static final String DEFAULT_BEARER = "test.jwt.bearer";
- static final String DEFAULT_HEADERS = "{\"alg\": \"none\"}";
-
- private final Optional> jwtAuthenticationConverter;
-
- private final Optional>> reactiveJwtAuthenticationConverter;
-
- private final Converter defaultAuthenticationConverter = new JwtAuthenticationConverter();
-
- @Override
- public SecurityContext createSecurityContext(WithJwt annotation) {
- final var auth = authentication(annotation);
-
- final var securityContext = SecurityContextHolder.createEmptyContext();
- securityContext.setAuthentication(auth);
-
- return securityContext;
- }
-
- /**
- * @param annotation Test annotation with reference to a classpath resource or a JSON string to get claims from (and optional JWT headers
- * and Bearer string)
- * @return an {@link Authentication} instance built by the JWT authentication converter in security configuration
- */
- public AbstractAuthenticationToken authentication(WithJwt annotation) {
- final var headers = parseJson(annotation.headers());
-
- final var claims = new HashMap();
- if (StringUtils.hasText(annotation.value())) {
- claims.putAll(parseFile(annotation.value()));
- }
- if (StringUtils.hasText(annotation.file())) {
- claims.putAll(parseFile(annotation.file()));
- }
- if (StringUtils.hasText(annotation.json())) {
- claims.putAll(parseJson(annotation.json()));
- }
-
- return authentication(claims, headers, annotation.bearerString());
- }
-
- /**
- * @param claims the test JWT claims
- * @param headers the test JWT headers
- * @param bearerString the test JWT Bearer String
- * @return an {@link Authentication} instance built by the JWT authentication converter in security configuration
- */
- @SuppressWarnings("null")
- public AbstractAuthenticationToken authentication(Map claims, Map headers, String bearerString) {
- final var now = Instant.now();
- final var iat = Optional.ofNullable((Integer) claims.get(JWTClaimNames.ISSUED_AT)).map(Instant::ofEpochSecond).orElse(now);
- final var exp = Optional.ofNullable((Integer) claims.get(JWTClaimNames.EXPIRATION_TIME)).map(Instant::ofEpochSecond).orElse(now.plusSeconds(42));
-
- final var jwt = new Jwt(bearerString, iat, exp, headers, claims);
-
- return jwtAuthenticationConverter.map(c -> {
- final AbstractAuthenticationToken auth = c.convert(jwt);
- return auth;
- }).orElseGet(() -> reactiveJwtAuthenticationConverter.map(c -> {
- final AbstractAuthenticationToken auth = c.convert(jwt).block();
- return auth;
- }).orElse(defaultAuthenticationConverter.convert(jwt)));
- }
-
- /**
- * Build an {@link Authentication} for each of the claim-sets provided as classpath resources (JSON file)
- *
- * @param classpathResources classpath resources to get JWT claims from
- * @return an stream of {@link Authentication} instances built by the JWT authentication converter in security
- * configuration (using default JWT headers and Bearer String)
- */
- public Stream authenticationsFrom(String... classpathResources) {
- return Stream.of(classpathResources).map(AuthenticationFactory::parseFile)
- .map(claims -> this.authentication(claims, parseJson(DEFAULT_HEADERS), DEFAULT_BEARER));
- }
-
- /**
- * Extracts the claim-set in a JSON file
- *
- * @param fileName
- * @return
- */
- public static Map parseFile(String fileName) {
- if (!StringUtils.hasText(fileName)) {
- return Map.of();
- }
-
- InputStream cpRessource;
- try {
- cpRessource = new ClassPathResource(fileName).getInputStream();
- } catch (IOException e) {
- throw new RuntimeException("Failed to load classpath resource %s".formatted(fileName), e);
- }
- try {
- return new JSONParser(JSONParser.MODE_PERMISSIVE).parse(cpRessource, JSONObject.class);
- } catch (final ParseException | UnsupportedEncodingException e) {
- throw new RuntimeException("Invalid JWT payload in classpath resource %s".formatted(fileName));
- }
- }
-
- /**
- * Extracts the claim-set in a JSON String
- *
- * @param json
- * @return
- */
- public static Map parseJson(String json) {
- if (!StringUtils.hasText(json)) {
- return Map.of();
- }
- try {
- return new JSONParser(JSONParser.MODE_PERMISSIVE).parse(json, JSONObject.class);
- } catch (final ParseException e) {
- throw new RuntimeException("Invalid JSON payload in @WithJwt");
- }
- }
- }
+ @AliasFor("file")
+ String value() default "";
+
+ @AliasFor("value")
+ String file() default "";
+
+ String json() default "";
+
+ String bearerString() default AuthenticationFactory.DEFAULT_BEARER;
+
+ String headers() default AuthenticationFactory.DEFAULT_HEADERS;
+
+ @RequiredArgsConstructor
+ public static final class AuthenticationFactory implements WithSecurityContextFactory {
+ static final String DEFAULT_BEARER = "test.jwt.bearer";
+ static final String DEFAULT_HEADERS = "{\"alg\": \"none\"}";
+
+ private final Optional> jwtAuthenticationConverter;
+
+ private final Optional>> reactiveJwtAuthenticationConverter;
+
+ private final Converter defaultAuthenticationConverter = new JwtAuthenticationConverter();
+
+ @Override
+ public SecurityContext createSecurityContext(WithJwt annotation) {
+ final var auth = authentication(annotation);
+
+ final var securityContext = SecurityContextHolder.createEmptyContext();
+ securityContext.setAuthentication(auth);
+
+ return securityContext;
+ }
+
+ /**
+ * @param annotation Test annotation with reference to a classpath resource or a JSON string to get claims from (and optional JWT headers and Bearer
+ * string)
+ * @return an {@link Authentication} instance built by the JWT authentication converter in security configuration
+ */
+ public AbstractAuthenticationToken authentication(WithJwt annotation) {
+ final var headers = parseJson(annotation.headers());
+
+ final var claims = new HashMap();
+ if (StringUtils.hasText(annotation.value())) {
+ claims.putAll(parseFile(annotation.value()));
+ }
+ if (StringUtils.hasText(annotation.file())) {
+ claims.putAll(parseFile(annotation.file()));
+ }
+ if (StringUtils.hasText(annotation.json())) {
+ claims.putAll(parseJson(annotation.json()));
+ }
+
+ return authentication(claims, headers, annotation.bearerString());
+ }
+
+ /**
+ * @param claims the test JWT claims
+ * @param headers the test JWT headers
+ * @param bearerString the test JWT Bearer String
+ * @return an {@link Authentication} instance built by the JWT authentication converter in security configuration
+ */
+ @SuppressWarnings("null")
+ public AbstractAuthenticationToken authentication(Map claims, Map headers, String bearerString) {
+ final var now = Instant.now();
+ final var iat = Optional.ofNullable(toLong(claims.get(JWTClaimNames.ISSUED_AT))).map(Instant::ofEpochSecond).orElse(now);
+ final var exp = Optional.ofNullable(toLong(claims.get(JWTClaimNames.EXPIRATION_TIME))).map(Instant::ofEpochSecond).orElse(now.plusSeconds(42));
+
+ final var jwt = new Jwt(bearerString, iat, exp, headers, claims);
+
+ return jwtAuthenticationConverter.map(c -> {
+ final AbstractAuthenticationToken auth = c.convert(jwt);
+ return auth;
+ }).orElseGet(() -> reactiveJwtAuthenticationConverter.map(c -> {
+ final AbstractAuthenticationToken auth = c.convert(jwt).block();
+ return auth;
+ }).orElse(defaultAuthenticationConverter.convert(jwt)));
+ }
+
+ private Long toLong(Object claim) {
+ if (claim == null) {
+ return null;
+ }
+ if (claim instanceof Long l) {
+ return l;
+ }
+ if (claim instanceof Integer i) {
+ return i.longValue();
+ }
+ return null;
+ }
+
+ /**
+ * Build an {@link Authentication} for each of the claim-sets provided as classpath resources (JSON file)
+ *
+ * @param classpathResources classpath resources to get JWT claims from
+ * @return an stream of {@link Authentication} instances built by the JWT authentication converter in security configuration (using default JWT headers
+ * and Bearer String)
+ */
+ public Stream authenticationsFrom(String... classpathResources) {
+ return Stream
+ .of(classpathResources)
+ .map(AuthenticationFactory::parseFile)
+ .map(claims -> this.authentication(claims, parseJson(DEFAULT_HEADERS), DEFAULT_BEARER));
+ }
+
+ /**
+ * Extracts the claim-set in a JSON file
+ *
+ * @param fileName
+ * @return
+ */
+ public static Map parseFile(String fileName) {
+ if (!StringUtils.hasText(fileName)) {
+ return Map.of();
+ }
+
+ InputStream cpRessource;
+ try {
+ cpRessource = new ClassPathResource(fileName).getInputStream();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load classpath resource %s".formatted(fileName), e);
+ }
+ try {
+ return new JSONParser(JSONParser.MODE_PERMISSIVE).parse(cpRessource, JSONObject.class);
+ } catch (final ParseException | UnsupportedEncodingException e) {
+ throw new RuntimeException("Invalid JWT payload in classpath resource %s".formatted(fileName));
+ }
+ }
+
+ /**
+ * Extracts the claim-set in a JSON String
+ *
+ * @param json
+ * @return
+ */
+ public static Map parseJson(String json) {
+ if (!StringUtils.hasText(json)) {
+ return Map.of();
+ }
+ try {
+ return new JSONParser(JSONParser.MODE_PERMISSIVE).parse(json, JSONObject.class);
+ } catch (final ParseException e) {
+ throw new RuntimeException("Invalid JSON payload in @WithJwt");
+ }
+ }
+ }
}