Context Standard Plugin: SimpleSecurityPlugin
The SimpleSecurityPlugin is a standard cfcommons plugin that provides sophisticated yet easy to configure and manage security for both Pluggable and HTTP Contexts. It integrates seamlessly with the SimpleMVCPlugin . This plugin supports two types of security applications, Request Based HTTP Security and Class Based Context Security.
Request Based HTTP Security
The SimpleSecurityPlugin is an HTTPPlugin, and as such can operate within an HTTPContext. Because HTTPPlugins are ordered - meaning the order in which they are configured within an HTTPContext is meaningful, it's important to have this plugin declared as one of the first HTTP Plugins in your context (especially if you're working with the SimpleMVCPlugin ). The reason for this is because this plugin provides a simple yet comprehensive mechanism for filtering HTTP Requests - ensuring that only the right requests are serviced by any subsequent HTTPPlugins within the context.
This plugin also provides context class security so it is not a requirement that it operate within an HTTP Context - you can use it within a simple PluggableContext - but of course, the HTTP functionality will be ignored.
Plugin Configuration
There are a handful of configuration options that the SimpleSecurityPlugin supports if you're going to employ the Plugin to secure an HTTPContext-based web application. This configuration is done via an XML file. However, if you are simply using this plugin to secure context class methods, then no configuration is required;
And you're done - you can now start using the @cfcommons:security:secure annotation on any method in the context (I detail this in the class-based context security section below). As always, the above code will recreate a context on every page request which is not necessary. Do your due diligence and cache the context in a manner that best suits you.
XML Config
If you want to establish HTTP request-based security, then you'll need to configure the plugin with an XML file. This properly structured XML config file is provided to the plugin in its init() constructor. Below are the details of the XML configuration file and each configurable option.
Context Config
Once you have your xml configuration file declared, you can now register the plugin with your context per the standard plugin configuration, providing the path of the security configuration file as the only plugin constructor argument;
Security Configuration Details
<logout />
Required attributes;
- REDIRECT: Upon successful logout, the plugin with perform a redirect to the current template and append the value of this tag as PATH_INFO, e.g.; http://localhost/index.cfm/logout/redirect.
Optional attributes;
- URL: The PATH_INFO value that must be present in the URL that will cause the plugin to perform the logout process. If this attribute is not present, the plugin will look for /cfcommons_security_logout. The presence of the configured URL or the default one will ultimately cause the invocation of the logout() method of the configured SecurityProvider implementation. In the cases where no SecurityProvider has been configured, the DefaultSecurityProvider will be used whose logout() method implements the native ColdFusion security frameworks <cflogout /> tag.
<login />
Required attributes;
- REDIRECT: Upon successful login, the plugin with perform a redirect to the current template and append the value of this tag as PATH_INFO, e.g.; http://localhost/index.cfm/logout/redirect
Optional attributes;
- URL: The PATH_INFO value that must be present in the URL that will cause the plugin to perform the login process. If this attribute is not present, the plugin will look for /cfcommons_security_login. The presense of the configured URL or the default one will ultimately cause the invocation of the login() method of the configured SecurityProvider implementation, passing in the values of the username and password form post keys. If login is unsuccessful, an exception of type SecurityException is thrown.
- TEMPLATE: The relative path to a .cfm file containing a custom login form. This form must provide form keys for both cfcommons_security_username and cfcommons_security_password IF the DefaultSecurityProvider is being used. If this attribute is not present, a default login form will be automatically provided.
<security-provider />
This tag is entirely optional and allows you to configure your own security implementation. When this tag is not present, the org.cfcommons.context.standardplugins.security.impl.DefaultSecurityProvider class is used. This class provides a simple implementation of the org.cfcommons.context.standardplugins.security.SecurityProvider interface. We discuss this in greater detail further down the page.
You're going to likely want to provide your own implementation SecurityProvider of this interface for the plugin to use because the DefaultSecurityProvider is only aware of configured dummy-users, not any actual user information from a database, LDAP directory server or other persistent user store.
<requests />
This is the parent tag for <request /> tag nodes. No attributes.
<request />
Required attributes;
- URL: The URL for which to perform a security check. This is the value of the requests PATH_INFO. This value can contain any valid regular expression.
- EXPRESSION: The expression that the SecurityProvider will use to determine whether or not access should be granted to the requested resource. This expression must evaluate to either true or false. This value will get passed to the secure() method of the current SecurityProvider implementation as an argument named expression. If the expression evaluates to false, then an exception of type SecurityException will be thrown.
Optional attributes;
- HTTPMETHOD: Can contain a comma-delimited list of any number of valid HTTP request methods, e.g., POST,GET,PUT,DELETE,TRACE,OPTIONS. If this attribute is present, then the plugin will only attempt to secure a request if both the URL and the current HTTP request method match the configuration. If this attribute is not present, then http method will not be taken into account when securing a request.
<dummy-users />
Parent tag to <user /> tags. No attributes.
<user />
Required attributes;
- NAME: The username for this current dummy-user. This value must exist within the form post cfcommons_security_username key for valid login request.
- PASSWORD: User in concert with the NAME value to authenticate a login request for a test user. The value of this attribute must be a MD5 hash representation of an actual password value.
- KEYS: A list of possible security keys - when the DefaultSecurityProvider is employed, these keys represent actual ROLES for use within the default ColdFusion application security framework.
Class-Based Context Security
Class or Component based security is a security mechanism that is focused on context object methods instead of the HTTP request response model. While this is accomplished through AOP, it is through annotations that security is applied seamlessly, without any interference to the underlying object and method.
The SimpleSecurityPlugin class-based security API is comprised entirely of a single annotation, @cfcommons:security:secure. This annotation can be applied to any method on any context Class to effectively apply security to the invocation of that method. The only caveat is that any context Plugin that may want an instance of a context class, needs to ask for the instance of that class through the centralized object factory via getObjectInstance(). When the context is responsible for creating the object instance (and of course, the SimpleSecurityPlugin is a member of the context) then you'll get an object that has security correctly applied. Below is an example;
The value of the annotation, which in the above example is isUserLoggedIn(), is the security expression that will be handed off to the secure() method of the configured SecurityProvider as an argument named expression. Just as with HTTP Request security (detailed above), this expression must evaluate to true. If not, then an exception of type SecurityException will be thrown.
Data Binding Security
The primary advantage of class based security is that the configured SecurityProvider is applied to secured method as a cross-cutting-concern (AOP "before" Aspect). This means that any argument data bound method arguments at runtime can be referenced within the security expression itself. This goes for simple and complex types alike. For example;
In the above example, the security expression is directly referencing the boolean argument flag in order to make it's decision. If the value of flag is true, then the method can be invoked / accessed - otherwise an exception will be thrown. Same is true for complex types;
Now we're getting crazy. Not only are we utilizing the state of the complex MyObject type (in which we of course are assuming exposes a someMethod() function that returns an integer), but we've created a composite expression relying upon the isUserLoggedIn() native CF security framework method to produce a boolean result.
Using Strings and Comparison Operators
When working with class-based context security and it's associated annotation, there are a few considerations to keep in mind when constructing your security expressions;
- Demarcate strings with colons - Due to a bug with the getMetaData() and getComponentMetaData() CF methods, expressions ending with a quoted string aren't parsed correctly. As an example; @cfcommons:security:secure arguments.inputText eq "hello world". While this expression looks valid, it actually with throw a cryptic exception. In order to get around this current limitation, the security plugin will allow you to substitute colons for quotes. In order to make that same expression work, reformat it to do this; @cfcommons:security:secure arguments.inputText eq :hello world:. This provides a functionaly equivalent expression as the one before it.
- Avoid using ! && || == - While again, this syntax is very valid CF code, it is not very well supported by evaluate(), in order to avoid problems, simply substitute those comparison operators with the older, supported AND, OR, XOR, NOT operators within the security annotation.
SecurityProvider In Depth
Regardless as to whether or not you're employing the security plugin for context CFC security or as an HTTP request filter, your SecurityProvider drives how security decisions are made. SecurityProvider is an interface and when you don't declare your own, the framework uses a default one called DefaultSecurityProvider. The DefaultSecurityProvider provides, as it's name implies, a default implementation of the SecurityProvider interface. That interface looks like this;
Whenever you configure a secured HTTP request in your XML configuration file, or whenever you secure a CFC method with the @cfcommons:security:secure annotation, the SecurityProvider implementation is put to use to make those security decisions.
The purpose of the SecurityProvider interface is to abstract the details of how security is enforced from the implementation of your application. This provides a very flexible platform where security implementations can be swapped out whenever needed without requiring any changes to your application codebase.
In order to ensure this loosely-coupled approach remains intact, you should avoid directly referencing the native CF security framework within security expressions like so;
The above expression-based security annotation is perfectly valid, and will work as expected with the DefaultSecurityProvider, but what you've done here is create a direct dependency upon the ColdFusion security framework by referencing the isUserLoggedIn() method. It would be much, much better for you to simply invoke the SecurityProvider isLoggedIn() method instead;
Because security expressions are handed off to the secure() method of the SecurityProvider, you can reference any of those class methods within your security expressions. By using the isLoggedIn() method of the SecurityProvider, you've ensured that you can swap out the actual implementation of SecurityProvider without any impact to your application.
Along those same lines, you should also avoid using isUserInRole('SomeRole') and isUserInAnyRole('SomeRole,OtherRole') and instead use the hasKeys() method of the SecurityProvider;
To provide your own custom security implementation, all that is needed is for you to create a CFC that properly implements the SecurityProvider interface. Once that is done, simply tell the Plugin about it by declaring it in your security configuration file like this;