From 4d5cbd5613c96d570dfcad9a2db78c62eaf91125 Mon Sep 17 00:00:00 2001 From: G8XSU <3442979+G8XSU@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:23:02 -0700 Subject: [PATCH] Add JWTAuthorizer Implementation. --- java/app/build.gradle | 2 + .../main/java/org/vss/auth/JwtAuthorizer.java | 80 +++++++++++++++++++ .../java/org/vss/auth/JwtAuthorizerTest.java | 75 +++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 java/app/src/main/java/org/vss/auth/JwtAuthorizer.java create mode 100644 java/app/src/test/java/org/vss/auth/JwtAuthorizerTest.java diff --git a/java/app/build.gradle b/java/app/build.gradle index 9980e95..4b91435 100644 --- a/java/app/build.gradle +++ b/java/app/build.gradle @@ -59,6 +59,8 @@ dependencies { implementation('org.glassfish.jersey.inject:jersey-hk2:3.1.0') implementation "org.glassfish.hk2:guice-bridge:3.0.3" + implementation "com.auth0:java-jwt:4.2.0" + compileOnly 'org.projectlombok:lombok:1.18.24' annotationProcessor 'org.projectlombok:lombok:1.18.24' diff --git a/java/app/src/main/java/org/vss/auth/JwtAuthorizer.java b/java/app/src/main/java/org/vss/auth/JwtAuthorizer.java new file mode 100644 index 0000000..9175c30 --- /dev/null +++ b/java/app/src/main/java/org/vss/auth/JwtAuthorizer.java @@ -0,0 +1,80 @@ +package org.vss.auth; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; +import jakarta.ws.rs.core.HttpHeaders; +import org.vss.exception.AuthException; + +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +// A JWT (https://datatracker.ietf.org/doc/html/rfc7519) based authorizer, +public class JwtAuthorizer implements Authorizer { + + private final PublicKey publicKey; + private final JWTVerifier verifier; + + private static final String BEARER_PREFIX = "Bearer "; + private static final int MAX_USER_TOKEN_LENGTH = 120; + + // `pemFormatRSAPublicKey` is RSA public key used by JWT Auth server for creating signed JWT tokens. + // Refer to OpenSSL(https://docs.openssl.org/1.1.1/man1/rsa/) docs for generating valid key pairs. + // Example: + // * To generate private key, run : `openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048` + // * To generate public key, run: `openssl rsa -pubout -in private_key.pem -out public_key.pem` + public JwtAuthorizer(String pemFormatRSAPublicKey) throws Exception { + this.publicKey = loadPublicKey(pemFormatRSAPublicKey); + + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) publicKey, null); + this.verifier = JWT.require(algorithm).build(); + } + + @Override + public AuthResponse verify(HttpHeaders headers) throws AuthException { + + try { + String authorizationHeader = headers.getHeaderString(HttpHeaders.AUTHORIZATION); + if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_PREFIX)) { + throw new AuthException("Missing or invalid Authorization header."); + } + + // Extract token by excluding BEARER_PREFIX. + String token = authorizationHeader.substring(BEARER_PREFIX.length()); + + DecodedJWT jwt = verifier.verify(token); + + // Extract the user identity from the token. + String userToken = jwt.getSubject(); + + if (userToken == null || userToken.isBlank()) { + throw new AuthException("Invalid JWT token."); + } else if (userToken.length() > MAX_USER_TOKEN_LENGTH) { + throw new AuthException("UserToken is too long"); + } + + return new AuthResponse(userToken); + + } catch (JWTVerificationException e) { + throw new AuthException("Invalid JWT token."); + } + } + + private PublicKey loadPublicKey(String pemFormatRSAPublicKey) throws Exception { + String key = pemFormatRSAPublicKey + .replaceAll("\\n", "") + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", ""); + + byte[] keyBytes = Base64.getDecoder().decode(key); + + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(spec); + } +} diff --git a/java/app/src/test/java/org/vss/auth/JwtAuthorizerTest.java b/java/app/src/test/java/org/vss/auth/JwtAuthorizerTest.java new file mode 100644 index 0000000..f9258f0 --- /dev/null +++ b/java/app/src/test/java/org/vss/auth/JwtAuthorizerTest.java @@ -0,0 +1,75 @@ +package org.vss.auth; + +import jakarta.ws.rs.core.HttpHeaders; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.vss.exception.AuthException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class JwtAuthorizerTest { + + private JwtAuthorizer jwtAuthorizer; + private HttpHeaders headers; + + private static final String PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAysGpKU+I9i9b+QZSANu/\n" + + "ExaA6w4qiQdFZaXeReiz49r1oDfABwKIFW9gK/kNnrnL9H8P+pYfj7jqUJ/glmgq\n" + + "MsvBshbbD2FhxytSS0mhsbh6QxUhlanymPcSUUyKBD6v7W0CGUhS5luHlsCFn4ys\n" + + "lFk4pavcBtGap0DTUc8yz0j/xnmSQbdjWgm0awbHN48uItRO3UhLAOetG+BzlWCR\n" + + "8YsTa5piV8KgJpG/rwYTGXuu3lcCmnWwjmbeDq1zFFrCDDVkaIHkGJgRuFIDPXaH\n" + + "yUw5H2HvKlP94ySbvTDLXWZj6TyzHEHDbstqs4DgvurB/bIhi/dQ7zK3EIXL8KRB\n" + + "hwIDAQAB\n" + + "-----END PUBLIC KEY-----"; + + private static final String VALID_AUTH_HEADER = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9." + + "eyJzdWIiOiJ2YWxpZF91c2VyX2lkIiwiaWF0IjoxNzI5NjM0MjYwLCJuYmYiOjE3Mjk2MzQyNjAsImV4cCI6MzA1" + + "NTY4OTE1OTQwMzc0NTl9.xBL5BYiv8B-ZN1bCuljuJ7dZeOPocVPPVwkeK_GH4lD5iQqD08zi93WuXw1c6NWWCK4" + + "jn4ZssYrzSLLL5q3tAYbLKuhQ2-2A-e1HTasfvSnx_jCBUNApbIv3rM19M3rhRVRSxT2s2jI7dJFlM6E_bGMfj9w" + + "uoZiT_amjIIPQJiRkDKcO2sXnD6eU_yx8EIhH_PemSX3kp9Sx9eTYqGbyCtLrs9jK7nr6GQ_1jc6ie03Uh2dsIzW" + + "sZqGHh2n_WmdyURWEfwsMYFpepRLzm77dP9q78RgA8eDLZSLNW9ssJMYWY9DRkOZBFFuf4uy-uqC9MWS64DkJSAo" + + "nH8Zof_tUiQ"; + + private static final String VALID_USER_ID = "valid_user_id"; + + @BeforeEach + public void setUp() throws Exception { + jwtAuthorizer = new JwtAuthorizer(PUBLIC_KEY); + headers = mock(HttpHeaders.class); + } + + @Test + public void testValidJwtToken() { + when(headers.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn(VALID_AUTH_HEADER); + + AuthResponse authResponse = jwtAuthorizer.verify(headers); + + assertNotNull(authResponse); + + assertEquals(VALID_USER_ID, authResponse.getUserToken()); + } + + @Test + public void testMissingAuthorizationHeader() { + when(headers.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn(null); + + assertThrows(AuthException.class, () -> jwtAuthorizer.verify(headers)); + } + + @Test + public void testInvalidAuthorizationHeader() { + when(headers.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn("InvalidHeader"); + + assertThrows(AuthException.class, () -> jwtAuthorizer.verify(headers)); + } + + @Test + public void testInvalidJwtToken() { + String invalidJwt = "Bearer invalid.jwt.token"; + when(headers.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn(invalidJwt); + + assertThrows(AuthException.class, () -> jwtAuthorizer.verify(headers)); + } +}