Constrain access to a fhir store resources through a Proxy (smart on fhir?)

I have a GCP Healthcare API deployed in GCP. I have an API Product & proxy returning data ok. The IAM service account in the Apigee project has firestore reader permissions in the fhir store project and to access the fhir store my proxy is reading the fhir store like this :

 

  <HTTPTargetConnection>
    <URL>https://SET_URL_FROM_CONFIG</URL>
    <Authentication>
      <GoogleAccessToken>
        <Scopes>
          <Scope>https://www.googleapis.com/auth/cloud-platform</Scope>
        </Scopes>
      </GoogleAccessToken>
    </Authentication>
  </HTTPTargetConnection>

 

The above works just fine.

I'm looking to create an API Product that constrains access to a subset of fhir resources. e.g. Organization, Location, Practitioner

What are my options?

What I've tried:

1. Using Operations --> No good. I can constrain the proxy by Operations e.g. /Practitioner/** but this is easily circumvented with a fhir reverse include query like this :
proxyname/Practitioner?_id=<unique_fhirstore_id>&_revinclude=Encounter:practitioner

2. Was hoping using scopes like this below would work? docs ref
See the various attempts commented out..

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AssignMessage continueOnError="false" enabled="true" name="AssignMessage-AddScopesHeader">
    <DisplayName>AssignMessage-AddScopesHeader</DisplayName>
    <Properties/>
    <Add>
        <!-- https://cloud.google.com/healthcare-api/docs/smart-on-fhir#set-and -->
        <!-- This is a scopes test. Will this restrict the FHIR server as described ?? -->
        <Headers>
            <!-- <Header name="X-Authorization-Scope">user/Practitioner.read user/PractitionerRole.read user/Organization.read user/Location.read</Header> -->
            <!-- <Header name="X-Authorization-Scope">user/Practitioner.read</Header> -->
            <!-- <Header name="X-Authorization-Scope">user/*.rs</Header> -->
            <Header name="X-Authorization-Scope">user/*.read</Header>
        </Headers>
    </Add>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    <AssignTo createNew="false" transport="http" type="request"/>
</AssignMessage>

 

It's not behaving at the moment. Still trying it. Thought I'd ask if in case I was heading down a rabbit hole!

Thanks













Solved Solved
0 7 494
3 ACCEPTED SOLUTIONS

What you're doing makes sense to me.

Injecting headers like the X-Authorization-Scope to communicate to Cloud Healthcare API what access control is necessary, is an exemplary use of Apigee in front of FHIRStore.

If I were you, I would make a couple changes:

  1. use Set instead of Add. Set overwrites any existing header, which is what you want here. Add will ... add to any existing header, or set the header if it is not already present. By using Set, you insure that the header holds only the value your proxy intends to be there. You don't want the client to be able to send in a X-Authorization-Scope header, and then have Apigee simply add to it. This would be a security vulnerability, obviously.
  2. Omit the

     <AssignTo createNew="false" transport="http" type="request"/>

    It does nothing. You DO have to make sure you attach that AssignMessage policy into the Request flow, in order for it to be received by the Healthcare API.

View solution in original post

I was trying to avoid adding a call to an Authorisation server (to turn a JWT with scopes into an access token as you've done here in github) between Apigee and the Healthcare API.

YES. And the Authentication element.... this one:

 

     <HTTPTargetConnection>
        <URL>https://SET_URL_FROM_CONFIG</URL>
        <Authentication>
            <GoogleIDToken>
                <Audience useTargetUrl="true"/>
            </GoogleIDToken>
        </Authentication>
        ...

 

Does that for you.

OR DOES IT?

You specifically wrote

...turn a JWT with scopes into an access token between Apigee and the Healthcare API.

I want to call your attention to the options under the Authentication element. With the child element, you can tell Apigee to either send an ID TOKEN or an ACCESS TOKEN in the Authorization header. With the configuration shown above, using GoogleIDToken, it sends an ID TOKEN. I think what you want is an ACCESS TOKEN, so you must use GoogleAccessToken in the configuration. Like this:

 

     <HTTPTargetConnection>
        <URL>https://SET_URL_FROM_CONFIG</URL>
        <Authentication>
          <GoogleAccessToken>
            <Scopes>
              <Scope>https://www.googleapis.com/auth/cloud-platform</Scope>
            </Scopes>
          </GoogleAccessToken>
        </Authentication>
        ...

 

View solution in original post

Wow! It works! Was expecting a bit of a mammoth struggle (my bad!). It's super helpful to get a clear answer here when you are struggling a bit. Thanks @dchiesa1. To Summarize what I now have in place:

The Target endpoint:

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TargetEndpoint name="default">
  <PreFlow>
    <Request>
      <Step>
        <Name>AssignMessage-AddScopesHeader</Name>
      </Step>
    </Request>
    <Response>
      <!-- This shared flow replaces any occurrences of the HTTPTargetConnection/URL with the proxy base URL  -->
      <Step>
        <Name>FlowCallout-RedirectBackendUrls</Name>
      </Step>
    </Response>
  </PreFlow>
  <PostFlow>
    <Request>
      <Step>
        <Name>FlowCallout-SetTargetUrlFromConfig</Name>
      </Step>
    </Request>
  </PostFlow>
  <HTTPTargetConnection>
    <URL>https://SET_URL_FROM_CONFIG</URL>
    <Authentication>
      <GoogleAccessToken>
        <Scopes>
          <Scope>https://www.googleapis.com/auth/cloud-platform</Scope>
        </Scopes>
      </GoogleAccessToken>
    </Authentication>
  </HTTPTargetConnection>
</TargetEndpoint>

 

 The fixed scopes header limiting the fhir server using scopes:

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AssignMessage continueOnError="false" enabled="true" name="AssignMessage-AddScopesHeader">
  <DisplayName>AssignMessage-AddScopesHeader</DisplayName>
  <Properties/>
  <Set>
    <!-- https://cloud.google.com/healthcare-api/docs/smart-on-fhir#set-and -->
    <!-- This is a scopes test. Will this restrict the FHIR server as described ?? -->
    <Headers>
      <Header name="X-Authorization-Scope">user/Practitioner.read user/Organization.read user/PractitionerRole.read user/Location.read</Header>
    </Headers>
  </Set>
  <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</AssignMessage>

 

Result:

- I can read the above 4 fhir resources fine. <proxy>/Organization ... etc
- When I now query a patient fhir resource that is on the Cloud Healthcare API the server is responding with 403 forbidden and the message below:

 

{
    "issue": [
        {
            "code": "security",
            "details": {
                "text": "permission_denied"
            },
            "diagnostics": "SMART access denied or the resource being accessed does not exist",
            "severity": "error"
        }
    ],
    "resourceType": "OperationOutcome"

 

This proves the SMART on fhir scopes are having the desired affect. I may look at how I can tidy up that message but it's nice and clear that the backend is doing what we wanted. 

I would like to be able to also pull this off with a JWT signed with the service account key which I think we will need in the future (for flexibly injecting scopes) but I'll post a separate post about how that goes (I'll have a few more questions no doubt). I'm assuming this repo is relevant for JWT generation for the Clould Healthcare API.

Marking this as solved. Thanks again.

 


View solution in original post

7 REPLIES 7

What you're doing makes sense to me.

Injecting headers like the X-Authorization-Scope to communicate to Cloud Healthcare API what access control is necessary, is an exemplary use of Apigee in front of FHIRStore.

If I were you, I would make a couple changes:

  1. use Set instead of Add. Set overwrites any existing header, which is what you want here. Add will ... add to any existing header, or set the header if it is not already present. By using Set, you insure that the header holds only the value your proxy intends to be there. You don't want the client to be able to send in a X-Authorization-Scope header, and then have Apigee simply add to it. This would be a security vulnerability, obviously.
  2. Omit the

     <AssignTo createNew="false" transport="http" type="request"/>

    It does nothing. You DO have to make sure you attach that AssignMessage policy into the Request flow, in order for it to be received by the Healthcare API.

Hi Dino,

Thanks. Your examples around the place have helped a lot with our Apigee journey.

I've made those changes but as soon as I set that X-Authentication-Scope header I'm getting a 401 (See below).


I'm wondering if I'm going to need to create a JWT to pass scopes through to the Cloud Healthcare API FHIR server as has been mentioned  in other discussions

 

 

{
    "error": {
        "code": 401,
        "message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
        "status": "UNAUTHENTICATED",
        "details": [
            {
                "@type": "type.googleapis.com/google.rpc.ErrorInfo",
                "reason": "CREDENTIALS_MISSING",
                "domain": "googleapis.com",
                "metadata": {
                    "method": "google.cloud.healthcare.v1beta1.fhir.rest.FhirService.SearchResources",
                    "service": "healthcare.googleapis.com"
                }
            }
        ]
    }
}

 

 

My Target Endpoint flow looks like this 

 

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TargetEndpoint name="default">
    <PreFlow>
        <Request>
            <Step>
                <Name>AssignMessage-AddScopesHeader</Name>
            </Step>
        </Request>
        <Response>
            <!-- This shared flow replaces any occurrences of the HTTPTargetConnection/URL with the proxy base URL  -->
            <Step>
                <Name>FlowCallout-RedirectBackendUrls</Name>
            </Step>
        </Response>
    </PreFlow>
    <PostFlow>
        <Request>
            <Step>
                <Name>FlowCallout-SetTargetUrlFromConfig</Name>
            </Step>
        </Request>
    </PostFlow>
    <HTTPTargetConnection>
        <URL>https://SET_URL_FROM_CONFIG</URL>
        <Authentication>
            <HeaderName>X-Serverless-Authorization</HeaderName>
            <GoogleIDToken>
                <Audience useTargetUrl="true"/>
            </GoogleIDToken>
        </Authentication>

 

 

Thanks. Your examples around the place have helped a lot with our Apigee journey.

Glad you have found them helpful!

I've made those changes but as soon as I set that X-Authentication-Scope header I'm getting a 401 (See below). ... I'm wondering if I'm going to need to create a JWT to pass scopes through to the Cloud Healthcare API FHIR server as has been mentioned in other discussions.

Hmm ok

The Authentication element under HTTPTargetConnection in the TargetEndpoint...can create an access token for you, automatically. And by default it injects that token into the header named Authorization , as a Bearer token. It's as if you used AssignMessage to inject the header yourself, like this:

 

<AssignMessage name='AM-Just-an-example'>
  <Set>
   <Headers>
     <Header name='Authorization'>Bearer {generated_token}</Header>
   </Headers>
  </Set>
</AssignMessage>

 

...except that you don't have to do the work of generating the token.

The HeaderName element in your configuration.... this part:

 

<HTTPTargetConnection>
  <URL>https://SET_URL_FROM_CONFIG</URL>
  <Authentication>
    <HeaderName>X-Serverless-Authorization</HeaderName> <!-- this part -->
    ...

 

...tells Apigee to use a different header to hold the bearer token. In that configuration, it says use the header named X-Serverless-Authorization .

I believe the Healthcare API is looking for the bearer token to appear in the default header, the header named Authorization . If I am correct then you should remove the HeaderName element. like this:

 

<HTTPTargetConnection>
  <URL>https://SET_URL_FROM_CONFIG</URL>
  <Authentication>
     <!-- <HeaderName>X-Serverless-Authorization</HeaderName> use default header -->
     ...

 

ok but one more thing. The Authentication element can create either an ID token or an access token. In your original post you had a configuration like this:

 

     <HTTPTargetConnection>
        <URL>https://SET_URL_FROM_CONFIG</URL>
        <Authentication>
          <GoogleAccessToken>
            <Scopes>
              <Scope>https://www.googleapis.com/auth/cloud-platform</Scope>
            </Scopes>
          </GoogleAccessToken>
        </Authentication>

 

But in your followup, you showed a configuration like this:

 

    <HTTPTargetConnection>
        <URL>https://SET_URL_FROM_CONFIG</URL>
        <Authentication>
            <HeaderName>X-Serverless-Authorization</HeaderName>
            <GoogleIDToken>
                <Audience useTargetUrl="true"/>
            </GoogleIDToken>
        </Authentication>

 

You can see the difference. The first tells Apigee to create an access token, and to use the default (Authorization) header for the token. The second tells Apigee to create an ID Token, and to use a custom header (X-Serverless-Authorization). I think you want the former.

And the result of all that, along with your other AssignMessage policy, is that two headers: Authorization and X-Authorization-Scope , get propagated to the Healthcare API. And the Authorization header will carry an access token with the scopes you desire. And all of that should give HealthcareAPI enough information to carry out the request.

The other point to add is that I took this route because this API Product is always going to restrict end users accessing the API to the same subset of FHIR resources. I don't need to dynamically update scopes (permissions) for different users. By doing this I was trying to avoid adding a call to an Authorisation server (to turn a JWT with scopes into an access token as you've done here in github) between Apigee and the Healthcare API. But I've considered that the overhead would be minimal and I could set the access token's expiry to a longer period of time.

I was trying to avoid adding a call to an Authorisation server (to turn a JWT with scopes into an access token as you've done here in github) between Apigee and the Healthcare API.

YES. And the Authentication element.... this one:

 

     <HTTPTargetConnection>
        <URL>https://SET_URL_FROM_CONFIG</URL>
        <Authentication>
            <GoogleIDToken>
                <Audience useTargetUrl="true"/>
            </GoogleIDToken>
        </Authentication>
        ...

 

Does that for you.

OR DOES IT?

You specifically wrote

...turn a JWT with scopes into an access token between Apigee and the Healthcare API.

I want to call your attention to the options under the Authentication element. With the child element, you can tell Apigee to either send an ID TOKEN or an ACCESS TOKEN in the Authorization header. With the configuration shown above, using GoogleIDToken, it sends an ID TOKEN. I think what you want is an ACCESS TOKEN, so you must use GoogleAccessToken in the configuration. Like this:

 

     <HTTPTargetConnection>
        <URL>https://SET_URL_FROM_CONFIG</URL>
        <Authentication>
          <GoogleAccessToken>
            <Scopes>
              <Scope>https://www.googleapis.com/auth/cloud-platform</Scope>
            </Scopes>
          </GoogleAccessToken>
        </Authentication>
        ...

 

Wow! It works! Was expecting a bit of a mammoth struggle (my bad!). It's super helpful to get a clear answer here when you are struggling a bit. Thanks @dchiesa1. To Summarize what I now have in place:

The Target endpoint:

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TargetEndpoint name="default">
  <PreFlow>
    <Request>
      <Step>
        <Name>AssignMessage-AddScopesHeader</Name>
      </Step>
    </Request>
    <Response>
      <!-- This shared flow replaces any occurrences of the HTTPTargetConnection/URL with the proxy base URL  -->
      <Step>
        <Name>FlowCallout-RedirectBackendUrls</Name>
      </Step>
    </Response>
  </PreFlow>
  <PostFlow>
    <Request>
      <Step>
        <Name>FlowCallout-SetTargetUrlFromConfig</Name>
      </Step>
    </Request>
  </PostFlow>
  <HTTPTargetConnection>
    <URL>https://SET_URL_FROM_CONFIG</URL>
    <Authentication>
      <GoogleAccessToken>
        <Scopes>
          <Scope>https://www.googleapis.com/auth/cloud-platform</Scope>
        </Scopes>
      </GoogleAccessToken>
    </Authentication>
  </HTTPTargetConnection>
</TargetEndpoint>

 

 The fixed scopes header limiting the fhir server using scopes:

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AssignMessage continueOnError="false" enabled="true" name="AssignMessage-AddScopesHeader">
  <DisplayName>AssignMessage-AddScopesHeader</DisplayName>
  <Properties/>
  <Set>
    <!-- https://cloud.google.com/healthcare-api/docs/smart-on-fhir#set-and -->
    <!-- This is a scopes test. Will this restrict the FHIR server as described ?? -->
    <Headers>
      <Header name="X-Authorization-Scope">user/Practitioner.read user/Organization.read user/PractitionerRole.read user/Location.read</Header>
    </Headers>
  </Set>
  <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</AssignMessage>

 

Result:

- I can read the above 4 fhir resources fine. <proxy>/Organization ... etc
- When I now query a patient fhir resource that is on the Cloud Healthcare API the server is responding with 403 forbidden and the message below:

 

{
    "issue": [
        {
            "code": "security",
            "details": {
                "text": "permission_denied"
            },
            "diagnostics": "SMART access denied or the resource being accessed does not exist",
            "severity": "error"
        }
    ],
    "resourceType": "OperationOutcome"

 

This proves the SMART on fhir scopes are having the desired affect. I may look at how I can tidy up that message but it's nice and clear that the backend is doing what we wanted. 

I would like to be able to also pull this off with a JWT signed with the service account key which I think we will need in the future (for flexibly injecting scopes) but I'll post a separate post about how that goes (I'll have a few more questions no doubt). I'm assuming this repo is relevant for JWT generation for the Clould Healthcare API.

Marking this as solved. Thanks again.

 


Wow! It works!

Cool!

I would like to be able to also pull this off with a JWT signed with the service account key which I think we will need in the future (for flexibly injecting scopes) but I'll post a separate post about how that goes (I'll have a few more questions no doubt).

I guess you might want to use a different service account , for different clients, is that it? When you deploy an apigee API proxy using a service account, you're getting the API proxy to act as that service account. If you want to vary that identity depending on the client identity, then... yes, using distinct SA keys and mapping client IDs to a set of service account keys, will work for you. What I might do if I were using this - I'd store the SA keys in the GCP SecretManager, then use the SecretManager API (via ServiceCallout) to retrieve the secrets, then do the signing in the API proxy. So there'd be multiple service accounts. The proxy would run as one Service Account , and then that would have access to read the secrets in secret manager. And the SA's for which the keys are stored in Secret Manager would have access to Healthcare API.

Another way to apply different scopes to the call Apigee makes to Healthcare API is to map the client IDs to scopes; for example use a custom attribute on the CLIENT, or on the API Product, etc., to specify the value you will send in the X-Authorization-Scopes header. After the proxy calls VerifyApiKey or OAuthV2/VerifyAccessToken , then the value of that custom attribute is available in the message context (screencast here) And you can use the context variable as the thing to set into the X-Authorization-Scope header. like this:

 

<AssignMessage name="AM-AddScopesHeader">
  <Set>
    <!-- specify scopes as set in the custom attr on the API Product -->
    <Headers>
      <Header name="X-Authorization-Scope">{apiproduct.custom-attribute-name}</Header>
    </Headers>
  </Set>
  <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
</AssignMessage>

 

You can also set/store Custom attributes on the developer entity, on the App entity, and even on individual credentials within the app. They work the same way as shown in that screencast, but you'd retrieve them via different context variable names.

I'm assuming this repo is relevant for JWT generation for the Clould Healthcare API.

Yes it will.

Marking this as solved. Thanks again.

glad to be of help!