Skip to content

Security Use Client

mdii edited this page Nov 25, 2014 · 53 revisions

OpenIoT security client can be used in web applications that provide a user interface and interact directly with users. It can also be used in intermediate service providers that interact with other components through WebServices or REST calls. This module provides authentication and access control utilities.

Maven Dependencies

The first step in using the authentication & authorization client is to add the following dependencies to your pom file:

<dependency>
    <groupId>org.openiot</groupId>
    <artifactId>security.client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.2.2</version>
    <scope>compile</scope>
</dependency>

We use Apache Shiro for authentication & authorization. buji-pac4j adds support for OAuth2.0 (using pac4j library) authentication to Apache Shiro.

Usage for Web Applications

If you are developing a web application that directly interacts with users, you can use the utilities that are provided by the OpenIoT security client.

Shiro Webapp Configuration

In order to initialize Shiro in a web application, add the following XML chunk to your web.xml:

<context-param>
    <param-name>shiroEnvironmentClass</param-name>
    <param-value>org.openiot.security.client.CustomIniWebEnvironment</param-value>
</context-param>
<context-param>
    <param-name>module-name</param-name>
    <param-value>MODULE_NAME</param-value>
</context-param>
<filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher> 
    <dispatcher>FORWARD</dispatcher> 
    <dispatcher>INCLUDE</dispatcher> 
    <dispatcher>ERROR</dispatcher>
</filter-mapping>
<listener>
    <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>

Note that in web.xml all other configuration places, MODULE_NAME should be replaced by the actual name you give to your module. For configuring Shiro, we can create a file named web-client.ini with the following example contents, taken from the security management console, and place it into the classpath (e.g. in src/main/resources directory). The recommended approach is however, to add the configuration to the global security-config.ini in the JBoss configuration directory. In the global security-config.ini, the configuration content must be placed between the following delimiters:

  • @BEGIN-MODULE_NAME
  • @END-MODULE_NAME

In fact, if security-config.ini is not found or the configuration content for the module is not found in security-config.ini, the security client will try to find a web-client.ini file in the classpath of your module.

[main]
casOauthClient=org.pac4j.oauth.client.CasOAuthWrapperClient
casOauthClient.casOAuthUrl=https://localhost:8443/openiot-cas/oauth2.0
casOauthClient.key=openiot-security-manager-app
casOauthClient.secret=openiot-security-manager-app-secret

# Sets the callbackUrl for each client in the list
clients = org.pac4j.core.client.Clients
clients.callbackUrl = http://localhost:8080/security.management/callback
clients.clientsList = $casOauthClient

clientsFilter = org.openiot.security.client.CasOAuthClientFilter
clientsFilter.clients = $clients
clientsFilter.failureUrl = /error.xhtml

casOauthRoles = io.buji.pac4j.filter.ClientRolesAuthorizationFilter
casOauthRoles.client = $casOauthClient

casOauthUsers = io.buji.pac4j.filter.ClientUserFilter
casOauthUsers.client = $casOauthClient

clientsRealm = org.openiot.security.client.CasOAuthClientRealm
clientsRealm.permissionsURL = https://localhost:8443/openiot-cas/oauth2.0/permissions
clientsRealm.defaultRoles = ROLE_USER
clientsRealm.clients = $clients

# Using a shiro native session manager instead of the Servlet container's sessions
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
# 1,800,000 milliseconds = 30 seconds
sessionManager.globalSessionTimeout=1800000
securityManager.sessionManager = $sessionManager

[roles]

[urls]

# NOTE: Order matters! The first match wins.

/logout = logout
/callback = clientsFilter
/signup.xhtml = anon
/error.xhtml = anon
/index.* = anon
/index = anon
/home.* = anon
/home = anon
/javax.faces.*/** = anon
/?*/** = casOauthRoles[ROLE_USER]
  • casOauthClient.casOAuthUrl property specifies the address of the OAuth provider to which users will be redirected for authentication.
  • casOauthClient.key designates the clientID of the web client and casOauthClient.secret specifies its secret.
  • clients.callbackUrl is the URL that is called back by CAS server. It consists of the URL of your application followed by /callback.
  • clientsRealm.permissionsURL is the URL on the OAuth provider for retrieving permissions of the authenticated user. If the OpenIoT CAS is deployed in a different place, this URL must be updated accordingly.
  • sessionManager is responsible for managing application sessions and must be set according to the above example ini config file.

