diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 39999a42386..75a0bdf7afa 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -46,8 +46,15 @@ 4.2.0 - + + + org.mitre + openid-connect-client + ${mitreid-connect-version} + true + + org.thymeleaf thymeleaf @@ -113,13 +120,19 @@ spring-beans ${spring_version} true + + + commons-logging + commons-logging + + javax.servlet javax.servlet-api - 3.0.1 + 3.1.0 provided @@ -166,8 +179,14 @@ 9.1.1.v20140108 test + + org.eclipse.jetty + jetty-webapp + 9.1.1.v20140108 + test + - + org.mockito mockito-all @@ -194,20 +213,17 @@ jdk15-sources test - - - - commons-httpclient - commons-httpclient - 3.1 - test - - directory-naming naming-java 0.8 test + + + commons-logging + commons-logging + + org.hamcrest @@ -215,6 +231,23 @@ ${hamcrest_version} test + + org.springframework.security + spring-security-web + ${spring_security_version} + test + + + org.springframework.security + spring-security-config + ${spring_security_version} + test + + + org.springframework.security + spring-security-jwt + 1.0.2.RELEASE + diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ISecurityManager.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ISecurityManager.java index 56f6e5f4d25..88d5c4fd939 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ISecurityManager.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/ISecurityManager.java @@ -25,8 +25,8 @@ import javax.servlet.http.HttpServletRequest; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; /** - * Created by dsotnikov on 3/7/2014. + * @deprecated Use {@link ca.uhn.fhir.rest.server.security.ISecurityManager} instead */ public interface ISecurityManager { - public void authenticate(HttpServletRequest request) throws AuthenticationException; + public void authenticate(HttpServletRequest request) throws AuthenticationException; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 4f2c9b4b14b..be47c36be2b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.rest.server; * #L% */ -import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.io.IOException; import java.io.OutputStreamWriter; @@ -82,7 +82,6 @@ import ca.uhn.fhir.util.VersionUtil; public class RestfulServer extends HttpServlet { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class); private static final long serialVersionUID = 1L; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/AuthenticationException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/AuthenticationException.java index a212592204e..c863964832e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/AuthenticationException.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/exceptions/AuthenticationException.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.rest.server.exceptions; +import java.text.ParseException; + import ca.uhn.fhir.rest.server.Constants; /* @@ -22,23 +24,25 @@ import ca.uhn.fhir.rest.server.Constants; * #L% */ - /** - * Represents an HTTP 401 Client Unauthorized response, which means that - * the client needs to provide credentials, or has provided invalid credentials. + * Represents an HTTP 401 Client Unauthorized response, which means that the client needs to provide credentials, or has provided invalid credentials. */ public class AuthenticationException extends BaseServerResponseException { - public static final int STATUS_CODE = Constants.STATUS_HTTP_401_CLIENT_UNAUTHORIZED; - + public static final int STATUS_CODE = Constants.STATUS_HTTP_401_CLIENT_UNAUTHORIZED; + private static final long serialVersionUID = 1L; - public AuthenticationException() { - super(STATUS_CODE, "Client unauthorized"); - } + public AuthenticationException() { + super(STATUS_CODE, "Client unauthorized"); + } - public AuthenticationException(String theMessage) { - super(STATUS_CODE, theMessage); - } + public AuthenticationException(String theMessage) { + super(STATUS_CODE, theMessage); + } + + public AuthenticationException(String theMessage, ParseException theCause) { + super(STATUS_CODE, theMessage, theCause); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/security/IResourceSecurity.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/security/IResourceSecurity.java new file mode 100644 index 00000000000..f1c01a69626 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/security/IResourceSecurity.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.rest.server.security; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * 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. + * #L% + */ + +import javax.servlet.http.HttpServletRequest; + +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; + +/** + * Implementations of this interface provide authorization to incoming service calls + * + */ +public interface IResourceSecurity { + + public ISecurityOutcome authenticate(HttpServletRequest request) throws AuthenticationException; + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/security/ISecurityOutcome.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/security/ISecurityOutcome.java new file mode 100644 index 00000000000..64e223cf15c --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/security/ISecurityOutcome.java @@ -0,0 +1,5 @@ +package ca.uhn.fhir.rest.server.security; + +public interface ISecurityOutcome { + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/security/OpenIdConnectBearerTokenSecurityManager.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/security/OpenIdConnectBearerTokenSecurityManager.java new file mode 100644 index 00000000000..9c1c971e56f --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/security/OpenIdConnectBearerTokenSecurityManager.java @@ -0,0 +1,162 @@ +package ca.uhn.fhir.rest.server.security; + +import java.text.ParseException; +import java.util.Date; + +import javax.servlet.http.HttpServletRequest; + +import org.mitre.jwt.signer.service.JwtSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.JWKSetCacheService; +import org.mitre.jwt.signer.service.impl.SymmetricCacheService; +import org.mitre.oauth2.model.RegisteredClient; +import org.mitre.openid.connect.client.service.ClientConfigurationService; +import org.mitre.openid.connect.client.service.ServerConfigurationService; +import org.mitre.openid.connect.config.ServerConfiguration; +import org.springframework.beans.factory.annotation.Autowired; + +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +public class OpenIdConnectBearerTokenSecurityManager implements IResourceSecurity { + + private static final String BEARER_PREFIX = "Bearer "; + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OpenIdConnectBearerTokenSecurityManager.class); + @Autowired + private ClientConfigurationService myClientConfigurationService; + + @Autowired + private ServerConfigurationService myServerConfigurationService; + + private int myTimeSkewAllowance = 300; + + private SymmetricCacheService symmetricCacheService; + private JWKSetCacheService validationServices; + + public OpenIdConnectBearerTokenSecurityManager() { + symmetricCacheService = new SymmetricCacheService(); + validationServices = new JWKSetCacheService(); + } + + @Override + public ISecurityOutcome authenticate(HttpServletRequest theRequest) throws AuthenticationException { + String token = theRequest.getHeader(Constants.HEADER_AUTHORIZATION); + if (token == null) { + throw new AuthenticationException("Not authorized (no authorization header found in request)"); + } + if (!token.startsWith(BEARER_PREFIX)) { + throw new AuthenticationException("Not authorized (authorization header does not contain a bearer token)"); + } + + token = token.substring(BEARER_PREFIX.length()); + + SignedJWT idToken; + try { + idToken = SignedJWT.parse(token); + } catch (ParseException e) { + throw new AuthenticationException("Not authorized (bearer token could not be validated)", e); + } + + // validate our ID Token over a number of tests + ReadOnlyJWTClaimsSet idClaims; + try { + idClaims = idToken.getJWTClaimsSet(); + } catch (ParseException e) { + throw new AuthenticationException("Not authorized (bearer token could not be validated)", e); + } + + String issuer = idClaims.getIssuer(); + + ServerConfiguration serverConfig = myServerConfigurationService.getServerConfiguration(issuer); + if (serverConfig == null) { + ourLog.error("No server configuration found for issuer: " + issuer); + throw new AuthenticationException("Not authorized (no server configuration found for issuer " + issuer + ")"); + } + + RegisteredClient clientConfig = myClientConfigurationService.getClientConfiguration(serverConfig); + if (clientConfig == null) { + ourLog.error("No client configuration found for issuer: " + issuer); + throw new AuthenticationException("Not authorized (no client configuration found for issuer " + issuer + ")"); + } + + // check the signature + JwtSigningAndValidationService jwtValidator = null; + + JWSAlgorithm alg = idToken.getHeader().getAlgorithm(); + if (alg.equals(JWSAlgorithm.HS256) || alg.equals(JWSAlgorithm.HS384) || alg.equals(JWSAlgorithm.HS512)) { + + // generate one based on client secret + jwtValidator = symmetricCacheService.getSymmetricValidtor(clientConfig.getClient()); + } else { + // otherwise load from the server's public key + jwtValidator = validationServices.getValidator(serverConfig.getJwksUri()); + } + + if (jwtValidator != null) { + if (!jwtValidator.validateSignature(idToken)) { + throw new AuthenticationException("Not authorized (signature validation failed)"); + } + } else { + ourLog.error("No validation service found. Skipping signature validation"); + throw new AuthenticationException("Not authorized (can't determine signature validator)"); + } + + // check expiration + if (idClaims.getExpirationTime() == null) { + throw new AuthenticationException("Id Token does not have required expiration claim"); + } else { + // it's not null, see if it's expired + Date now = new Date(System.currentTimeMillis() - (myTimeSkewAllowance * 1000)); + if (now.after(idClaims.getExpirationTime())) { + throw new AuthenticationException("Id Token is expired: " + idClaims.getExpirationTime()); + } + } + + // check not before + if (idClaims.getNotBeforeTime() != null) { + Date now = new Date(System.currentTimeMillis() + (myTimeSkewAllowance * 1000)); + if (now.before(idClaims.getNotBeforeTime())) { + throw new AuthenticationException("Id Token not valid untill: " + idClaims.getNotBeforeTime()); + } + } + + // check issued at + if (idClaims.getIssueTime() == null) { + throw new AuthenticationException("Id Token does not have required issued-at claim"); + } else { + // since it's not null, see if it was issued in the future + Date now = new Date(System.currentTimeMillis() + (myTimeSkewAllowance * 1000)); + if (now.before(idClaims.getIssueTime())) { + throw new AuthenticationException("Id Token was issued in the future: " + idClaims.getIssueTime()); + } + } + + return new ISecurityOutcome() { + }; + } + + public int getTimeSkewAllowance() { + return myTimeSkewAllowance; + } + + public void setClientConfigurationService(ClientConfigurationService theClientConfigurationService) { + myClientConfigurationService = theClientConfigurationService; + } + + public void setServerConfigurationService(ServerConfigurationService theServerConfigurationService) { + myServerConfigurationService = theServerConfigurationService; + } + + public void setTimeSkewAllowance(int theTimeSkewAllowance) { + myTimeSkewAllowance = theTimeSkewAllowance; + } + + public void setValidationServices(JWKSetCacheService theValidationServices) { + validationServices = theValidationServices; + } + +} diff --git a/hapi-fhir-base/src/site/example/java/example/ClientExamples.java b/hapi-fhir-base/src/site/example/java/example/ClientExamples.java index 95434151eac..24ce61cc761 100644 --- a/hapi-fhir-base/src/site/example/java/example/ClientExamples.java +++ b/hapi-fhir-base/src/site/example/java/example/ClientExamples.java @@ -65,7 +65,7 @@ IPatientClient annotationClient = ctx.newRestfulClient(IPatientClient.class, "ht annotationClient.registerInterceptor(loggingInterceptor); IGenericClient genericClient = ctx.newRestfulGenericClient("http://localhost:9999/fhir"); -annotationClient.registerInterceptor(loggingInterceptor); +genericClient.registerInterceptor(loggingInterceptor); //END SNIPPET: logging } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java index 939e6bf3e0e..224ab6b3dbf 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java @@ -71,8 +71,8 @@ import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.annotation.VersionIdParam; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.param.CodingListParam; -import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.provider.ServerProfileProvider; diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerSecurityTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerSecurityTest.java new file mode 100644 index 00000000000..c7fd5d33e7d --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerSecurityTest.java @@ -0,0 +1,183 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceFactory; +import org.eclipse.jetty.webapp.WebAppContext; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.model.dstu.composite.HumanNameDt; +import ca.uhn.fhir.model.dstu.composite.IdentifierDt; +import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.dstu.valueset.IdentifierUseEnum; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.UriDt; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.server.provider.ServerProfileProvider; +import ca.uhn.fhir.testutil.RandomServerPortProvider; + +/** + * Created by dsotnikov on 2/25/2014. + */ +public class ServerSecurityTest { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerSecurityTest.class); + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx; + + @BeforeClass + public static void beforeClass() throws Exception { + ourCtx = new FhirContext(Patient.class); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + } + + @Test + public void testContextWithSpace() throws Exception { + if (true) return; + + int port = RandomServerPortProvider.findFreePort(); + Server server = new Server(port); + + RestfulServer restServer = new RestfulServer(); + restServer.setFhirContext(ourCtx); + restServer.setResourceProviders(new DummyPatientResourceProvider()); + + // ServletHandler proxyHandler = new ServletHandler(); + ServletHolder servletHolder = new ServletHolder(restServer); + + WebAppContext wac = new WebAppContext(); + wac.setWar("src/test/resources/securitytest_war"); + +// wac.addServlet(servletHolder, "/fhir/*"); + + + ServletContextHandler ch = new ServletContextHandler(); + ch.setContextPath("/"); + ch.addServlet(servletHolder, "/fhir/*"); +// ch.add +// ch.addFilter(org.springframework.web.filter.DelegatingFilterProxy.class, "/*", null); + + ContextHandlerCollection contexts = new ContextHandlerCollection(); + server.setHandler(wac); + +// server.setHandler(ch); + server.start(); + try { + + String baseUri = "http://localhost:" + port + "/fhir"; + String uri = baseUri + "/Patient?identifier=urn:hapitest:mrns%7C00001"; + HttpGet httpGet = new HttpGet(uri); + httpGet.addHeader("Authorization", "Basic eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MDY4MzU3MjMsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvdWhudmVzYjAxZC51aG4ub24uY2E6MjUxODBcL3Vobi1vcGVuaWQtY29ubmVjdC1zZXJ2ZXJcLyIsImp0aSI6IjEwZTYwYWY1LTEyZmUtNDFhYy05MWQyLTliNWY0NzBiNGM5OSIsImlhdCI6MTQwNjgzMjEyM30.LaCAOmoM0ikkaalKBfccU_YE8NBXvT5M7L9ITfR86vEp5-3_kVeSrYHI4CjVUSDafyVwQF4x5eJVSZdtQxtEk9F1L6be_JQI84crqff53EKA10z7m9Lkc5jJFs6sd9cnlayhlBgxL7BNdSf0Bjs7aS6YMEcn9IY3RNSeQj7kjhs"); + HttpResponse status = ourClient.execute(httpGet); + + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(200, status.getStatusLine().getStatusCode()); + Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent); + + assertEquals(1, bundle.getEntries().size()); + + } finally { + server.stop(); + } + + } + + + /** + * Created by dsotnikov on 2/25/2014. + */ + public static class DummyPatientResourceProvider implements IResourceProvider { + + public Map getIdToPatient() { + Map idToPatient = new HashMap(); + { + Patient patient = new Patient(); + patient.setId("1"); + patient.addIdentifier(); + patient.getIdentifier().get(0).setUse(IdentifierUseEnum.OFFICIAL); + patient.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns")); + patient.getIdentifier().get(0).setValue("00001"); + patient.addName(); + patient.getName().get(0).addFamily("Test"); + patient.getName().get(0).addGiven("PatientOne"); + patient.getGender().setText("M"); + idToPatient.put("1", patient); + } + { + Patient patient = new Patient(); + patient.setId("2"); + patient.getIdentifier().add(new IdentifierDt()); + patient.getIdentifier().get(0).setUse(IdentifierUseEnum.OFFICIAL); + patient.getIdentifier().get(0).setSystem(new UriDt("urn:hapitest:mrns")); + patient.getIdentifier().get(0).setValue("00002"); + patient.getName().add(new HumanNameDt()); + patient.getName().get(0).addFamily("Test"); + patient.getName().get(0).addGiven("PatientTwo"); + patient.getGender().setText("F"); + idToPatient.put("2", patient); + } + return idToPatient; + } + + @Search() + public Patient getPatient(@RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) { + for (Patient next : getIdToPatient().values()) { + for (IdentifierDt nextId : next.getIdentifier()) { + if (nextId.matchesSystemAndValue(theIdentifier)) { + return next; + } + } + } + return null; + } + + /** + * Retrieve the resource by its identifier + * + * @param theId + * The resource identity + * @return The resource + */ + @Read() + public Patient getResourceById(@IdParam IdDt theId) { + return getIdToPatient().get(theId.getValue()); + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + } + +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerSecurityTestRestfulServlet.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerSecurityTestRestfulServlet.java new file mode 100644 index 00000000000..2f9d5af9f4f --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerSecurityTestRestfulServlet.java @@ -0,0 +1,15 @@ +package ca.uhn.fhir.rest.server; + +import javax.servlet.ServletException; + +import ca.uhn.fhir.rest.server.ServerSecurityTest.DummyPatientResourceProvider; + +public class ServerSecurityTestRestfulServlet extends RestfulServer +{ + + @Override + protected void initialize() throws ServletException { + setResourceProviders(new DummyPatientResourceProvider()); + } + +} \ No newline at end of file diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/security/OpenIdConnectBearerTokenSecurityManagerTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/security/OpenIdConnectBearerTokenSecurityManagerTest.java new file mode 100644 index 00000000000..4e83f0b1e10 --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/security/OpenIdConnectBearerTokenSecurityManagerTest.java @@ -0,0 +1,73 @@ +package ca.uhn.fhir.rest.server.security; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashMap; + +import javax.servlet.http.HttpServletRequest; + +import org.hamcrest.core.StringContains; +import org.junit.Test; +import org.mitre.jose.keystore.JWKSetKeyStore; +import org.mitre.jwt.signer.service.JwtSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.DefaultJwtSigningAndValidationService; +import org.mitre.jwt.signer.service.impl.JWKSetCacheService; +import org.mitre.oauth2.model.RegisteredClient; +import org.mitre.openid.connect.client.service.impl.DynamicRegistrationClientConfigurationService; +import org.mitre.openid.connect.client.service.impl.StaticClientConfigurationService; +import org.mitre.openid.connect.client.service.impl.StaticServerConfigurationService; +import org.mitre.openid.connect.config.ServerConfiguration; +import org.springframework.core.io.ClassPathResource; + +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; + +public class OpenIdConnectBearerTokenSecurityManagerTest { + + @Test + public void testValidateToken() throws Exception { + + StaticServerConfigurationService srv = new StaticServerConfigurationService(); + srv.setServers(new HashMap()); + ServerConfiguration srvCfg = new ServerConfiguration(); + srvCfg.setJwksUri("AAAAAA"); + srvCfg.setIssuer("http://localhost:8888/uhn-openid-connect/"); + srv.getServers().put("http://localhost:8888/uhn-openid-connect/", srvCfg); + srv.afterPropertiesSet(); + + StaticClientConfigurationService cli = new StaticClientConfigurationService(); + cli.setClients(new HashMap()); + cli.getClients().put("http://localhost:8888/uhn-openid-connect/", new RegisteredClient()); + + OpenIdConnectBearerTokenSecurityManager mgr = new OpenIdConnectBearerTokenSecurityManager(); + mgr.setClientConfigurationService(cli); + mgr.setServerConfigurationService(srv); + + HttpServletRequest req = mock(HttpServletRequest.class); + when(req.getHeader(Constants.HEADER_AUTHORIZATION)).thenReturn("Bearer " + "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MDY4NDE4NTgsImF1ZCI6WyJjbGllbnQiXSwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0Ojg4ODhcL3Vobi1vcGVuaWQtY29ubmVjdFwvIiwianRpIjoiOTNiMzRjOTUtNTNiMC00YzZmLTkwYjEtYWVjODRjZTc3OGFhIiwiaWF0IjoxNDA2ODM4MjU4fQ.fYtwehPUulUYnDG_10bN6TNf7uw2FNUh_E40YagpITrVfXsV06pjU2YpNgy8nbSFmxY9IBH44UXTmMH9PLFiRn88WsPMSrUQbFCcvGIYwhqkRjGm_J1Y6oWIafUzCwZBCvk4Ne44p3DJRR6FSZRnnC850p55901DGQmNLe-rZJk3t0MHl6wySduqT3K1-Vbuq-7H6xLE10hKpLhSqBTghpQNKNjm48jm0sHcFa3ENWzyWPOmpNfzDKmJAYK2UnBtqNSJP6AJzVrJXqSu-uzasq0VOVcRU4n8b39vU1olbho1eKF0cfQlQwbrtvWipBJJSsRp_tmB9SV9BXhENxOFTw"); + + JWKSetCacheService val = mock(JWKSetCacheService.class); + + JWKSetKeyStore keyStore = new JWKSetKeyStore(); + keyStore.setLocation(new ClassPathResource("/svr_keystore.jwks")); + DefaultJwtSigningAndValidationService valSvc = new DefaultJwtSigningAndValidationService(keyStore); + when(val.getValidator("AAAAAA")).thenReturn(valSvc); + + mgr.setValidationServices(val); + + try { + mgr.authenticate(req); + fail(); + } catch (AuthenticationException e) { + assertThat(e.getMessage(), StringContains.containsString("expired")); + } + + mgr.setTimeSkewAllowance(10 * 365 * 24 * 60); + mgr.authenticate(req); + + + } + +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/security/RestAuthenticationEntryPoint.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/security/RestAuthenticationEntryPoint.java new file mode 100644 index 00000000000..60a0f262eb0 --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/security/RestAuthenticationEntryPoint.java @@ -0,0 +1,18 @@ +package ca.uhn.fhir.rest.server.security; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{ + + @Override + public void commence( HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException ) throws IOException{ + response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized" ); + } +} \ No newline at end of file diff --git a/hapi-fhir-base/src/test/resources/logback-test.xml b/hapi-fhir-base/src/test/resources/logback-test.xml index fdd916f6e23..ba6af5fea46 100644 --- a/hapi-fhir-base/src/test/resources/logback-test.xml +++ b/hapi-fhir-base/src/test/resources/logback-test.xml @@ -7,8 +7,10 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + openid + email + address + profile + phone + + + + + + http://localhost:8080/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hapi-fhir-base/src/test/resources/securitytest_war/WEB-INF/web.xml b/hapi-fhir-base/src/test/resources/securitytest_war/WEB-INF/web.xml new file mode 100644 index 00000000000..56e6a03c828 --- /dev/null +++ b/hapi-fhir-base/src/test/resources/securitytest_war/WEB-INF/web.xml @@ -0,0 +1,35 @@ + + + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + + springSecurityFilterChain + /* + + + + org.springframework.web.context.ContextLoaderListener + + + + contextConfigLocation + + WEB-INF/app-ctx.xml + + + + + fhir + ca.uhn.fhir.rest.server.ServerSecurityTestRestfulServlet + + + + fhir + /fhir/* + + + \ No newline at end of file diff --git a/hapi-fhir-base/src/test/resources/svr_keystore.jwks b/hapi-fhir-base/src/test/resources/svr_keystore.jwks new file mode 100644 index 00000000000..dfa8e782ded --- /dev/null +++ b/hapi-fhir-base/src/test/resources/svr_keystore.jwks @@ -0,0 +1,11 @@ +{ + "keys": [ + { + "d": "S0pTrzR78vyso2ufMTAJMPNpqSgF0jggquQ2crgBw_il-QQ5zAHzlrreGWDcvr94i6PAy4xQYRAAaO_ZtjO4kMmEKdJ-bgsAJiIrUAlJdv0JCK-j3G64R0EiwrSzXqyt3E6kiA14o5wYgs86RO7TV6dpFyNuSIoZJeNG-NBviPIwY222PX07xjaMk7tSQdlTP6Vg1EZV-i0AqYGE-Ivn2eaI0KvjzPWVXVki1F6J4yQzErxcVILgicAp6Eeqy24E3enmrsQozj54Sswjy8r_MrGf2iSaf0GedLsCXWSQ-uojWF6llfjL9Q5be65ztPkvN4cPGn0o_sHzqewvFnZuAQ", + "e": "AQAB", + "n": "kmjocunUF1bw2rSnBtiA9P2LJcuUdTSpgS8e2MccFv0VLNFd50P7JMxmLt_ildr040371QPflz0wdE_O8gypJr17RyJCg7mthCRpNLxkVHS8Pd3p0fzn_xlmrm4gkR033k7aTH3bB-usjq5Z8A6GjxlUp2Of5TRJ1Tj9287m9X2w65DDCcOQYC8horObEEUkchfpyf5PGCdkq-1U46_P66RcFuLjxaO6R5OWrVK8Zd-tHOG-O3-nn0eAHxFvtGDiYOVImQ7rWEriTDQdefiIIOM_m73xbtdVM2jEgQfn76u0TvK7nLmz1w4XvddOv2TatXmAtNmTpP16629eqnKcMw", + "kty": "RSA", + "kid": "rsa1" + } + ] +} diff --git a/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component b/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component index 15e9bb84a2d..c94187882b4 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component +++ b/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component @@ -6,10 +6,10 @@ - + uses - + uses diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index fb4d411f568..2d1ca1e3b96 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -9,7 +9,6 @@ ../pom.xml - ca.uhn.hapi.fhir hapi-tinder-plugin maven-plugin diff --git a/pom.xml b/pom.xml index 7ce8213e3db..0c14d49d111 100644 --- a/pom.xml +++ b/pom.xml @@ -75,16 +75,18 @@ 5.1.0.Final 9.1.1.v20140108 9.1.1.v20140108 + 4.3.1 4.11 1.1.1 2.9.1 3.3 + 1.1.8 1.9.5 UTF-8 1.7.7 4.0.1.RELEASE + 3.2.4.RELEASE 2.1.3.RELEASE - 4.3.1