From 86a6b6734fc9a5dd36e44b8d6a34bd8dac5a40e9 Mon Sep 17 00:00:00 2001 From: ch4mpy Date: Sat, 29 Jun 2024 04:04:49 -1000 Subject: [PATCH] gh-219 cast timestamp claims to Long instead of Integer in @WithJwt Authentication factory --- .../oauth2/test/annotations/WithJwt.java | 289 +++++++++--------- 1 file changed, 152 insertions(+), 137 deletions(-) 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"); + } + } + } }