Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,48 @@
# java-jwt

JWT verifier using QuintoAndar public key.
> **Deprecated:** This library is being phased out. New services should use
> [auth-java](https://github.com/quintoandar/backend-services/tree/main/libraries/auth-java) instead.

## publishing
JWT parser for QuintoAndar services.

Run a `./gradlew publish` to generate the artifacts and commit all files in `mvn-repo` directory.
## Versioning

## usage
### v1.6.x and earlier

TODO
The library validated JWTs at parse time:

- Fetched the public RSA key from an external endpoint at runtime (QuintoAndar auth service or Keycloak realm info).
- Verified the JWT signature against that key.
- For Keycloak tokens specifically, also required the `exp` claim to be present.
- Threw `InvalidJwtException` (jose4j) when validation failed.

### v2.0.0+

This version was introduced as a transitional step to remove internal JWT validation from services,
making it possible to rotate signing keys without requiring coordinated secret updates across all consumers.

The library now only parses the JWT payload: it base64url-decodes the payload segment and returns the claims map.
No signature verification, no expiry check. JWT validation is handled at the infrastructure level by Istio,
so by the time the token reaches the application it has already been validated.

Changes from v1.6.x:
- `getPayload()` no longer throws `InvalidJwtException`.
- `QuintoAndarPublicKeyService` and `QuintoAndarKeycloakPublicKeyService` were removed.
- `main.url` and `keycloak.url` properties are no longer required.
- jose4j and commons-io dependencies were removed.

## Publishing

Run `./gradlew publish` to generate the artifacts and commit all files in the `mvn-repo` directory.

## Usage

Inject `QuintoAndarJwt` (for QuintoAndar tokens) or `QuintoAndarKeycloakJwt` (for Keycloak tokens).
Both beans are auto-configured via Spring Boot autoconfiguration.

```java
@Autowired
QuintoAndarJwt quintoAndarJwt;

Optional<Map<String, Object>> payload = quintoAndarJwt.getPayload(jwtString);
```
6 changes: 2 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
}

group 'br.com.quintoandar'
version '1.6.2'
version '2.0.0'

sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
Expand All @@ -22,8 +22,6 @@ dependencies {
compile('org.springframework:spring-context:5.0.8.RELEASE')
compile('org.springframework:spring-beans:5.0.8.RELEASE')
compile('org.codehaus.groovy:groovy-all:2.4.15')
compile('commons-io:commons-io:2.6')
compile('org.bitbucket.b_c:jose4j:0.9.3')
compile('net.bytebuddy:byte-buddy:1.8.22')
compile('com.fasterxml.jackson.core:jackson-databind:2.11.1')

Expand Down Expand Up @@ -63,7 +61,7 @@ publishing {
mavenJava(MavenPublication) {
groupId = 'br.com.quintoandar'
artifactId = 'java-jwt'
version = '1.6.2'
version = '2.0.0'
from components.java
artifact sourcesJar
artifact javadocJar
Expand Down
4 changes: 1 addition & 3 deletions src/main/java/br/com/quintoandar/javajwt/QuintoAndarJwt.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package br.com.quintoandar.javajwt;

import org.jose4j.jwt.consumer.InvalidJwtException;

import java.util.Map;
import java.util.Optional;

public interface QuintoAndarJwt {

Optional<Map<String, Object>> getPayload(String jwt) throws InvalidJwtException;
Optional<Map<String, Object>> getPayload(String jwt);

}
91 changes: 21 additions & 70 deletions src/main/java/br/com/quintoandar/javajwt/QuintoAndarJwtBean.java
Original file line number Diff line number Diff line change
@@ -1,90 +1,41 @@
package br.com.quintoandar.javajwt;

import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.Optional;

/**
* Parses JWTs issued by QuintoAndar's main auth service.
*
* <p>This class performs <strong>no validation</strong> (no signature verification, no expiry check).
* It simply decodes the JWT payload and returns the claims map. Validation is expected to be
* handled externally (e.g. by the identity provider or API gateway).
*
* <p>This class is intentionally kept separate from {@link QuintoAndarKeycloakJwtBean} for
* backward compatibility: consumers of this library may depend on either interface
* ({@link QuintoAndarJwt} or {@link QuintoAndarKeycloakJwt}) and both must remain injectable
* as distinct Spring beans.
*/
public class QuintoAndarJwtBean implements QuintoAndarJwt {

private static final Logger LOGGER = LoggerFactory.getLogger(QuintoAndarJwtBean.class);

private static final String KEY_ALGORITHM = "RSA";

private QuintoAndarPublicKeyService quintoAndarPublicKeyService;

private JwtConsumer jwtConsumer;

@Autowired
public QuintoAndarJwtBean(final QuintoAndarPublicKeyService quintoAndarPublicKeyService) {
this.quintoAndarPublicKeyService = quintoAndarPublicKeyService;
}
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

@Override
public Optional<Map<String, Object>> getPayload(final String jwt) throws InvalidJwtException {
public Optional<Map<String, Object>> getPayload(final String jwt) {
if (jwt == null) {
return Optional.empty();
}

if (!isReady()) {
try {
setup();
} catch (SetupException e) {
LOGGER.error("Failed to setup QuintoAndarJwtBean", e);
return Optional.empty();
}
}

final Map<String, Object> payload = jwtConsumer.processToClaims(jwt).getClaimsMap();
return Optional.ofNullable(payload);
}

private void setup() throws SetupException {
try {
LOGGER.info("Setting up QuintoAndarJwtBean");
final KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
final X509EncodedKeySpec keySpec = getPublicKeySpec();
final RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
final RsaJsonWebKey webKey = new RsaJsonWebKey(publicKey);
jwtConsumer = new JwtConsumerBuilder().setVerificationKey(webKey.getKey()).build();
} catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) {
throw new SetupException(e);
final String[] parts = jwt.split("\\.");
final byte[] decoded = Base64.getUrlDecoder().decode(parts[1]);
final Map<String, Object> payload = OBJECT_MAPPER.readValue(decoded, Map.class);
return Optional.ofNullable(payload);
} catch (Exception e) {
return Optional.empty();
}
}

private boolean isReady() {
return jwtConsumer != null;
}

private X509EncodedKeySpec getPublicKeySpec() throws IOException {
final String encodedKey = strippedKey(quintoAndarPublicKeyService.fetchMainPublicKey());

final byte[] decodedKey = Base64.getDecoder().decode(encodedKey);

return new X509EncodedKeySpec(decodedKey);
}

private String strippedKey(final String publicKey) {
return publicKey.replaceAll("-----(BEGIN|END) PUBLIC KEY-----", "")
.replaceAll("\\n", "");
}

// visible for testing
protected void setQuintoAndarPublicKeyService(final QuintoAndarPublicKeyService quintoAndarPublicKeyService) {
this.quintoAndarPublicKeyService = quintoAndarPublicKeyService;
}

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package br.com.quintoandar.javajwt;

import org.jose4j.jwt.consumer.InvalidJwtException;

import java.util.Map;
import java.util.Optional;

public interface QuintoAndarKeycloakJwt {

Optional<Map<String, Object>> getPayload(String jwt) throws InvalidJwtException;
Optional<Map<String, Object>> getPayload(String jwt);

}
Original file line number Diff line number Diff line change
@@ -1,94 +1,41 @@
package br.com.quintoandar.javajwt;

import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.Optional;

/**
* Parses JWTs issued by Keycloak (QuintoAndar realm).
*
* <p>This class performs <strong>no validation</strong> (no signature verification, no expiry check).
* It simply decodes the JWT payload and returns the claims map. Validation is expected to be
* handled externally (e.g. by the identity provider or API gateway).
*
* <p>This class is intentionally kept separate from {@link QuintoAndarJwtBean} for
* backward compatibility: consumers of this library may depend on either interface
* ({@link QuintoAndarKeycloakJwt} or {@link QuintoAndarJwt}) and both must remain injectable
* as distinct Spring beans.
*/
public class QuintoAndarKeycloakJwtBean implements QuintoAndarKeycloakJwt {

private static final Logger LOGGER = LoggerFactory.getLogger(QuintoAndarKeycloakJwtBean.class);

private static final String KEY_ALGORITHM = "RSA";

private QuintoAndarKeycloakPublicKeyService quintoAndarKeycloakPublicKeyService;

private JwtConsumer jwtConsumer;

@Autowired
public QuintoAndarKeycloakJwtBean(final QuintoAndarKeycloakPublicKeyService quintoAndarKeycloakPublicKeyService) {
this.quintoAndarKeycloakPublicKeyService = quintoAndarKeycloakPublicKeyService;
}
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

@Override
public Optional<Map<String, Object>> getPayload(final String jwt) throws InvalidJwtException {
public Optional<Map<String, Object>> getPayload(final String jwt) {
if (jwt == null) {
return Optional.empty();
}

if (!isReady()) {
try {
setup();
} catch (SetupException e) {
LOGGER.error("Failed to setup QuintoAndarKeycloakJwtBean", e);
return Optional.empty();
}
}

final Map<String, Object> payload = jwtConsumer.processToClaims(jwt).getClaimsMap();
return Optional.ofNullable(payload);
}

private void setup() throws SetupException {
try {
LOGGER.info("Setting up QuintoAndarKeycloakJwtBean");
final KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
final X509EncodedKeySpec keySpec = getPublicKeySpec();
final RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
final RsaJsonWebKey webKey = new RsaJsonWebKey(publicKey);
jwtConsumer = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setSkipDefaultAudienceValidation()
.setVerificationKey(webKey.getKey()).build();
} catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException e) {
throw new SetupException(e);
final String[] parts = jwt.split("\\.");
final byte[] decoded = Base64.getUrlDecoder().decode(parts[1]);
final Map<String, Object> payload = OBJECT_MAPPER.readValue(decoded, Map.class);
return Optional.ofNullable(payload);
} catch (Exception e) {
return Optional.empty();
}
}

private boolean isReady() {
return jwtConsumer != null;
}

private X509EncodedKeySpec getPublicKeySpec() throws IOException {
final String encodedKey = strippedKey(quintoAndarKeycloakPublicKeyService.fetchKeycloakPublicKey());

final byte[] decodedKey = Base64.getDecoder().decode(encodedKey);

return new X509EncodedKeySpec(decodedKey);
}

private String strippedKey(final String publicKey) {
return publicKey.replaceAll("\\n", "");
}

// visible for testing
protected void setQuintoAndarKeycloakPublicKeyService(
final QuintoAndarKeycloakPublicKeyService quintoAndarKeycloakPublicKeyService
) {
this.quintoAndarKeycloakPublicKeyService = quintoAndarKeycloakPublicKeyService;
}

}

This file was deleted.

Loading