In the [urls] section, we can define protection rules using URL patterns. In the sample configuration file, /signup.xhtml, /index.*, /javax.faces.*/**, and /home.* are configured to be accessible to users without requiring them to log in. /?*/** = casOauthRoles[ROLE_USER] signifies that all other resources are protected and are accessible only to authenticated users. If the user has not been logged in yet, he/she will be redirected to OpenIoT CAS login page. /logout = logout and /callback = clientsFilter must be left as they are to provide logout capability and OAuth authentication. For more information on configuring Shiro please refer to its official documents.

Manual Redirection

Using access control rules in the [urls] section of Shiro configuration file is the easiest way to handle access to different resources of the web application. However, this can also be performed manually, for example in a Servlet. The following code snippet in a servlet checks whether or not the user has already been authenticated. If not, it redirects the user to the login page (in our case, it redirects the user to the login page on the OpenIoT CAS).

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.openiot.security.client.AccessControlUtil;
...
Subject subject = SecurityUtils.getSubject();
	if (!subject.isAuthenticated())
		AccessControlUtil.getInstance().redirectToLogin(req, resp);

Checking for Permissions and Roles

Having made sure that the user has been authenticated, it is straightforward to check if the user has a role or permission for the given service (e.g., current web application) using org.openiot.security.client.AccessControlUtil utility class:

  1. hasPermission(String perm): checks whether or not the user has the given permission
  2. hasRole(String role): checks whether or not the user has the given role

In order to use the utility methods provided by AccessControlUtil first we need to obtain a reference to its (singleton) object using AccessControlUtil.getInstance() method. Then you can call the desired methods on this reference as shown in the following example:

...
for (RegisteredService registeredService : services) {
    String name = registeredService.getName();
		
    // Checking access
    if (AccessControlUtil.getInstance().hasPermission("admin:user_mgmt:" + name))
        allServices.put(registeredService.getId(), registeredService);
    }
...

AccessControlUtil.getInstance().getOAuthorizationCredentials() returns an instance of OAuthorizationCredentials that contains the userId and accessToken of the authenticated user, and the clientId of the web application.

Logging out

For logging out the user, simply use the URL BASE_APP_URL/logout, where BASE_APP_URL is the root URL of your web application. This will invalidate the user session managed by Shiro, but does not invalidate the CAS token. Furthermore, Logging out of CAS does not invalidate the current user session managed by Shiro. Therefore, it is necessary to always explicitly log out the user by using the logout URL.

Tag libraries

A set of JSP and JSF tag libraries are provided to facilitate using authentication and authorization in OpenIoT applications. We explain how to use JSF taglib here. Using JSP taglib is similar.

In order to be able to use JSF taglib in an XHTML page, we need to declare the openiot namespace by adding xmlns:openiot="http://openiot.org/tags" to the html tag:

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:p="http://primefaces.org/ui"
xmlns:f="http://java.sun.com/jsf/core" xmlns:openiot="http://openiot.org/tags">

The available tags are the following:

  • <openiot:hasPermission name="PERM_NAME"> renders the tag body if the user has the permission specified by PERM_NAME
  • <openiot:lacksPermission name="PERM_NAME"> does not render the tag body if the user lacks the permission specified by PERM_NAME
  • <openiot:hasRole name="ROLE_NAME"> renders the tag body if the user has the role specified by ROLE_NAME
  • <openiot:laksRole name="ROLE_NAME"> does not render the tag body if the user lacks the role specified by ROLE_NAME
  • <openiot:hasAnyRoles name="ROLE_NAME1, ROLE_NAME2, ..."> renders the tag body if the user has at least one of the roles specified by ROLE_NAME1, ROLE_NAME2, etc.

The security-client module also provides JSF tags for Apache Shiro (shiro-faces). In order to use these tags in your XHML document, declare Shiro's namespace in your documents by adding xmlns:shiro="http://shiro.apache.org/tags" to the html tag. The following tags can be used in your JSF pages (other Shiro tags are not recommended to be used):

  • <shiro:user> renders the tag body if the user is authenticated
  • <shiro:guest> renders the tag body if the user is not authenticated
  • <shiro:principal type="org.pac4j.oauth.profile.casoauthwrapper.CasOAuthWrapperProfile" property="id"/> displays the username of the user

Sample Web Application using Security&Privacy module

