SMART on FHIR – adding OAuth2

You may recall that a week back we had a look at one of the connectathon scenarios – the SMART scenario.

In this post we’re going to take the work that we had done in the last post, and make it secure using the SMART version of the OAuth2 standard. As always, a primary reason I’m writing it down is so that when I forget what I did to make it work – I’ll have this as a reference to remind me <s>. And a reminder – I’m using the Java based HAPI FHIR client, in a web based application running in a Tomcat servlet engine, with IntelliJ IDEA as my IDE.

This is going to be a pretty technical post. If you’re from a clinical background, then the key take-away is that SMART is a healthcare-specific implementation of OAuth2, with the goal of creating a secure way of sharing what can be extremely sensitive data in a manner that uses standard industry protocols. The SMART stuff can cover off a lot of different scenarios (see the on-line documentation for more details), and I think it’s going to be an important underpinning of the health care ecosystem.

The specific scenario that I’m supporting is where I’m a user logged into an EMR/EHR system, and I want to use an external application to do cool stuff with my data – in this case a paediatric charting app provided by the SMART team. There are many ways of doing this – but as far as I am aware, they all use bespoke rather than standard solutions – and that won’t wash in an ecosystem.

Just to set the background – here’s a picture of the overall architecture.

oauth2

It’s a web-based application served off a Tomcat server which exposes FHIR data endpoints and the OAuth endpoints (that are the real subject of this post). The SMART app is deployed off an external server, and runs in an iFrame on the page. The Tomcat server does have some local data storage, but accesses external FHIR servers (eg Blaze) so is acing as a ‘facade’ or a ‘proxy’ to those services (once the app has been authenticated).

The job was made a bit more complex, because I wanted to move to using Maven as the basis of my Java work. James assured me that the gain was worth the pain – and he was right – but it did slow things down a bit. (He paid the price by having to support me through my trials and tribulations though…).

Installing Maven was easy (there are a ton of examples on the web). What did cause a few issues was that the latest versions of HAPI are released as snapshots, and maven doesn’t download those by default. I had to do 2 things:

Having done that, I created a new project based on the “maven-archetype-webapp” archetype. (btw – archetype is a rather overloaded term in our space – my friend Koray might take issue with its use here!). I configured the maven pom.xml file (that’s the maven config file in the application) to download HAPI. Then, I moved the FHIR endpoints across.

Next, I needed to enable CORS support. This is required because the SMART client is a JavaScript application that runs on a different domain, and so – as a server – I need to give permission for it to be able to access my data. I followed the instructions on the HAPI site and because I had got maven working, all I had to do was add a dependency to my pom.xml to download the filter, and then I updated the web.xml file with the permissions I wanted (I copied the instructions from the HAPI site) and it just worked.

I could now look at the specifics of the SMART integration. The details are on the quick start guide – and it’s all based on OAuth2 – but in summary:

  1. From the EMR web page, create an iframe, and direct that to the SMART application, passing across some initial config.
  2. The SMART applications loads and reads the conformance statement from my server so it knows how to find the authorization and token end points.
  3. It then calls the authorization end point, including the scopes (what it wants to do).
  4. Assuming that it passes authorization, the endpoint re-directs back to the SMART application passing across an Authorization Code.
  5. The SMART application then calls the token endpoint, passing in the auth code it just received – and receives back the Authorization Token.
  6. It then calls the EMR FHIR endpoints (including the Auth Token in the call) to get the data it needs, and renders the display to the user.

Note that be default all end points (Authorization, Token and FHIR) are hosted off the same server. I’m not sure if they HAVE to be – perhaps I should experiment with that.

So the first step is to set up the Authorization and Token end points, and then tell the SMART application where they are. This is done by using some extensions to the Conformance resource for the server.

In my case, I had a couple of steps to do first:

Create a basic ‘login’ function from my sample EMR application. It’s quite simple – the user sends login details to a login endpoint, which validates them, and creates a user token that is saved on the server in a context object, and sent back to the local app which it will include with the launch call. (I originally tried to use the session capabilities of tomcat, but these are all ajax calls, and it doesn’t work so well). Because the user of the system (as opposed to the SMART app user) also needs to access protected resources, they get an access token created as well.

