diff --git a/exec/pom.xml b/exec/pom.xml index cb1fca03..2297157e 100755 --- a/exec/pom.xml +++ b/exec/pom.xml @@ -21,7 +21,7 @@ com.google.fhir.gateway fhir-gateway - 0.1.35 + 0.1.36 exec diff --git a/plugins/pom.xml b/plugins/pom.xml index ae174164..09e33f72 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -23,7 +23,7 @@ implementations do not have to do this; they can redeclare those deps. --> com.google.fhir.gateway fhir-gateway - 0.1.35 + 0.1.36 plugins diff --git a/pom.xml b/pom.xml index da62eba3..9a437bfd 100755 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ com.google.fhir.gateway fhir-gateway - 0.1.35 + 0.1.36 pom FHIR Information Gateway diff --git a/server/pom.xml b/server/pom.xml index c52d3070..89535c8d 100755 --- a/server/pom.xml +++ b/server/pom.xml @@ -21,7 +21,7 @@ com.google.fhir.gateway fhir-gateway - 0.1.35 + 0.1.36 server diff --git a/server/src/main/java/com/google/fhir/gateway/BearerAuthorizationInterceptor.java b/server/src/main/java/com/google/fhir/gateway/BearerAuthorizationInterceptor.java index b049117a..6e503e3b 100755 --- a/server/src/main/java/com/google/fhir/gateway/BearerAuthorizationInterceptor.java +++ b/server/src/main/java/com/google/fhir/gateway/BearerAuthorizationInterceptor.java @@ -26,13 +26,7 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import com.auth0.jwt.JWT; -import com.auth0.jwt.JWTVerifier; -import com.auth0.jwt.algorithms.Algorithm; -import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; -import com.auth0.jwt.interfaces.Verification; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.fhir.gateway.interfaces.AccessChecker; @@ -40,27 +34,14 @@ import com.google.fhir.gateway.interfaces.AccessDecision; import com.google.fhir.gateway.interfaces.RequestDetailsReader; import com.google.fhir.gateway.interfaces.RequestMutation; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.Writer; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.EncodedKeySpec; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; -import java.util.Base64; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; -import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; @@ -72,7 +53,6 @@ public class BearerAuthorizationInterceptor { LoggerFactory.getLogger(BearerAuthorizationInterceptor.class); private static final String DEFAULT_CONTENT_TYPE = "application/json; charset=UTF-8"; - private static final String BEARER_PREFIX = "Bearer "; private static final String ACCEPT_ENCODING_HEADER = "Accept-Encoding"; @@ -84,25 +64,16 @@ public class BearerAuthorizationInterceptor { // For fetching CapabilityStatement: https://www.hl7.org/fhir/http.html#capabilities @VisibleForTesting static final String METADATA_PATH = "metadata"; - // TODO: Make this configurable or based on the given JWT; we should at least support some other - // RSA* and ES* algorithms (requires ECDSA512 JWT algorithm). - private static final String SIGN_ALGORITHM = "RS256"; - - private final String tokenIssuer; - private final Verification jwtVerifierConfig; - private final HttpUtil httpUtil; + private final TokenVerifier tokenVerifier; private final RestfulServer server; private final HttpFhirClient fhirClient; private final AccessCheckerFactory accessFactory; private final AllowedQueriesChecker allowedQueriesChecker; - private final String configJson; BearerAuthorizationInterceptor( HttpFhirClient fhirClient, - String tokenIssuer, - String wellKnownEndpoint, + TokenVerifier tokenVerifier, RestfulServer server, - HttpUtil httpUtil, AccessCheckerFactory accessFactory, AllowedQueriesChecker allowedQueriesChecker) throws IOException { @@ -110,113 +81,12 @@ public class BearerAuthorizationInterceptor { Preconditions.checkNotNull(server); this.server = server; this.fhirClient = fhirClient; - this.httpUtil = httpUtil; - this.tokenIssuer = tokenIssuer; + this.tokenVerifier = tokenVerifier; this.accessFactory = accessFactory; this.allowedQueriesChecker = allowedQueriesChecker; - RSAPublicKey issuerPublicKey = fetchAndDecodePublicKey(); - jwtVerifierConfig = JWT.require(Algorithm.RSA256(issuerPublicKey, null)); - this.configJson = httpUtil.fetchWellKnownConfig(tokenIssuer, wellKnownEndpoint); logger.info("Created proxy to the FHIR store " + this.fhirClient.getBaseUrl()); } - private RSAPublicKey fetchAndDecodePublicKey() throws IOException { - // Preconditions.checkState(SIGN_ALGORITHM.equals("ES512")); - Preconditions.checkState(SIGN_ALGORITHM.equals("RS256")); - // final String keyAlgorithm = "EC"; - final String keyAlgorithm = "RSA"; - try { - // TODO: Make sure this works for any issuer not just Keycloak; instead of this we should - // read the metadata and choose the right endpoint for the keys. - HttpResponse response = httpUtil.getResourceOrFail(new URI(tokenIssuer)); - JsonObject jsonObject = - JsonParser.parseString(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)) - .getAsJsonObject(); - String keyStr = jsonObject.get("public_key").getAsString(); - if (keyStr == null) { - ExceptionUtil.throwRuntimeExceptionAndLog( - logger, "Cannot find 'public_key' in issuer metadata."); - } - KeyFactory keyFactory = KeyFactory.getInstance(keyAlgorithm); - EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(keyStr)); - return (RSAPublicKey) keyFactory.generatePublic(keySpec); - } catch (URISyntaxException e) { - ExceptionUtil.throwRuntimeExceptionAndLog( - logger, "Error in token issuer URI " + tokenIssuer, e, AuthenticationException.class); - } catch (NoSuchAlgorithmException e) { - ExceptionUtil.throwRuntimeExceptionAndLog( - logger, "Invalid algorithm " + keyAlgorithm, e, AuthenticationException.class); - } catch (InvalidKeySpecException e) { - ExceptionUtil.throwRuntimeExceptionAndLog( - logger, "Invalid KeySpec: " + e.getMessage(), e, AuthenticationException.class); - } - // We should never get here, this is to keep the IDE happy! - return null; - } - - private JWTVerifier buildJwtVerifier(String issuer) { - - if (tokenIssuer.equals(issuer)) { - return jwtVerifierConfig.withIssuer(tokenIssuer).build(); - } else if (FhirProxyServer.isDevMode()) { - // If server is in DEV mode, set issuer to one from request - logger.warn("Server run in DEV mode. Setting issuer to issuer from request."); - return jwtVerifierConfig.withIssuer(issuer).build(); - } else { - ExceptionUtil.throwRuntimeExceptionAndLog( - logger, - String.format("The token issuer %s does not match the expected token issuer", issuer), - AuthenticationException.class); - return null; - } - } - - @VisibleForTesting - synchronized DecodedJWT decodeAndVerifyBearerToken(String authHeader) { - if (!authHeader.startsWith(BEARER_PREFIX)) { - ExceptionUtil.throwRuntimeExceptionAndLog( - logger, - "Authorization header is not a valid Bearer token!", - AuthenticationException.class); - } - String bearerToken = authHeader.substring(BEARER_PREFIX.length()); - DecodedJWT jwt = null; - try { - jwt = JWT.decode(bearerToken); - } catch (JWTDecodeException e) { - ExceptionUtil.throwRuntimeExceptionAndLog( - logger, "Failed to decode JWT: " + e.getMessage(), e, AuthenticationException.class); - } - String issuer = jwt.getIssuer(); - String algorithm = jwt.getAlgorithm(); - JWTVerifier jwtVerifier = buildJwtVerifier(issuer); - logger.info( - String.format( - "JWT issuer is %s, audience is %s, and algorithm is %s", - issuer, jwt.getAudience(), algorithm)); - - if (!SIGN_ALGORITHM.equals(algorithm)) { - ExceptionUtil.throwRuntimeExceptionAndLog( - logger, - String.format( - "Only %s signing algorithm is supported, got %s", SIGN_ALGORITHM, algorithm), - AuthenticationException.class); - } - DecodedJWT verifiedJwt = null; - try { - verifiedJwt = jwtVerifier.verify(jwt); - } catch (JWTVerificationException e) { - // Throwing an AuthenticationException instead since it is handled by HAPI and a 401 - // status code is returned in the response. - ExceptionUtil.throwRuntimeExceptionAndLog( - logger, - String.format("JWT verification failed with error: %s", e.getMessage()), - e, - AuthenticationException.class); - } - return verifiedJwt; - } - private AccessDecision checkAuthorization(RequestDetails requestDetails) { if (METADATA_PATH.equals(requestDetails.getRequestPath())) { // No further check is required; provide CapabilityStatement with security information. @@ -241,7 +111,7 @@ private AccessDecision checkAuthorization(RequestDetails requestDetails) { ExceptionUtil.throwRuntimeExceptionAndLog( logger, "No Authorization header provided!", AuthenticationException.class); } - DecodedJWT decodedJwt = decodeAndVerifyBearerToken(authHeader); + DecodedJWT decodedJwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); FhirContext fhirContext = server.getFhirContext(); PatientFinderImp patientFinder = PatientFinderImp.getInstance(fhirContext); AccessChecker accessChecker = @@ -283,7 +153,7 @@ public boolean authorizeRequest(RequestDetails requestDetails) { HttpResponse response = fhirClient.handleRequest(servletDetails); HttpUtil.validateResponseEntityExistsOrFail(response, requestPath); // TODO communicate post-processing failures to the client; see: - // https://github.com/google/fhir-gateway/issues/66 + // https://github.com/google/fhir-access-proxy/issues/66 String content = null; if (HttpUtil.isResponseValid(response)) { @@ -400,7 +270,7 @@ private void serveWellKnown(ServletRequestDetails request) { DEFAULT_CONTENT_TYPE, Constants.CHARSET_NAME_UTF8, false); - writer.write(configJson); + writer.write(tokenVerifier.getWellKnownConfig()); writer.close(); } catch (IOException e) { logger.error( diff --git a/server/src/main/java/com/google/fhir/gateway/TokenVerifier.java b/server/src/main/java/com/google/fhir/gateway/TokenVerifier.java new file mode 100644 index 00000000..5837174f --- /dev/null +++ b/server/src/main/java/com/google/fhir/gateway/TokenVerifier.java @@ -0,0 +1,201 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway; + +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.Verification; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import org.apache.http.HttpResponse; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TokenVerifier { + + private static final Logger logger = LoggerFactory.getLogger(TokenVerifier.class); + private static final String TOKEN_ISSUER_ENV = "TOKEN_ISSUER"; + private static final String WELL_KNOWN_ENDPOINT_ENV = "WELL_KNOWN_ENDPOINT"; + private static final String WELL_KNOWN_ENDPOINT_DEFAULT = ".well-known/openid-configuration"; + private static final String BEARER_PREFIX = "Bearer "; + + // TODO: Make this configurable or based on the given JWT; we should at least support some other + // RSA* and ES* algorithms (requires ECDSA512 JWT algorithm). + private static final String SIGN_ALGORITHM = "RS256"; + + private final String tokenIssuer; + // Note the Verification class is _not_ thread-safe but the JWTVerifier instances created by its + // `build()` are thread-safe and reusable. It is important to reuse those instances, otherwise + // we may end up with a memory leak; details: https://github.com/auth0/java-jwt/issues/592 + // Access to `jwtVerifierConfig` and `verifierForIssuer` should be non-concurrent. + private final Verification jwtVerifierConfig; + private final Map verifierForIssuer; + private final HttpUtil httpUtil; + private final String configJson; + + @VisibleForTesting + TokenVerifier(String tokenIssuer, String wellKnownEndpoint, HttpUtil httpUtil) + throws IOException { + this.tokenIssuer = tokenIssuer; + this.httpUtil = httpUtil; + RSAPublicKey issuerPublicKey = fetchAndDecodePublicKey(); + jwtVerifierConfig = JWT.require(Algorithm.RSA256(issuerPublicKey, null)); + this.configJson = httpUtil.fetchWellKnownConfig(tokenIssuer, wellKnownEndpoint); + this.verifierForIssuer = new HashMap<>(); + } + + public static TokenVerifier createFromEnvVars() throws IOException { + String tokenIssuer = System.getenv(TOKEN_ISSUER_ENV); + if (tokenIssuer == null) { + throw new IllegalArgumentException( + String.format("The environment variable %s is not set!", TOKEN_ISSUER_ENV)); + } + + String wellKnownEndpoint = System.getenv(WELL_KNOWN_ENDPOINT_ENV); + if (wellKnownEndpoint == null) { + wellKnownEndpoint = WELL_KNOWN_ENDPOINT_DEFAULT; + logger.info( + String.format( + "The environment variable %s is not set! Using default value of %s instead ", + WELL_KNOWN_ENDPOINT_ENV, WELL_KNOWN_ENDPOINT_DEFAULT)); + } + return new TokenVerifier(tokenIssuer, wellKnownEndpoint, new HttpUtil()); + } + + public String getWellKnownConfig() { + return configJson; + } + + private RSAPublicKey fetchAndDecodePublicKey() throws IOException { + // Preconditions.checkState(SIGN_ALGORITHM.equals("ES512")); + Preconditions.checkState(SIGN_ALGORITHM.equals("RS256")); + // final String keyAlgorithm = "EC"; + final String keyAlgorithm = "RSA"; + try { + // TODO: Make sure this works for any issuer not just Keycloak; instead of this we should + // read the metadata and choose the right endpoint for the keys. + HttpResponse response = httpUtil.getResourceOrFail(new URI(tokenIssuer)); + JsonObject jsonObject = + JsonParser.parseString(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)) + .getAsJsonObject(); + String keyStr = jsonObject.get("public_key").getAsString(); + if (keyStr == null) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, "Cannot find 'public_key' in issuer metadata."); + } + KeyFactory keyFactory = KeyFactory.getInstance(keyAlgorithm); + EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(keyStr)); + return (RSAPublicKey) keyFactory.generatePublic(keySpec); + } catch (URISyntaxException e) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, "Error in token issuer URI " + tokenIssuer, e, AuthenticationException.class); + } catch (NoSuchAlgorithmException e) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, "Invalid algorithm " + keyAlgorithm, e, AuthenticationException.class); + } catch (InvalidKeySpecException e) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, "Invalid KeySpec: " + e.getMessage(), e, AuthenticationException.class); + } + // We should never get here, this is to keep the IDE happy! + return null; + } + + private synchronized JWTVerifier getJwtVerifier(String issuer) { + if (!tokenIssuer.equals(issuer)) { + if (FhirProxyServer.isDevMode()) { + // If server is in DEV mode, set issuer to one from request + logger.warn("Server run in DEV mode. Setting issuer to issuer from request."); + } else { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, + String.format("The token issuer %s does not match the expected token issuer", issuer), + AuthenticationException.class); + return null; + } + } + if (!verifierForIssuer.containsKey(issuer)) { + verifierForIssuer.put(issuer, jwtVerifierConfig.withIssuer(issuer).build()); + } + return verifierForIssuer.get(issuer); + } + + @VisibleForTesting + DecodedJWT decodeAndVerifyBearerToken(String authHeader) { + if (!authHeader.startsWith(BEARER_PREFIX)) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, + "Authorization header is not a valid Bearer token!", + AuthenticationException.class); + } + String bearerToken = authHeader.substring(BEARER_PREFIX.length()); + DecodedJWT jwt = null; + try { + jwt = JWT.decode(bearerToken); + } catch (JWTDecodeException e) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, "Failed to decode JWT: " + e.getMessage(), e, AuthenticationException.class); + } + String issuer = jwt.getIssuer(); + String algorithm = jwt.getAlgorithm(); + JWTVerifier jwtVerifier = getJwtVerifier(issuer); + logger.info( + String.format( + "JWT issuer is %s, audience is %s, and algorithm is %s", + issuer, jwt.getAudience(), algorithm)); + + if (!SIGN_ALGORITHM.equals(algorithm)) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, + String.format( + "Only %s signing algorithm is supported, got %s", SIGN_ALGORITHM, algorithm), + AuthenticationException.class); + } + DecodedJWT verifiedJwt = null; + try { + verifiedJwt = jwtVerifier.verify(jwt); + } catch (JWTVerificationException e) { + // Throwing an AuthenticationException instead since it is handled by HAPI and a 401 + // status code is returned in the response. + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, + String.format("JWT verification failed with error: %s", e.getMessage()), + e, + AuthenticationException.class); + } + return verifiedJwt; + } +} diff --git a/server/src/test/java/com/google/fhir/gateway/BearerAuthorizationInterceptorTest.java b/server/src/test/java/com/google/fhir/gateway/BearerAuthorizationInterceptorTest.java index f6a4ea70..466ef318 100644 --- a/server/src/test/java/com/google/fhir/gateway/BearerAuthorizationInterceptorTest.java +++ b/server/src/test/java/com/google/fhir/gateway/BearerAuthorizationInterceptorTest.java @@ -29,14 +29,9 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.server.IRestfulResponse; import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRestfulResponse; -import com.auth0.jwt.JWT; -import com.auth0.jwt.JWTCreator; -import com.auth0.jwt.algorithms.Algorithm; -import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.io.Resources; @@ -49,16 +44,8 @@ import java.io.IOException; import java.io.StringWriter; import java.io.Writer; -import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -89,19 +76,15 @@ public class BearerAuthorizationInterceptorTest { private BearerAuthorizationInterceptor testInstance; - private static final String BASE_URL = "http://myprxy/fhir"; + private static final String BASE_URL = "http://myproxy/fhir"; private static final String FHIR_STORE = "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/" + "synthea-sample-data/fhirStores/gcs-data/fhir"; - private static final String TOKEN_ISSUER = "https://token.issuer"; - - private KeyPair keyPair; - @Mock private HttpFhirClient fhirClientMock; @Mock private RestfulServer serverMock; - @Mock private HttpUtil httpUtilMock; + @Mock private TokenVerifier tokenVerifierMock; @Mock private ServletRequestDetails requestMock; @@ -110,29 +93,12 @@ public class BearerAuthorizationInterceptorTest { private final Writer writerStub = new StringWriter(); - private String generateKeyPairAndEncode() { - try { - KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); - generator.initialize(1024); - keyPair = generator.generateKeyPair(); - Key publicKey = keyPair.getPublic(); - Preconditions.checkState("X.509".equals(publicKey.getFormat())); - return Base64.getEncoder().encodeToString(publicKey.getEncoded()); - } catch (GeneralSecurityException e) { - logger.error("error in generating keys", e); - Preconditions.checkState(false); // We should never get here! - } - return null; - } - private BearerAuthorizationInterceptor createTestInstance( boolean isAccessGranted, String allowedQueriesConfig) throws IOException { return new BearerAuthorizationInterceptor( fhirClientMock, - TOKEN_ISSUER, - "test", + tokenVerifierMock, serverMock, - httpUtilMock, (jwt, httpFhirClient, fhirContext, patientFinder) -> new AccessChecker() { @Override @@ -145,75 +111,21 @@ public AccessDecision checkAccess(RequestDetailsReader requestDetails) { @Before public void setUp() throws IOException { - String publicKeyBase64 = generateKeyPairAndEncode(); - HttpResponse responseMock = Mockito.mock(HttpResponse.class, Answers.RETURNS_DEEP_STUBS); when(serverMock.getServerBaseForRequest(any(ServletRequestDetails.class))).thenReturn(BASE_URL); when(serverMock.getFhirContext()).thenReturn(fhirContext); - when(httpUtilMock.getResourceOrFail(any(URI.class))).thenReturn(responseMock); - TestUtil.setUpFhirResponseMock( - responseMock, String.format("{public_key: '%s'}", publicKeyBase64)); - URL idpUrl = Resources.getResource("idp_keycloak_config.json"); - String testIdpConfig = Resources.toString(idpUrl, StandardCharsets.UTF_8); - when(httpUtilMock.fetchWellKnownConfig(anyString(), anyString())).thenReturn(testIdpConfig); when(fhirClientMock.handleRequest(requestMock)).thenReturn(fhirResponseMock); when(fhirClientMock.getBaseUrl()).thenReturn(FHIR_STORE); testInstance = createTestInstance(true, null); } - private String signJwt(JWTCreator.Builder jwtBuilder) { - Algorithm algorithm = - Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), (RSAPrivateKey) keyPair.getPrivate()); - String token = jwtBuilder.sign(algorithm); - logger.debug(String.format(" The generated JWT is: %s", token)); - return token; - } - - @Test - public void decodeAndVerifyBearerTokenTest() { - JWTCreator.Builder jwtBuilder = JWT.create().withIssuer(TOKEN_ISSUER); - testInstance.decodeAndVerifyBearerToken("Bearer " + signJwt(jwtBuilder)); - } - - @Test(expected = AuthenticationException.class) - public void decodeAndVerifyBearerTokenWrongIssuer() { - JWTCreator.Builder jwtBuilder = JWT.create().withIssuer(TOKEN_ISSUER + "WRONG"); - testInstance.decodeAndVerifyBearerToken("Bearer " + signJwt(jwtBuilder)); - } - - @Test(expected = AuthenticationException.class) - public void decodeAndVerifyBearerTokenBadSignature() { - // We overwrite the original `keyPair` hence the signature won't match the original public key. - generateKeyPairAndEncode(); - JWTCreator.Builder jwtBuilder = JWT.create().withIssuer(TOKEN_ISSUER); - testInstance.decodeAndVerifyBearerToken("Bearer " + signJwt(jwtBuilder)); + private void setupBearerAndFhirResponse(String fhirStoreResponse) throws IOException { + setupFhirResponse(fhirStoreResponse, true); } - @Test(expected = AuthenticationException.class) - public void decodeAndVerifyBearerTokenNoBearer() { - JWTCreator.Builder jwtBuilder = JWT.create().withIssuer(TOKEN_ISSUER); - testInstance.decodeAndVerifyBearerToken(signJwt(jwtBuilder)); - } - - @Test(expected = AuthenticationException.class) - public void decodeAndVerifyBearerTokenMalformedBearer() { - JWTCreator.Builder jwtBuilder = JWT.create().withIssuer(TOKEN_ISSUER); - testInstance.decodeAndVerifyBearerToken("BearerTTT " + signJwt(jwtBuilder)); - } - - @Test(expected = AuthenticationException.class) - public void decodeAndVerifyBearerTokenMalformedToken() { - JWTCreator.Builder jwtBuilder = JWT.create().withIssuer(TOKEN_ISSUER); - testInstance.decodeAndVerifyBearerToken("Bearer TTT"); - } - - private void authorizeRequestCommonSetUp(String fhirStoreResponse) throws IOException { - JWTCreator.Builder jwtBuilder = JWT.create().withIssuer(TOKEN_ISSUER); - String jwt = signJwt(jwtBuilder); - when(requestMock.getHeader("Authorization")).thenReturn("Bearer " + jwt); - setupFhirResponse(fhirStoreResponse); - } - - private void setupFhirResponse(String fhirStoreResponse) throws IOException { + private void setupFhirResponse(String fhirStoreResponse, boolean addBearer) throws IOException { + if (addBearer) { + when(requestMock.getHeader("Authorization")).thenReturn("Bearer ANYTHING"); + } IRestfulResponse proxyResponseMock = Mockito.mock(IRestfulResponse.class); when(requestMock.getResponse()).thenReturn(proxyResponseMock); when(proxyResponseMock.getResponseWriter( @@ -226,7 +138,7 @@ private void setupFhirResponse(String fhirStoreResponse) throws IOException { public void authorizeRequestPatient() throws IOException { URL patientUrl = Resources.getResource("test_patient.json"); String testPatientJson = Resources.toString(patientUrl, StandardCharsets.UTF_8); - authorizeRequestCommonSetUp(testPatientJson); + setupBearerAndFhirResponse(testPatientJson); testInstance.authorizeRequest(requestMock); assertThat(testPatientJson, equalTo(writerStub.toString())); } @@ -235,7 +147,7 @@ public void authorizeRequestPatient() throws IOException { public void authorizeRequestList() throws IOException { URL patientUrl = Resources.getResource("patient-list-example.json"); String testListJson = Resources.toString(patientUrl, StandardCharsets.UTF_8); - authorizeRequestCommonSetUp(testListJson); + setupBearerAndFhirResponse(testListJson); testInstance.authorizeRequest(requestMock); assertThat(testListJson, equalTo(writerStub.toString())); } @@ -244,7 +156,7 @@ public void authorizeRequestList() throws IOException { public void authorizeRequestTestReplaceUrl() throws IOException { URL searchUrl = Resources.getResource("patient_id_search.json"); String testPatientIdSearch = Resources.toString(searchUrl, StandardCharsets.UTF_8); - authorizeRequestCommonSetUp(testPatientIdSearch); + setupBearerAndFhirResponse(testPatientIdSearch); testInstance.authorizeRequest(requestMock); String replaced = testPatientIdSearch.replaceAll(FHIR_STORE, BASE_URL); assertThat(replaced, equalTo(writerStub.toString())); @@ -254,7 +166,7 @@ public void authorizeRequestTestReplaceUrl() throws IOException { public void authorizeRequestTestResourceErrorResponse() throws IOException { URL errorUrl = Resources.getResource("error_operation_outcome.json"); String errorResponse = Resources.toString(errorUrl, StandardCharsets.UTF_8); - authorizeRequestCommonSetUp(errorResponse); + setupBearerAndFhirResponse(errorResponse); when(fhirResponseMock.getStatusLine().getStatusCode()) .thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); testInstance.authorizeRequest(requestMock); @@ -277,6 +189,10 @@ public void authorizeRequestWellKnown() throws IOException { HttpServletRequest servletRequestMock = Mockito.mock(HttpServletRequest.class); when(requestMock.getServletRequest()).thenReturn(servletRequestMock); when(servletRequestMock.getProtocol()).thenReturn("HTTP/1.1"); + URL idpUrl = Resources.getResource("idp_keycloak_config.json"); + String testIdpConfig = Resources.toString(idpUrl, StandardCharsets.UTF_8); + when(tokenVerifierMock.getWellKnownConfig()).thenReturn(testIdpConfig); + testInstance.authorizeRequest(requestMock); Gson gson = new Gson(); Map jsonMap = Maps.newHashMap(); @@ -321,7 +237,7 @@ public void authorizeRequestMetadata() throws IOException { noAuthRequestSetup(BearerAuthorizationInterceptor.METADATA_PATH); URL capabilityUrl = Resources.getResource("capability.json"); String capabilityJson = Resources.toString(capabilityUrl, StandardCharsets.UTF_8); - authorizeRequestCommonSetUp(capabilityJson); + setupBearerAndFhirResponse(capabilityJson); testInstance.authorizeRequest(requestMock); IParser parser = fhirContext.newJsonParser(); IBaseResource resource = parser.parseResource(writerStub.toString()); @@ -340,7 +256,7 @@ public void authorizeAllowedUnauthenticatedRequest() throws IOException { createTestInstance( false, Resources.getResource("allowed_unauthenticated_queries.json").getPath()); String responseJson = "{\"resourceType\": \"Bundle\"}"; - setupFhirResponse(responseJson); + setupFhirResponse(responseJson, false); when(requestMock.getRequestPath()).thenReturn("Composition"); testInstance.authorizeRequest(requestMock); @@ -354,7 +270,7 @@ public void deniedRequest() throws IOException { testInstance = createTestInstance( false, Resources.getResource("allowed_unauthenticated_queries.json").getPath()); - authorizeRequestCommonSetUp("never returned response"); + setupBearerAndFhirResponse("never returned response"); when(requestMock.getRequestPath()).thenReturn("Patient"); testInstance.authorizeRequest(requestMock); @@ -375,13 +291,10 @@ public boolean canAccess() { return true; } - public void preProcess(ServletRequestDetails servletRequestDetails) {} - public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsReader) { return RequestMutation.builder().queryParams(paramMutations).build(); } - @Override public String postProcess( RequestDetailsReader requestDetailsReader, HttpResponse response) throws IOException { return null; @@ -403,8 +316,7 @@ public String postProcess( public void shouldSendGzippedResponseWhenRequested() throws IOException { testInstance = createTestInstance(true, null); String responseJson = "{\"resourceType\": \"Bundle\"}"; - JWTCreator.Builder jwtBuilder = JWT.create().withIssuer(TOKEN_ISSUER); - when(requestMock.getHeader("Authorization")).thenReturn("Bearer " + signJwt(jwtBuilder)); + when(requestMock.getHeader("Authorization")).thenReturn("Bearer ANYTHING"); when(requestMock.getHeader("Accept-Encoding".toLowerCase())).thenReturn("gzip"); // requestMock.getResponse() {@link ServletRequestDetails#getResponse()} is an abstraction HAPI