diff --git a/tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java b/tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java index 665e9af..d1eb992 100644 --- a/tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java +++ b/tests/src/test/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolverTests.java @@ -43,6 +43,25 @@ void testResolveTokenSuccessfully(VaultEnvironment vaultEnvironment) throws Exce assertTrue(jwtToken.payload().size() > 0, "jwtToken.payload: " + jwtToken.payload()); } + @Test + void testParseTokenSuccessfully(VaultEnvironment vaultEnvironment) { + final var token = vaultEnvironment.newServiceToken(); + + final var jwtToken = + new JsonwebtokenResolver( + JwksKeyLocator.builder() + .jwksUri(vaultEnvironment.jwksUri()) + .connectTimeout(Duration.ofSeconds(3)) + .requestTimeout(Duration.ofSeconds(3)) + .keyTtl(1000) + .build()) + .parse(token); + + assertNotNull(jwtToken, "jwtToken"); + assertTrue(jwtToken.header().size() > 0, "jwtToken.header: " + jwtToken.header()); + assertTrue(jwtToken.payload().size() > 0, "jwtToken.payload: " + jwtToken.payload()); + } + @Test void testJwksKeyLocatorThrowsError(VaultEnvironment vaultEnvironment) { final var token = vaultEnvironment.newServiceToken(); diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java index 60e6489..a94670a 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JsonwebtokenResolver.java @@ -1,8 +1,13 @@ package io.scalecube.security.tokens.jwt; +import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Locator; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.Key; +import java.util.Base64; +import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; import org.slf4j.Logger; @@ -30,7 +35,7 @@ public CompletableFuture resolve(String token) { (jwtToken, ex) -> { if (jwtToken != null) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Resolved token: {}", mask(token)); + LOGGER.debug("Resolved JWT: {}", mask(token)); } return jwtToken; } @@ -38,13 +43,36 @@ public CompletableFuture resolve(String token) { if (ex instanceof JwtTokenException) { throw (JwtTokenException) ex; } else { - throw new JwtTokenException("Failed to resolve token: " + mask(token), ex); + throw new JwtTokenException("Failed to resolve JWT: " + mask(token), ex); } } return null; }); } + @Override + public JwtToken parse(String token) { + String[] parts = token.split("\\."); + if (parts.length != 3) { + throw new JwtTokenException("Invalid JWT format"); + } + + try { + final var urlDecoder = Base64.getUrlDecoder(); + final var headerJson = new String(urlDecoder.decode(parts[0]), StandardCharsets.UTF_8); + final var payloadJson = new String(urlDecoder.decode(parts[1]), StandardCharsets.UTF_8); + + final var mapper = new ObjectMapper(); + final var header = mapper.readValue(headerJson, Map.class); + final var claims = mapper.readValue(payloadJson, Map.class); + + //noinspection unchecked + return new JwtToken(header, claims); + } catch (IOException e) { + throw new JwtTokenException("Failed to decode JWT", e); + } + } + private static String mask(String data) { if (data == null || data.length() < 5) { return "*****"; diff --git a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java index 4c5a61a..d326043 100644 --- a/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java +++ b/tokens/src/main/java/io/scalecube/security/tokens/jwt/JwtTokenResolver.java @@ -5,10 +5,18 @@ public interface JwtTokenResolver { /** - * Verifies and returns token claims. + * Verifies given JWT and parses its header and claims. * * @param token jwt token - * @return mono result with parsed claims (or error) + * @return async result with {@link JwtToken}, or error */ CompletableFuture resolve(String token); + + /** + * Parses given JWT without verifying its signature. + * + * @param token jwt token + * @return parsed token + */ + JwtToken parse(String token); }