Here’s the code for the Login servlet:

@WebServlet(urlPatterns= {&amp;quot;/auth/login&amp;quot;}, displayName=&amp;quot;Login&amp;quot;).
public class Login extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println(&amp;quot;login&amp;quot;);
        PrintWriter out = response.getWriter();
        //Here is where we check and validate username &amp;amp; password. We're going to cheat right now...
        //create a user object. This would ultimately be a FHIR obect I suspect...
        Person person = new Person();
        person.userName = request.getParameter(&amp;quot;username&amp;quot;);
        person.userToken = java.util.UUID.randomUUID().toString(); //generate a user token

        //save the user details in the context - we previously created this map...
        ServletContext context =  getServletContext();
        Map&amp;lt;String,Person&amp;gt; usertokens = (Map&amp;lt;String,Person&amp;gt;) context.getAttribute(&amp;quot;usertokens&amp;quot;);
        //save the access token for later use - in production persistent store...
        usertokens.put(person.userToken,person);

        //create an access token for this person ...
        Map&amp;lt;String,JsonObject&amp;gt; oauthtokens = (Map&amp;lt;String,JsonObject&amp;gt;) context.getAttribute(&amp;quot;oauthtokens&amp;quot;);
        JsonObject json = Json.createObjectBuilder()
                .add(&amp;quot;access_token&amp;quot;, person.userToken)
                .add(&amp;quot;token_type&amp;quot;, &amp;quot;bearer&amp;quot;)
                .add(&amp;quot;expires_in&amp;quot;, 3600)
                .add(&amp;quot;scope&amp;quot;, &amp;quot;patient/*.read&amp;quot;)
                .build();
        oauthtokens.put(person.userToken,json);

        response.addHeader(&amp;quot;Content-Type&amp;quot;,&amp;quot;application/json+fhir&amp;quot;);
        out.println(person.getJson().toString());
    }
}

Then I added an end-point to launch the SMART app – the icon on my EMR page makes the iframe navigate to that point (passing across the user token it got in the previous step as the ‘launch’ token). Here it is:

@WebServlet(urlPatterns= {&amp;quot;/auth/launch&amp;quot;}, displayName=&amp;quot;Launch SMART application&amp;quot;)
public class Launch extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println(&amp;quot;launch endpoint accessed&amp;quot;);
        String userToken = request.getParameter(&amp;quot;usertoken&amp;quot;);
        String patientId = request.getParameter(&amp;quot;patientid&amp;quot;);

        //make sure this is a logged in person (the have a valid token)
        ServletContext context =  getServletContext();
        Map&amp;lt;String,Person&amp;gt; usertokens = (Map&amp;lt;String,Person&amp;gt;) context.getAttribute(&amp;quot;usertokens&amp;quot;);

        if (usertokens.containsKey(userToken)) {
            //yep, this is a valid user...

            //retrieve the user object and update with the patient they have in context. We'll need this for the access token...
            Person person = (Person) usertokens.get(userToken);
            person.currentPatientId = patientId;

            //the re-direct URL. In reality the url and 'iss' would come from config...
            String url = &amp;quot;https://fhir.smartplatforms.org/apps/growth-chart/launch.html?&amp;quot;;
            url += &amp;quot;iss=http://localhost:8081/fhir&amp;quot;;

            //we'll use the user token as the launch token as we can use that to validate the Auth call..
            url += &amp;quot;&amp;amp;launch=&amp;quot; + userToken;

            response.sendRedirect(url);
        } else {
            response.setStatus(403);    //forbidden.
            PrintWriter out = response.getWriter();
            out.println(&amp;quot;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;User not logged in&amp;lt;/h1&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&amp;quot;);
        }
    }
}

Next up is the authorization end-point. The launch token (actually the user token in this case) is part of the scope, so we pull that out to make sure the request came from a valid source and then redirect to the applications redirect_url. In reality, we’d check that the scope was acceptable (in fact – deciding on a standardized set of scopes is going to be one of the many excellent outcomes of the SMART initiative) .

Here’s the code.

@WebServlet(urlPatterns= {&amp;quot;/auth/authorize&amp;quot;}, displayName=&amp;quot;Authorize endpoint for FHIR Server&amp;quot;)
public class Authorize extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println(&amp;quot;auth check...&amp;quot;);

        String response_type = request.getParameter(&amp;quot;response_type&amp;quot;);
        String client_id = request.getParameter(&amp;quot;client_id&amp;quot;);   //the id of the client
        String redirect_uri = request.getParameter(&amp;quot;redirect_uri&amp;quot;);
        String scope = request.getParameter(&amp;quot;scope&amp;quot;);   //what the app wants to do
        String state = request.getParameter(&amp;quot;state&amp;quot;);

        //the scope parameter includes the launch token - eg patient/*.read launch:7bceb3c6-66e9-46c9-8efd-9f87e76a5f9a
        //so we would pull out both scope and token, check that the token matches the one we set (actually the patient token)
        //and that the scope is acceptable to us. Should move this to a function somewhere...
        String[] arScopes =  scope.split(&amp;quot; &amp;quot;);
        String launchToken = &amp;quot;&amp;quot;;
        for (int i = 0; i &amp;lt; arScopes.length; i++){
            System.out.println(arScopes[i]);
            if (arScopes[i].substring(0,7).equals(&amp;quot;launch:&amp;quot;)) {
                launchToken = arScopes[i].substring(7);
            }
        }

        ServletContext context =  getServletContext();
        Map&amp;lt;String,Person&amp;gt; usertokens = (Map&amp;lt;String,Person&amp;gt;) context.getAttribute(&amp;quot;usertokens&amp;quot;);

        if (usertokens.containsKey(launchToken)) {
            //we'll assume that the user is OK with this scope, but this is where we can check...
            //so, now we create an auth_code and re-direct to the redirect_url...
            String auth_code = java.util.UUID.randomUUID().toString();
            //we'll save the auth code in a previously defined context variable. In real life you'd use a
            //persistent store of some type, and likely save more details...
            Map&amp;lt;String,Person&amp;gt; oauthcodes = (Map&amp;lt;String,Person&amp;gt;) context.getAttribute(&amp;quot;oauthcodes&amp;quot;);

            Person person = (Person) usertokens.get(launchToken);

            oauthcodes.put(auth_code,person);
            //and re-direct to the 'authenticated' endpoint of the application
            response.sendRedirect(redirect_uri + &amp;quot;?code=&amp;quot;+auth_code+ &amp;quot;&amp;amp;state=&amp;quot;+state);
        } else {
            response.setStatus(403);    //forbidden.
        }

    }
}

Note that we redirect to the applications callback – just as OAuth2 dictates – on successful authentication. We could choose to have a registration process that stores the application id and call-back url locally to further improve security.

So now the application can exchange its auth code for an auth token. It’s a simple matter of checking that the auth code matches the one we set during authorization, and constructing the auth token – which is a JSON object in SMART. Here we go with the Token endpoint:

@WebServlet(urlPatterns= {&amp;quot;/auth/token&amp;quot;}, displayName=&amp;quot;Token endpoint for FHIR Server&amp;quot;)
public class Token extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        PrintWriter out = response.getWriter();

        String code = request.getParameter(&amp;quot;code&amp;quot;);

        //the map containing auth_code that was set during the authorization phase...
        ServletContext context =  getServletContext();
        Map&amp;lt;String,Person&amp;gt; oauthcodes = (Map&amp;lt;String, Person&amp;gt;) context.getAttribute(&amp;quot;oauthcodes&amp;quot;);

        //is this a valid access code?
        if (oauthcodes.containsKey(code)) {
            Person person = (Person) oauthcodes.get(code);
            String access_token  = java.util.UUID.randomUUID().toString();
            JsonObject json = Json.createObjectBuilder()
                    .add(&amp;quot;access_token&amp;quot;, access_token)
                    .add(&amp;quot;patient&amp;quot;, person.currentPatientId)
                    .add(&amp;quot;token_type&amp;quot;, &amp;quot;bearer&amp;quot;)
                    .add(&amp;quot;expires_in&amp;quot;, 3600)
                    .add(&amp;quot;scope&amp;quot;, &amp;quot;patient/*.read&amp;quot;)
                    .build();
            response.addHeader(&amp;quot;Content-Type&amp;quot;,&amp;quot;application/json+fhir&amp;quot;);

            //save the access token for later use - like the codes, you'd use a persistent store...
            Map&amp;lt;String,JsonObject&amp;gt; oauthtokens = (Map&amp;lt;String,JsonObject&amp;gt;) context.getAttribute(&amp;quot;oauthtokens&amp;quot;);
            oauthtokens.put(access_token,json);
            //and return the token to the applciation
            out.println(json.toString());

        } else {
            //the auth codes don't match.
            response.setStatus(403);    //forbidden.
            out.println(&amp;quot;{}&amp;quot;);
        }
    }
}

So now that we have a valid auth token, we can make calls of the actual FHIR endpoint. I’sure that there will be further enhancements in HAPI to make this easier, so for the moment I’ve taken a simple step of overriding the ‘handleRequest’ method in the RestfulServer to check that there is a valid access token whenever anyone tries to access a FHIR clinical resource. (We let the conformance query pass of course, as that’s needed way back at the beginning so the external app knows where our auth and token end points are).

    @Override
    public void handleRequest(SearchMethodBinding.RequestType theRequestType,
                              javax.servlet.http.HttpServletRequest theRequest,
                              javax.servlet.http.HttpServletResponse theResponse)
            throws javax.servlet.ServletException,
            IOException {

        String uri = theRequest.getRequestURI();

        //anyone can access metadata...
        if (uri.equals(&amp;quot;/fhir/metadata&amp;quot;)) {
            super.handleRequest(theRequestType,theRequest,theResponse);
        } else {
            //but you need to be authorized to access clincial data...
            String auth = theRequest.getHeader(&amp;quot;Authorization&amp;quot;);
            if (auth != null){
                auth = auth.substring(7);//get rid of the 'Bearer ' at the front
                ServletContext context =  getServletContext();// request.    .setAttribute(&amp;quot;oauthtokens&amp;quot;, oauthtokens);
                Map&amp;lt;String,JsonObject&amp;gt; oauthtokens = (Map&amp;lt;String,JsonObject&amp;gt;) context.getAttribute(&amp;quot;oauthtokens&amp;quot;);
                if (oauthtokens.containsKey(auth)) {
                    //we could pull out the actual access token, and apply security logic there...
                    super.handleRequest(theRequestType,theRequest,theResponse);
                } else {
                    theResponse.setStatus(403);    //forbidden.
                }
            } else {
                theResponse.setStatus(403);    //forbidden.
            }
        }
    }

And here’s a screen shot of it all working. Magic! I’m going for the worlds worst EMR interface – click the login and then the SMART buttons and it will work. It’ll be more complete by connectathon…

Screen Shot 2014-08-12 at 10.58.22 am

Now, this is not the only way of doing it (by any stretch of the imagination) – and the code is hardly production quality – but I do think that it shows that it’s reasonably straight forward to apply OAuth2 to protecting health data.

We should also think about expiring Auth tokens, and what the flow should be when that happens. (I haven’t done this because I suspect that HAPI will do it for me, and I’m lazy…)

And I’m sure that the different solutions that people come up with (as well as this one) will be properly hammered at connectathon!

 

About David Hay
I'm an independent contractor working with a number of Organizations in the health IT space. I'm an HL7 Fellow, Chair Emeritus of HL7 New Zealand and a co-chair of the FHIR Management Group. I have a keen interest in health IT, especially health interoperability with HL7 and the FHIR standard. I'm the author of a FHIR training and design tool - clinFHIR - which is sponsored by InterSystems Ltd.

2 Responses to SMART on FHIR – adding OAuth2

  1. Good post, David. With respect to providing the OAuth2 endpoints via extensions to the Conformance resource, has consideration been given to include these as defined datatypes within the Security element? Otherwise, a SMART app (or any other consumer) would need to parse out the URIs to find the authorization endpoint.

  2. David Hay says:

    Hi Peter – thanks for that! (Was one of the harder ones <s>)…

    wrt converting extensions to defined elements, that may be feasible, though would need the same consideration as any other alteration to resource structure, and hard to argue that most systems are doing that now…

    But as a ‘commonly used’ extension – they are pretty easy to use, especially with the libraries available, so not sure how much effort would be saved…

Leave a Reply

%d bloggers like this: