Add client interceptor which adds bearer tokens for OAUTH2
This commit is contained in:
parent
5c0ccaab9a
commit
a610c1cb38
|
@ -5,6 +5,7 @@ import ca.uhn.fhir.rest.client.IGenericClient;
|
|||
import ca.uhn.fhir.rest.client.IRestfulClientFactory;
|
||||
import ca.uhn.fhir.rest.client.api.IBasicClient;
|
||||
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.server.EncodingEnum;
|
||||
|
||||
|
@ -45,6 +46,27 @@ public class ClientExamples {
|
|||
// 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")
|
||||
public void createLogging() {
|
||||
{
|
||||
|
|
|
@ -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/>
|
||||
* <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
|
||||
}
|
||||
|
||||
}
|
|
@ -48,6 +48,8 @@ public class Constants {
|
|||
public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
|
||||
public static final String HEADER_AUTHORIZATION = "Authorization";
|
||||
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_CONTENT_DISPOSITION = "Content-Disposition";
|
||||
public static final String HEADER_CONTENT_ENCODING = "Content-Encoding";
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
The following section shows some sample interceptors which may be used.
|
||||
</p>
|
||||
|
||||
<subsection name="HTTP Basic Authorization">
|
||||
<subsection name="Security: HTTP Basic Authorization">
|
||||
|
||||
<p>
|
||||
The following example shows how to configure your client to
|
||||
|
@ -39,14 +39,29 @@
|
|||
|
||||
</subsection>
|
||||
|
||||
<a name="req_resp_logging"/>
|
||||
<subsection name="Logging Requests and Responses">
|
||||
<subsection name="Security: HTTP Bearer Token Authorization">
|
||||
|
||||
<p>
|
||||
An interceptor is provided with HAPI FHIR which can be used to
|
||||
log every transaction. The interceptor can be configured to be extremely
|
||||
The following example shows how to configure your client to
|
||||
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)
|
||||
or simply to log request URLs.
|
||||
or simply to log request URLs, or some combination in between.
|
||||
</p>
|
||||
|
||||
<macro name="snippet">
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +1,17 @@
|
|||
package ca.uhn.fhir.rest.client;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.ResourceBundle;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.mockito.Matchers.argThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
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.client.interceptor.LoggingInterceptor;
|
||||
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.util.PortUtil;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.classic.spi.LoggingEvent;
|
||||
import ch.qos.logback.core.Appender;
|
||||
|
@ -33,44 +35,45 @@ import ch.qos.logback.core.Appender;
|
|||
/**
|
||||
* Created by dsotnikov on 2/25/2014.
|
||||
*/
|
||||
public class InterceptorTest {
|
||||
public class LoggingInterceptorTest {
|
||||
|
||||
private static int ourPort;
|
||||
private static Server ourServer;
|
||||
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
|
||||
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);
|
||||
client.registerInterceptor(new LoggingInterceptor(true));
|
||||
Patient patient = client.read(Patient.class, "1");
|
||||
assertFalse(patient.getIdentifierFirstRep().isEmpty());
|
||||
|
||||
// int times = 1;
|
||||
// if (LoggerFactory.getLogger(ResourceBinding.class).isDebugEnabled()) {
|
||||
// times = 3;
|
||||
// }
|
||||
|
||||
verify(mockAppender).doAppend(argThat(new ArgumentMatcher<ILoggingEvent>() {
|
||||
verify(myMockAppender).doAppend(argThat(new ArgumentMatcher<ILoggingEvent>() {
|
||||
@Override
|
||||
public boolean matches(final Object argument) {
|
||||
return ((LoggingEvent) argument).getFormattedMessage().contains("Content-Type: application/xml+fhir; charset=UTF-8");
|
||||
}
|
||||
}));
|
||||
|
||||
} finally {
|
||||
root.detachAppender(mockAppender);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterClass
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -47,8 +47,6 @@ import com.nimbusds.jwt.SignedJWT;
|
|||
|
||||
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);
|
||||
|
||||
@Autowired
|
||||
|
@ -68,8 +66,6 @@ public class OpenIdConnectBearerTokenServerInterceptor extends InterceptorAdapte
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException {
|
||||
if (theRequestDetails.getOtherOperationType() == OtherOperationTypeEnum.METADATA) {
|
||||
|
@ -89,11 +85,11 @@ public class OpenIdConnectBearerTokenServerInterceptor extends InterceptorAdapte
|
|||
if (token == null) {
|
||||
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)");
|
||||
}
|
||||
|
||||
token = token.substring(BEARER_PREFIX.length());
|
||||
token = token.substring(Constants.HEADER_AUTHORIZATION_VALPREFIX_BEARER.length());
|
||||
|
||||
SignedJWT idToken;
|
||||
try {
|
||||
|
|
Loading…
Reference in New Issue