Skip to content
This repository has been archived by the owner on Jun 30, 2022. It is now read-only.

Latest commit

 

History

History
365 lines (282 loc) · 18.5 KB

README.md

File metadata and controls

365 lines (282 loc) · 18.5 KB

OBSOLETE

We've stopped using Mulesoft quite a while ago. This code is no longer maintained and has dependencies that are flagged as having multiple CVE's. It is now archived for historical purposes. Use at your own risk.

MuleSoft custom API gateway OAuth 2.0 scope enforcement policy

Contents

Introduction

This Mulesoft API Gateway custom policy validates OAuth 2.0 required scopes for each method & resource and optionally adds enterprise scope validation in which group memberships from the enterprise's authn/z system are intersected with granted scopes to create effective scope grants.

It is meant to be added after one of the OAuth 2.0 Access Token enforcement policies has already validated the Access Token and put the token information into the _agwTokenContext flowVar.

Installing the Policy

You must have API Manager privileges that allow you to install custom policies. Follow the Mulesoft instructions for adding custom policies, uploading the YAML and XML files.

Applying the Policy

In API Manager for your app, select Policies/Apply New Policy and search for this custom policy. You can narrow down the search by selecting Fulfills OAuth 2.0 scope enforcement.

alt-text

Note that this policy requires an upstream OAuth 2.0 protected policy to be installed first.

Configuring Enterprise Scope Validation

Provide the triggering scope and group attribute name. The triggering scope causes enterprise validation for the given attribute that is in the OAuth 2.0 validation result's access_token (_agwTokenContext). For example: auth-columbia:group is used when the auth-columbia scope is present and the access_token contains a group attribute that is a list of "OAuth groups" the user is a member of.

The first matching triggering scope is used and any other matching triggers will be ignored. At least one of the triggering scopes must be present in the Access Token's granted scopes. If no matching triggering scopes are present then a 403 Invalid Scope error will be returned.

This group list will be intersected with the OAuth 2.0 granted scope list to produce the effective scope list which will then be tested for scope enforcement.

Enterprise Scope Validation is a workaround for the inability of some (all) currently Mule-supported OAuth 2.0 service providers to provide a mechanism to alter the granted scopes.

