Add client interceptor which adds bearer tokens for OAUTH2

This commit is contained in:
James Agnew 2014-09-24 14:57:37 -04:00
parent 5c0ccaab9a
commit a610c1cb38
11 changed files with 323 additions and 48 deletions

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.client.IRestfulClientFactory; import ca.uhn.fhir.rest.client.IRestfulClientFactory;
import ca.uhn.fhir.rest.client.api.IBasicClient; import ca.uhn.fhir.rest.client.api.IBasicClient;
import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor; import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor;
import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.EncodingEnum;
@ -45,6 +46,27 @@ public class ClientExamples {
// END SNIPPET: security // END SNIPPET: security
} }
@SuppressWarnings("unused")
public void createSecurityBearer() {
// START SNIPPET: securityBearer
// Create a context and get the client factory so it can be configured
FhirContext ctx = new FhirContext();
IRestfulClientFactory clientFactory = ctx.getRestfulClientFactory();
// In reality the token would have come from an authorization server
String token = "3w03fj.r3r3t";
BearerTokenAuthInterceptor authInterceptor = new BearerTokenAuthInterceptor(token);
// Register the interceptor with your client (either style)
IPatientClient annotationClient = ctx.newRestfulClient(IPatientClient.class, "http://localhost:9999/fhir");
annotationClient.registerInterceptor(authInterceptor);
IGenericClient genericClient = ctx.newRestfulGenericClient("http://localhost:9999/fhir");
annotationClient.registerInterceptor(authInterceptor);
// END SNIPPET: securityBearer
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public void createLogging() { public void createLogging() {
{ {

View File

@ -0,0 +1,63 @@
package ca.uhn.fhir.rest.client.interceptor;
/*
* #%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 java.io.IOException;
import org.apache.commons.lang3.Validate;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import ca.uhn.fhir.rest.client.IClientInterceptor;
import ca.uhn.fhir.rest.server.Constants;
/**
* HTTP interceptor to be used for adding HTTP Authorization using "bearer tokens" to requests. Bearer tokens are used for protocols such as OAUTH2 (see the <a
* href="http://tools.ietf.org/html/rfc6750">RFC 6750</a> specification on bearer token usage for more information).
* <p>
* This interceptor adds a header resembling the following:<br/>
* &nbsp;&nbsp;&nbsp;<code>Authorization: Bearer dsfu9sd90fwp34.erw0-reu</code><br/>
* where the token portion (at the end of the header) is supplied by the invoking code.
* </p>
* <p>
* See the <a href="http://hl7api.sourceforge.net/hapi-fhir/doc_rest_client.html#HTTP_Basic_Authorization">HAPI Documentation</a> for information on how to use this class.
* </p>
*/
public class BearerTokenAuthInterceptor implements IClientInterceptor {
private String myToken;
public BearerTokenAuthInterceptor(String theToken) {
Validate.notNull("theToken must not be null");
myToken = theToken;
}
@Override
public void interceptRequest(HttpRequestBase theRequest) {
theRequest.addHeader(Constants.HEADER_AUTHORIZATION, (Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER + myToken));
}
@Override
public void interceptResponse(HttpResponse theResponse) throws IOException {
// nothing
}
}

View File

@ -48,6 +48,8 @@ public class Constants {
public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding"; public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
public static final String HEADER_AUTHORIZATION = "Authorization"; public static final String HEADER_AUTHORIZATION = "Authorization";
public static final String HEADER_CATEGORY = "Category"; public static final String HEADER_CATEGORY = "Category";
public static final String HEADER_AUTHORIZATION_VALPREFIX_BASIC = "Basic ";
public static final String HEADER_AUTHORIZATION_VALPREFIX_BEARER = "Bearer ";
public static final String HEADER_CATEGORY_LC = HEADER_CATEGORY.toLowerCase(); public static final String HEADER_CATEGORY_LC = HEADER_CATEGORY.toLowerCase();
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
public static final String HEADER_CONTENT_ENCODING = "Content-Encoding"; public static final String HEADER_CONTENT_ENCODING = "Content-Encoding";

View File

@ -25,7 +25,7 @@
The following section shows some sample interceptors which may be used. The following section shows some sample interceptors which may be used.
</p> </p>
<subsection name="HTTP Basic Authorization"> <subsection name="Security: HTTP Basic Authorization">
<p> <p>
The following example shows how to configure your client to The following example shows how to configure your client to
@ -39,14 +39,29 @@
</subsection> </subsection>
<a name="req_resp_logging"/> <subsection name="Security: HTTP Bearer Token Authorization">
<subsection name="Logging Requests and Responses">
<p> <p>
An interceptor is provided with HAPI FHIR which can be used to The following example shows how to configure your client to
log every transaction. The interceptor can be configured to be extremely inject a bearer token authorization header into every request. This
is used to satisfy servers which are protected using OAUTH2.
</p>
<macro name="snippet">
<param name="id" value="securityBearer" />
<param name="file" value="examples/src/main/java/example/ClientExamples.java" />
</macro>
</subsection>
<a name="req_resp_logging"/>
<subsection name="Logging: Log Requests and Responses">
<p>
The <code>LoggingInterceptor</code> can be used to
log every transaction. The interceptor is flexible and can be configured to be extremely
verbose (logging entire transactions including HTTP headers and payload bodies) verbose (logging entire transactions including HTTP headers and payload bodies)
or simply to log request URLs. or simply to log request URLs, or some combination in between.
</p> </p>
<macro name="snippet"> <macro name="snippet">

View File

@ -0,0 +1,87 @@
package ca.uhn.fhir.rest.client;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.StringReader;
import java.nio.charset.Charset;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicStatusLine;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor;
import ca.uhn.fhir.rest.server.Constants;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class BasicAuthInterceptorTest {
private static FhirContext ourCtx;
private HttpClient myHttpClient;
private HttpResponse myHttpResponse;
@Before
public void before() {
myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs());
ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient);
myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
}
private String crerateMsg() {
//@formatter:off
String msg = "<Patient xmlns=\"http://hl7.org/fhir\">"
+ "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>"
+ "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>"
+ "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>"
+ "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>"
+ "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>"
+ "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>"
+ "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />"
+ "</Patient>";
//@formatter:on
return msg;
}
@Test
public void testRequest() throws Exception {
String msg = crerateMsg();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
Header[] headers = new Header[] {};
when(myHttpResponse.getAllHeaders()).thenReturn(headers);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo");
client.registerInterceptor(new BasicAuthInterceptor("myuser", "mypass"));
client.getPatientById(new IdDt("111"));
HttpUriRequest req = capt.getValue();
assertEquals("Basic bXl1c2VyOm15cGFzcw==", req.getFirstHeader("Authorization").getValue());
}
@BeforeClass
public static void beforeClass() {
ourCtx = new FhirContext();
}
}

View File

@ -0,0 +1,87 @@
package ca.uhn.fhir.rest.client;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.StringReader;
import java.nio.charset.Charset;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicStatusLine;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor;
import ca.uhn.fhir.rest.server.Constants;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class BearerTokenAuthInterceptorTest {
private static FhirContext ourCtx;
private HttpClient myHttpClient;
private HttpResponse myHttpResponse;
@Before
public void before() {
myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs());
ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient);
myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
}
private String crerateMsg() {
//@formatter:off
String msg = "<Patient xmlns=\"http://hl7.org/fhir\">"
+ "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>"
+ "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>"
+ "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>"
+ "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>"
+ "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>"
+ "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>"
+ "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />"
+ "</Patient>";
//@formatter:on
return msg;
}
@Test
public void testRequest() throws Exception {
String msg = crerateMsg();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
Header[] headers = new Header[] {};
when(myHttpResponse.getAllHeaders()).thenReturn(headers);
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo");
client.registerInterceptor(new BearerTokenAuthInterceptor("mytoken"));
client.getPatientById(new IdDt("111"));
HttpUriRequest req = capt.getValue();
assertEquals("Bearer mytoken", req.getFirstHeader("Authorization").getValue());
}
@BeforeClass
public static void beforeClass() {
ourCtx = new FhirContext();
}
}

View File

@ -1,15 +1,17 @@
package ca.uhn.fhir.rest.client; package ca.uhn.fhir.rest.client;
import static org.junit.Assert.*; import static org.junit.Assert.assertFalse;
import static org.mockito.Matchers.*; import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.util.ResourceBundle; import static org.mockito.Mockito.when;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatcher;
@ -23,9 +25,9 @@ import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.PortUtil;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender; import ch.qos.logback.core.Appender;
@ -33,44 +35,45 @@ import ch.qos.logback.core.Appender;
/** /**
* Created by dsotnikov on 2/25/2014. * Created by dsotnikov on 2/25/2014.
*/ */
public class InterceptorTest { public class LoggingInterceptorTest {
private static int ourPort; private static int ourPort;
private static Server ourServer; private static Server ourServer;
private static FhirContext ourCtx; private static FhirContext ourCtx;
private Appender<ILoggingEvent> myMockAppender;
private Logger myLoggerRoot;
@SuppressWarnings("unchecked")
@Before
public void before() {
/*
* This is a bit funky, but it's useful for verifying that the headers actually get logged
*/
myLoggerRoot = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
myMockAppender = mock(Appender.class);
when(myMockAppender.getName()).thenReturn("MOCK");
myLoggerRoot.addAppender(myMockAppender);
}
@After
public void after() {
myLoggerRoot.detachAppender(myMockAppender);
}
@Test @Test
public void testLogger() throws Exception { public void testLogger() throws Exception {
/*
* This is a bit funky, but we're verifying that the headers actually get logged
*/
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
@SuppressWarnings("unchecked")
Appender<ILoggingEvent> mockAppender = mock(Appender.class);
when(mockAppender.getName()).thenReturn("MOCK");
root.addAppender(mockAppender);
try {
IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort); IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort);
client.registerInterceptor(new LoggingInterceptor(true)); client.registerInterceptor(new LoggingInterceptor(true));
Patient patient = client.read(Patient.class, "1"); Patient patient = client.read(Patient.class, "1");
assertFalse(patient.getIdentifierFirstRep().isEmpty()); assertFalse(patient.getIdentifierFirstRep().isEmpty());
// int times = 1; verify(myMockAppender).doAppend(argThat(new ArgumentMatcher<ILoggingEvent>() {
// if (LoggerFactory.getLogger(ResourceBinding.class).isDebugEnabled()) {
// times = 3;
// }
verify(mockAppender).doAppend(argThat(new ArgumentMatcher<ILoggingEvent>() {
@Override @Override
public boolean matches(final Object argument) { public boolean matches(final Object argument) {
return ((LoggingEvent) argument).getFormattedMessage().contains("Content-Type: application/xml+fhir; charset=UTF-8"); return ((LoggingEvent) argument).getFormattedMessage().contains("Content-Type: application/xml+fhir; charset=UTF-8");
} }
})); }));
} finally {
root.detachAppender(mockAppender);
}
} }
@AfterClass @AfterClass