The OpenIoT security management console uses security-client module for authentication and authorization. It is a good example of how to use this module in your application. Information regarding how to install the management console can be found here.

RESTful Usage

Configuration

In order to use the security client, first obtain an instance of org.openiot.security.client.AccessControlUtil configured for RESTful authentication and authorization. This can be done by calling one of the following methods:

  1. AccessControlUtil.getRestInstance("module-name", "config-dir")
  2. AccessControlUtil.getRestInstance("module-name")
  3. AccessControlUtil.getRestInstance()

The configuration content is located by the security client as follows:

  1. If the method that only takes "module-name" as parameter is used, then the security module first looks for security configuration content between delimiters @BEGIN-MODULE_NAME and @END-MODULE_NAME in the global security-config.ini file in the JBoss configuration directory. "module-name" is the name of the module that is using the security client. MODULE_NAME refers to the same name. If the configuration content is not found, the security client falls back to rest-client.ini in the classpath.
  2. If a different configuration directory is specified by passing the "config-dir" parameter to the method, the security module looks for a file named rest-client.ini in the specified directory. If the configuration content is not found, the security client falls back to rest-client.ini in the classpath.
  3. If the method with no parameter is called for obtaining an instance of the AccessControlUtil class, then the security module looks for a file named rest-client.ini in the classpath.

The configuration content should be similar to the following example. The values must be modified to conform to the setting of your module.

[main]

casOauthClient=org.openiot.security.client.rest.CasOAuthWrapperClientRest
casOauthClient.casOAuthUrl=https://localhost:8443/openiot-cas/oauth2.0
casOauthClient.casOAuthRestUrl=https://localhost:8443/openiot-cas/openiot1/tickets
casOauthClient.key=testservice1
casOauthClient.secret=testsecret1

# Sets the callbackUrl for each client in the list
clients = org.pac4j.core.client.Clients
clients.callbackUrl = http://localhost:8080/sth/callback
clients.clientsList = $casOauthClient

clientsRealm = org.openiot.security.client.rest.CasOAuthClientRealmRest
clientsRealm.permissionsURL = https://localhost:8443/openiot-cas/oauth2.0/permissions
clientsRealm.defaultRoles = ROLE_USER
clientsRealm.clients = $clients

In this configuration file, the following parameters are to be modified by the module developers if needed:

  • casOauthClient.casOAuthRestUrl property specifies the address of the OAuth provider for RESTful authentication. If the OpenIoT CAS is deployed in a different place, this URL must be updated accordingly.
  • casOauthClient.key designates the clientID of the client and casOauthClient.secret specifies its secret.
  • clientsRealm.permissionsURL is the URL on the OAuth provider for retrieving permissions of the authenticated user. If the OpenIoT CAS is deployed in a different place, this URL must be updated accordingly.

SSL Troubleshooting

If the security-server (openiot-cas) is deployed on a host other than the localhost and the server has a self-signed certificate, you might get the following error:

javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated

In this case you need to add the server's certificate to the list of trusted certificates of the JVM. One way of doing that is by performing the following steps:

  • echo -n | openssl s_client -connect SERVER_ADDRESS:8443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > SERVER_ADDRESS.cert
  • keytool -importcert -alias "SERVER_ADDRESS" -file SERVER_ADDRESS.cert -keystore JRE_PATH/lib/security/jssecacerts

Replace SERVER_ADDRESS with the address of the server and JRE_PATH with the path of your JRE. If jssecacerts does not exist, use cacerts instead.

Authentication and Authorization

Having obtained, for example in the variable restInstance, an instance of org.openiot.security.client.AccessControlUtil configured for RESTful authentication and authorization, the following functionality is accessible.

  • For logging in and obtaining a token, use restInstance.login() method and provide your username and password.
  • Use restInstance.getOAuthorizationCredentials() to obtain an OAuthorizationCredentials object containing the token, clientId, userId, and the full resource URI of userId.
  • For logging out and destroying the token, use restInstance.logout().

After having made sure that your application has been authenticated, it is straightforward to check if a user has a role or permission for your application or another service.

Assume that your OpenIoT application (e.g., SD&UM) is called A and has the clientId serviceA and token tokenA. A web application (e.g., Request-Presentation) called C with clientId serviceC requests one of your services on behalf of user U. User U has been authenticated. Token tokenU has been assigned to C (because in a web application, the token is given to the application instead of the user). For fulfilling the C's request you need to check if U is permitted to access the requested resource(s) on your application. You can write a similar code as the following:

String permissionString = ...
AccessControlUtil instance = AccessControlUtil.getRestInstance();
OAuthorizationCredentials callerCredentials = new OAuthorizationCredentials(tokenU, serviceC, null);
OAuthorizationCredentials credentials = new OAuthorizationCredentials(tokenA, serviceA, callerCredentials);
if(instance.hasPermission(permissionString, credentials)){
    // Proceed with fulfilling the request
} else {
    // Deny fulfilling the request
}

Now, assume that C is requesting a service from an OpenIoT service provider B (e.g., LSM), having clientId serviceB and token tokenB. For fulfilling this request, B needs to use a service from your application. For fulfilling the B's request you need to check if U is permitted to access the requested resource(s) on your application. You can write a similar code as the following:

String permissionString = ...
AccessControlUtil instance = AccessControlUtil.getRestInstance();
OAuthorizationCredentials userCredentials = new OAuthorizationCredentials(tokenU, serviceC, null);
OAuthorizationCredentials callerCredentials = new OAuthorizationCredentials(tokenB, serviceB, userCredentials);
OAuthorizationCredentials credentials = new OAuthorizationCredentials(tokenA, serviceA, callerCredentials);
if(instance.hasPermission(permissionString, credentials)){
    // Proceed with fulfilling the request
} else {
    // Deny fulfilling the request
}

Notes

  • The same procedure can be used for testing if a user has a specific role, using AccessControlUtil.hasRole(..) methods.
  • OAuthorizationCredentials allows more than two callers to be chained, but this must be avoided. if the chain contains two credentials (including the credentials of the current application), the second credentials is used as user's credentials. If the chain contains three credentials (including the credentials of the current application), the third credentials is used as user's credentials. If the chain contains more than three credentials, the rest are ignored.
  • In cases where you need to check if a user is authorized to access some resources on another OpenIoT service rather than your application, there are variants of the above methods that take a targetClientId parameter. Your application is allowed to do this only if it has the necessary permission on the target OpenIoT service provider denoted by targetClientId. By default this permission is called "ext:retrieve_permissions".

Sample Restful usage

lsm-light.server uses the restful authentication and authorization provided by the security client module for verifying if the incoming requests have the sufficient permissions to be fulfilled. lsm-light.server uses the SecurityUtil class for checking permissions.

Cache Configuration

In order to reduce the traffic between your application and the OpenIoT CAS, the security client can cache the authorization information it retrieves from OpenIoT CAS. In order to activate caching you need to add the following in the Shiro configuration file (web-client[-MODULE_NAME].ini or rest-client[-MODULE_NAME].ini).

cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
cacheManager.cacheManagerConfigFile=classpath:ehcache-sec-client.xml
securityManager.cacheManager = $cacheManager

We use Ehcache cache provider and cacheManager.cacheManagerConfigFile specifies the configuration file. For example in the above configuration, Shiro will look for a file named ehcache-sec-client.xml in the classpath. A sample configuration file can be found in security-client/src/test/resources. Instead of classpath: prefix you can use url:, file: or simply provide a valid path to the config file without any prefix. In Ehcache config file you can change the default values in defaultCache element. Particularly, tune timeToIdleSeconds and timeToLiveSeconds parameters.

Great attention must be given to using caching, otherwise you might allow permissions to users while their tokens have been expired or they don't have those permissions anymore. You can clean the whole cache by calling AccessControlUtil.getAuthorizationManager().clearCache(null). The authorization information for the current user can be removed from the cache by calling AccessControlUtil.reset().

Token Expiry

Since tokens are represented only by a string, the only way to find out if a token has been expired is to contact the OpenIoT CAS by calling hasPermission() or hasRole() and check for an exception of type AccessTokenExpiredException. Since this exception is a runtime exception, you don't need to surround every call to hasPermission() or hasRole() methods by try-catch blocks. In a web application you can instruct your container to forward the user to a specific page when this exception occurs. AccessTokenExpiredException.getToken() returns the token that is expired.

The above approach is not reliable if we enable caching. AccessControlUtil also provides the method getExpiredAccessToken(OAuthorizationCredentials credentials). This method returns the first expired token in credentials, if there is any. Otherwise, it returns null.

Clone this wiki locally