As a special case, the auth-none scope explicitly identifies that enterprise scope validation is not required (e.g. when one of your resource's methods is OK with a Client Credentials grant). Note that your OAuth 2.0 server must have the auth-none scope defined for this to work.

Configuring resource and method required scopes

Paste a JSON policy document as produced by ramlpolicy.js (which parses the API's RAML file) or create one manually.

Configure the scope enforcement policy by adding method:resource keys and values that are the scopes you want to enforce for the given method:resource. In this example, we add get:/things with three alternative lists of required scopes and so on.

The method:resource name is similar to what the MuleSoft APIkit router uses for flow names.

If you have a common (set of) scope(s) that you want enforced you can repeat them here for each table entry, or list them in the prior OAuth 2.0 protected policy.

Configuring Method & Resource Conditions

As with most newer (Mule 3.8.1+) API Policies, you can limit this Policy to apply only to specific resources and methods. This is a level above this policy's own method:resource scope enforcement that simply identifies the methods and resources for which this entire policy is applied. Yes, this is confusing! If you don't understand this feature, just leave it set to "Apply configurations to all API methods & resources". That's probably what you want.

Example Configuration with Enterprise Validation

alt-text

This example configuration enables enterprise scope validation whenever one of the auth-columbia, auth-facebook or auth-google scopes is present by retrieving the group attribute, which happens to be the same attribute all three identity providers in our configuration.

The granted scopes in the Access Token are interesected with the scopes in the group list to become the effective scopes. If none of the auth-* is present in the granted scopes, the policy fails.

Generating the policy JSON document

The following method:resource scopes were generated by ramlpolicy.js based on parsing the application's RAML API document. Run it like this (you need node and npm):

$ npm install 
# get help
$ ./ramlpolicy.js -h 
Usage: ./ramlpolicy.js [raml URL] (default https://localhost:8082/v1/console/api/?raml)

# in AnyPoint Studio, run the app with default configuration:
$ ./ramlpolicy.js http://localhost:8081/console/api/?raml
{"get:/things/?":[["auth-columbia","read"],["auth-google","read"],["auth-none","read"]],"post:/things/?":[["auth-columbia","demo-netphone-admin","create"]],"get:/things/.+/?":[["read"]],"put:/things/.+/?":[["auth-columbia","update"]],"get:/things/.+/foo/?":[]}

Here's that same JSON pretty-printed:

{
    "post:/things/?": [
        [
            "auth-columbia", 
            "demo-netphone-admin", 
            "create"
        ]
    ], 
    "get:/things/.+/?": [
        [
            "read"
        ]
    ], 
    "get:/things/.+/foo/?": [], 
    "get:/things/?": [
        [
            "auth-columbia", 
            "read"
        ], 
        [
            "auth-google", 
            "read"
        ], 
        [
            "auth-none", 
            "read"
        ]
    ], 
    "put:/things/.+/?": [
        [
            "auth-columbia", 
            "update"
        ]
    ]
}

N.B. Due to a current bug in raml-json-enhance-node(0.2.6), a bunch of spurious errors mayb be logged to stderr. You can ignore these.

You'll note that a GET of /things will succeed if the effective scopes matching any of the mutually exclusive choices shown. The idea is to use the auth-* scope selectors which identify which identity provider is used by the Authorization Code grant. Also note that the "get:/things/.+" scope doesn't require any enterprise validation. That's allowed but is a non-sensical example.

If you are using a Client Credentials grant, there is no user identified and this policy will fail if enterprise scopes are configured unless you add the auth-none. This can be a good mechanism to require a given API to use Authorization Code grants.

Configuring Logging

To debug this Policy change the logging level to DEBUG in log4j2.xml:

<AsyncLogger name="org.mule.module.scripting.component.Scriptable" level="DEBUG"/>

Some example debugging log messages follow:

DEBUG 2017-08-23 16:43:48,295 [[demo-echo].throttling-task.01] org.mule.module.scripting.component.Scriptable: Policy 236386: Access token (granted) scopes: "create demo-netphone-admin read openid auth-columbia"
DEBUG 2017-08-23 16:43:48,295 [[demo-echo].throttling-task.01] org.mule.module.scripting.component.Scriptable: Policy 236386: Enterprise validation scope alternatives: "auth-columbia | auth-facebook"
DEBUG 2017-08-23 16:43:48,297 [[demo-echo].throttling-task.01] org.mule.module.scripting.component.Scriptable: Policy 236386: Enterprise groups: "read openid create update auth-columbia delete"
DEBUG 2017-08-23 16:43:48,297 [[demo-echo].throttling-task.01] org.mule.module.scripting.component.Scriptable: Policy 236386: Enterprise validation results in effective scopes: "read openid auth-columbia create"
DEBUG 2017-08-23 16:43:48,301 [[demo-echo].throttling-task.01] org.mule.module.scripting.component.Scriptable: Policy 236386: Found a match in the scopeMap for "post:/things" requiring scopes: "create demo-netphone-admin"
DEBUG 2017-08-23 16:43:48,302 [[demo-echo].throttling-task.01] org.mule.module.scripting.component.Scriptable: Policy 236386: Access Token does not have one or more of the required enterprise scopes: create demo-netphone-admin
WARN  2017-08-23 16:43:48,329 [[demo-echo].throttling-task.01] org.mule.api.processor.LoggerMessageProcessor: Policy 236386 invalid_scope: Access Token does not have one or more of the required enterprise scopes: create demo-netphone-admin

flowVars added

As a convenience, this policy also sets the following flowVars for use by subsequent modules in the flow:

scope: (effective) list of granted scopes

requiredScopes: list of required scopes configured by this policy for this method & resource

Errors

This policy throws the following errors and in general "fails closed" which prevents inadvertent unprotected access. Upon success it sets a 200 status and flow continues to the Mule app.

Status Error Code Error Description Possible causes
403 invalid_scope Access Token does not have the required [enterprise] scopes: %s Client app didn't request one or more of the listed scopes or enterprise scope validation removed a required scope
403 invalid_scope Access Token does not have any of the required enterprise scopes: %s Enterprise scope validation was configured and none of the configuration validation triggering scopes was requested/granted
403 invalid_scope required Access Token is missing Method & Resource Conditions are set incorrectly in the upstream OAuth 2.0 protected policy (flowVars._agwTokenContext is not defined)
503 policy_misconfigured Enterprise validation attribute (%s) was not found in Access Token The wrong attribute name was configured for the "OAuth group list"
503 policy_misconfigured request path (%s) has no required scopes: Configure Method & Resource conditions A method:request was specified with a blank required scope list (which is impossible to configure, so how'd you end up here?). If you want a method:resource that requires OAuth 2.0 protected but with no scopes, use a Method & Resource condition here. If you want a method:resource that doesn't require OAuth 2.0 protected at all, use a Method & Resource scope in that Policy's configuration.
503 policy_misconfigured request path (%s) doesn't match listener path (%s) impossible condition:-) Probably a bug in Policy code
503 policy_misconfigured unknown misconfiguration bug in Policy code

Why this custom policy is required

The standard policy scope list checking for the entire API (or some subset of resource-level regexps) is cumbersome to configure when different resource and methods require different scopes. One must apply the same resource-level policy over-and-over, customizing each time for different methods, resources and scopes. And, this doesn’t guarantee compliance with the RAML securedBy scopes nor offer a means of implementing alternative securitySchemes in the securedBy list. This policy does that all in one fell swoop based on parsing the RAML. It also adds the enterprise validation feature which, admittedly, is weird.

#%RAML 1.0
title: demo
version: v1
baseUri: https://demo.com/{version}
securitySchemes: 
  oauth_2_0: !include oauth2.raml

/things:
  get:
    securedBy:
      - oauth_2_0: { scopes: [ openid, auth-columbia, read ] }
  post:
    securedBy:
      - oauth_2_0: { scopes: [ openid, auth-columbia, create ] }
  /{id}:
	get:
	  securedBy:
		- oauth_2_0: { scopes: [ openid, auth-columbia, read ] }
	put:
	  securedBy:
		- oauth_2_0: { scopes: [ openid, auth-columbia, update ] }

In this example, GET /things requires the read scope and POST /things requires the create scope and PUT /things/{id} requires the update scope.

Policy Developer Notes

The following are notes for developers of this Policy.

The end game is to pass in a URL to the RAML description and then parse the RAML to find the securedBy and cache this into a map later. The current implementation develops that map offline and pastes it in to the configration.

There was some confusion around whether all the scopes in a securedBy are required for the the method to be allowed to execute. This has been clarified as being the case although the [RAML 1.0 spec](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md#applying-security-schemes language is (was) unclear on this. See this issue.

The RAML can be found at /console/api/?raml if the APIkit console is enabled or in the API Developer Portal (subject to permissions TBD).

Developer workflow:

  1. Run the API app in Anypoint Studio
  2. node ramlpolicy.js 2>/dev/null # ignore non-error errors
  3. Cut the output and paste into the policy configuration.

Example inbound scoped properties for PUT /v1/api/things/123 which belongs to the APIkit flow named put:/things/{id}:api-config:

http.listener.path=/v1/api/*
http.method=PUT
http.query.params=ParameterMap{[]}
http.query.string=
http.relative.path=/api/things/123
http.remote.address=/127.0.0.1:51589
http.request.path=/v1/api/things/123
http.request.uri=/v1/api/things/123
http.scheme=https
http.uri.params=ParameterMap{[id=[123]]}

Developing and Testing Mulesoft Custom Policies in Anypoint Studio

See https://docs.mulesoft.com/anypoint-studio/v/6/studio-policy-editor and https://docs.mulesoft.com/api-manager/creating-a-policy-walkthrough

However, it seems when testing a custom policy in Studio, other policies are no longer applied. This breaks things as we need the flowVars created by the upstream OAuth 2.0 protected policy.

Since it's hard to debug Python Code in the MuleSoft Jython world, use test.py as a simple test harness: Copy the configured Python code from the relevant XML file in ~/AnypointStudio/workspace/.mule/policies/ and append it to test.py. You can see the name of the applied policy in the Console log, for example:

INFO  2017-08-28 11:56:27,479 [agw-policy-notifier.01] com.mulesoft.module.policies.lifecyle.PolicyRegistryLifecycleManager: Policy 25246-236386.xml was correctly applied

Inconsistency across Mule implementations of OAuth 2.0

MuleSoft has three documented implementations of OAuth 2.0 token validation and there are inconsistencies with how the result of a successful validation is made available to the downstream API app:

Additional observed, but undocumented, flowVars:

The deprecated API Gateway 2.0.2 release notes documents flowVars[_agwTokenContext] which on inspection is a String containing the returned JSON map. This flowVar is critical as it contains the granted scope list, among other things.

I've also seen these flowVars:

  • _client_id ID of the registered client app
  • _client_name name of the registered client app

So, I conclude that the “standard” is what’s used for OpenAM and PingFederate. If you are using some other OAuth 2.0 token validation policy, this custom policy will break if _agwTokenContext is not present.

TODO

  • consider refactoring Python into native Mule code
  • Caching of method:url decision for performance
  • add "late binding" of enterprise scopes via LDAP query (or similar) rather than carrying forward the (stale) groups that were valid at the initial authorization code grant.

Author

Alan Crosswell

Copyright (c) 2017 The Trustees of Columbia University in the City of New York

LICENSE

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.