View File

@ -47,8 +47,6 @@ import com.nimbusds.jwt.SignedJWT;
public class OpenIdConnectBearerTokenServerInterceptor extends InterceptorAdapter { public class OpenIdConnectBearerTokenServerInterceptor extends InterceptorAdapter {
private static final String BEARER_PREFIX = "Bearer ";
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OpenIdConnectBearerTokenServerInterceptor.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OpenIdConnectBearerTokenServerInterceptor.class);
@Autowired @Autowired
@ -68,8 +66,6 @@ public class OpenIdConnectBearerTokenServerInterceptor extends InterceptorAdapte
} }
@Override @Override
public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException { public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException {
if (theRequestDetails.getOtherOperationType() == OtherOperationTypeEnum.METADATA) { if (theRequestDetails.getOtherOperationType() == OtherOperationTypeEnum.METADATA) {
@ -89,11 +85,11 @@ public class OpenIdConnectBearerTokenServerInterceptor extends InterceptorAdapte
if (token == null) { if (token == null) {
throw new AuthenticationException("Not authorized (no authorization header found in request)"); throw new AuthenticationException("Not authorized (no authorization header found in request)");
} }
if (!token.startsWith(BEARER_PREFIX)) { if (!token.startsWith(Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER)) {
throw new AuthenticationException("Not authorized (authorization header does not contain a bearer token)"); throw new AuthenticationException("Not authorized (authorization header does not contain a bearer token)");
} }
token = token.substring(BEARER_PREFIX.length()); token = token.substring(Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER.length());
SignedJWT idToken; SignedJWT idToken;
try { try {