IdP Authorization and Enforcement

Background

The IdP includes a generic mechanism for building and enforcing authorization rules, the “context-check” interceptor, which runs after authentication and attribute resolution/filtering, but before completing a request. The interceptor can examine the state of the request and arbitrarily signal custom events as desired to halt the normal processing flow and dispatch to the error handling features in the software.

What is less obvious is the best way to leverage this feature, in particular to make it more agile. The configuration of the interceptor is, like all flow configuration, not reloadable. You have to restart the IdP to apply changes. This is obviously not ideal. Since both the attribute resolver service, metadata, and the views are generally reloadable, it follows that it is advantageous to design the use the interceptor in such a way that all of the actual rules are located in one of those places. There are many ways to do this; this page just illustrates some of them.

A really interesting extension of all this would be expressing attribute names and values to check for in the metadata itself using a tag.

Use of the Attribute Resolver

This is a common thread in the IdP: any time it makes a decision about something by running a condition of some sort, that condition will usually have access to any IdPAttributes resolved for the subject (both filtered and unfiltered). As a result, it’s a good pattern to use the attribute resolver wherever possible to produce information to feed those decisions because the resolver is very flexible, very robust, and is fully reloadable so can be altered at runtime.

With respect to authorization, a good way to do this is to define an AttributeDefinition (often via scripting, though obviously that’s not required) that reflects the result of applying the desired business rules to the request. There are obviously lots of ways to do this, but a couple of patterns I find useful:

  • Resolve a value of “1” or “true” to indicate that the request should be blocked. If not, no values are resolved and the attribute won’t exist at all. This makes checking the result very simple and less likely to fail through misconfiguration or struggles with Javascript.

  • More advanced, resolve the custom event string to signal later, which opens up the possibility of producing different outcomes that lead to different error views or at least different output. Again, ideally only resolve one value, and if none apply, nothing is resolved.

Examples

A simple example I use that is fairly brute force involves a scripted attribute definition that depends on the resolution of eduPersonEntitlement, which in our case typically is computed from a combination of sources, principally our directory and Grouper. The script is also designed conservatively to fail closed, by assuming that authorization is denied and then flipping it to allowed only after the logic actually completes. In practice this wouldn’t fail, but the principle is ueful.

It would be a simple enhancement to apply an activation condition to the attribute definition using the relyingParties attribute or perhaps some more generic mechanism based on a metadata tag. The longer the script or the more RPs that require rules, the more likely that would be needed.

This example produces an attribute named “contextCheckDenied” (or nothing).

var block = true; var rpid = resolutionContext.getAttributeRecipientID(); function checkAttributeValue(attr, value) { if (value != null) { return typeof attr != "undefined" && attr != null && attr.getValues() != null && attr.getValues().contains(value); } else if (typeof attr != "undefined" && attr != null && attr.getValues() != null && !attr.getValues().isEmpty()) { var iter = attr.getValues().iterator(); while (iter.hasNext()) { if (iter.next().startsWith(rpid)) { return true; } } } return false; } if (rpid == null) { // This applies to internal IdP cases like admin flows. block = false; } else if (rpid.equals("https://ohio-state.slack.com")) { block = !checkAttributeValue(eduPersonEntitlement, "https://ohio-state.slack.com/users"); } else if (rpid.equals("https://osu.zoom.us")) { block = !checkAttributeValue(eduPersonEntitlement, "https://osu.zoom.us/users"); } else if (rpid.equals("https://app.biorender.com")) { block = !checkAttributeValue(eduPersonEntitlement, "https://app.biorender.com/users"); } else { // Most systems are not regulated by this script. block = false; } if (block) { contextCheckDenied.getValues().add("1"); }

A similar example could resolve different values based on each service, such as “SlackDenied”, “ZoomDenied”, etc., which would in turn make it somewhat simpler later to construct customized error responses.

Context Check Configuration

Because all the hard work is done in the resolver, the logic to configure in conf/intercept/context-check-intercept-config.xml is more limited (and should be fairly static):

<bean id="osu.BlockCondition" parent="shibboleth.Conditions.SimpleAttribute" p:useUnfilteredAttributes="true"> <property name="attributeValueMap"> <map> <entry key="contextCheckDenied"> <list> <value>1</value> </list> </entry> </map> </property> </bean> <bean id="shibboleth.context-check.Function" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript" p:customObject-ref="osu.BlockCondition"> <constructor-arg name="scriptSource"> <value> <![CDATA[ var event = "proceed"; if (custom.test(input)) { event = "ContextCheckDenied"; } event; ]]> </value> </constructor-arg> </bean>

If I had used the resolver to actually generate the events to signal, I wouldn’t bother with the condition at all and just write a script that checks for a value in a candidate attribute and sets event to that value.

var event = "proceed"; var rpctx = input.getSubcontext("net.shibboleth.idp.profile.context.RelyingPartyContext"); var attributeCtx = rpCtx.getSubcontext("net.shibboleth.idp.attribute.context.AttributeContext"); if (attributeCtx != null) { var attribute = attributeCtx.getUnfilteredIdPAttributes().get("contextCheckEvent"); if (attribute != null && !attribute.getValues().isEmpty()) { event = attribute.getValues().get(0).getValue(); } } event;

Error Handling

The last piece is error handling. In most cases, my goal is to force system owners that insist on the IdP doing this to provide a page to send people to rather than requiring the IdP to host something. This is obviously not always successful but most people prefer to control that content anyway.

In the simple case where a single event is used, any specific logic pertaining to an SP is going to be handled by the error view itself, so again is reloadable.

The part that is baked into the IdP is to get it to handle the custom event by routing it to a local error view template. In my case, the “ContextCheckDenied” event is actually already known to the IdP, so I don’t have to define it per se, only handle the dispatching via conf/errors.xml:

Both of these maps may contain many entries. The “true” flag in the second one signifies that the IdP should record an audit log entry for any requests that finish with that event.

The views/access-denied.vm template is something like this (I am skipping a lot of the local look and feel material to just focus on the important parts):

Another enhancement to this would be to resolve an attribute containing the proper redirection page or message (or both) and simply extract that attribute’s value to encode into the page.