Proposal : Resource Owner Initiated Delegation OAuth 2.0 Grant Type

Please see my previous blog post for more context on the $subject. After that, I was pointed to have a look at UMA. I've been following UMA for sometime and it once again builds a very useful/realistic set of use cases on top of OAuth 2.0. But still - it too, Client initiated or Requester initiated.

I believe my use case can be addressed in OAuth 2.0 it self with a new grant type or in UMA.

Let me break down this in to four phases.

1. Client registers with the Resource Server via Authorization Server
2. Resource Owner grants access to a selected Client.
3. Client gets access_token from the Authorization Server.
4. Client accesses the protected resource on behalf of the Resource Owner.

Let's dig deep.

1. Client registers with the Resource Server via Authorization Server.

This is the a normal OAuth request with any of the defined grant types with the scope resource_owner_initiated.

At the time of Client registration on the Resource Server, Client will be redirected to his Authorization Server.

In return Resource Server gets an access_token along with the client_id. Now Resource Server has a set of registered Clients with corresponding access_tokens.

2. Resource Owner grants access to a selected Client.

Resource Owner logs in to the Resource Server and he can see all registered Clients with the Resource Server.

Resource Owner picks a Client.

Let's say the client_id we got was Foo from Step-1, along with the access_token.

Now,  the Resource Server will redirect the Resource Owner to the Authorization Server.

GET /authorize? grant_type=resource_owner_initiated&client_id=Foo&scope=Bar
Host: server.example.com

If the above is successful then the Authorization Server will send the following to the Resource Server.

HTTP/1.1 200 OK

After this step completed, Authorization Server will create an access_token for the Client - to access the a Protected Resource on behalf of the Resource Owner .

3. Client gets access_token from the Authorization Server.

Client should be able to get all the resource owner delegated access_tokens for him or a single one by specifying the resource owner Id.

To get all the delegated access_tokens - from the Client to the Authorization Server.

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic XfhgFgdsdsdkhewe
Content-Type: application/x-www-form-urlencoded

grant_type=resource_owner_initiated

Response from the Authorization Server to the Client.

HTTP/1.1 200 OKContent-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
   "delegated_tokens" : [
      {  "access_token":"2YotnFZFEjr1zCsicMWpAA",
          "token_type":"example",
          "expires_in":3600,
          "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
          "resource_owner_id":"Bob"
      },
     {  "access_token":"2YotnFZFEjr1zCsicMWpAA",
          "token_type":"example",
          "expires_in":3600,
          "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
          "resource_owner_id":"Tom"   
      } 
    ]
}

To get a single delegated access_token - from the Client to the Authorization Server.

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic XfhgFgdsdsdkhewe
Content-Type: application/x-www-form-urlencoded

grant_type=resource_owner_initiated&resource_owner_id=Bob

Response from the Authorization Server to the Client.

HTTP/1.1 200 OKContent-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
          "access_token":"2YotnFZFEjr1zCsicMWpAA",
          "token_type":"example",
          "expires_in":3600,
          "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
          "resource_owner_id":"Bob"
}

4. Client accesses the protected resource on behalf of the Resource Owner.

This is normal OAuth flow. Client can now access the Protected Resource with the access_token.

What OAuth lacks ? Resource owner initiated OAuth delegation

Irrespective of all the criticism against OAuth 2.0 - it has produced a very powerful, highly extensible authorization framework.

The use cases covered in the spec are not imaginary - rather very realistic.

At WSO2 we have integrated OAuth 2.0 support in to WSO2 Identity Server and WSO2 API Manager. Currently we do support all four grant types defined in the core specification. Also - we have very strong customer use cases for SAML2 grant types as well - which we have already started implementing.

If you look at the core specification - there are two key roles involved.

Resource Owner : An entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end- user.

Client : An application making protected resource requests on behalf of the resource owner and with its authorization. The term client does not imply any particular implementation characteristics (e.g. whether the application executes on a server, a desktop, or other devices).

Now if you look at the complete OAuth flow - you will notice that - all are initiated by the Client. This has ignored the use cases where the Resource Owner has to initiate the flow. Let me give a more concrete example.

I am a user of an online photo sharing site. There can be multiple Clients like Facebook applications, Twitter applications registered with it. Now I want to pick some client applications from the list and give them the access to my photos under different scopes. Validation of the trust/legitimacy of the registered applications can be carried out in other means.

That is just an example from social networking point of view. But there are more concrete enterprise use cases as well.

Let's take an access delegation use case.

I am an employee of Foo.com. I'll be going on vacation for two weeks - now I want to delegate some of my access rights to Peter only for that time. Conceptually OAuth fits nicely here. But - this is a use case which is initiated by the Resource Owner - which does not addressed in the OAuth specification.

How to import LDAP Groups but not Users to Liferay

When you configure Lifeary to talk to an LDAP - it imports users and groups in two different ways.

1. Import users and while importing users look at the "Group" attribute of the user and import groups.
2. Import groups and while importing groups look at the "User" attribute of the group and import all the users of that group.

By default Liferay uses the first approach.

If we want to import Groups only - not the users from the LDAP - then we need to have the 2nd approach.

To enable second approach, you need to add the following to the liferay_home/tomcat/webapps/ROOT/WEB-INF/classes/portal-ext.properties file.

ldap.import.method=group

Then you need to keep the "User" attribute of the group configuration blank.

Integrating WSO2 Identity Server with Liferay

Liferay has a highly extensible architecture. You decide what you want to override in Liferay - it has an extension somewhere. This blog post shows how to delegate Liferay's authentication and authorization functionality to WSO2 Identity Server.



One of the challenging part we found in this integration is the LDAP Users/Groups import. You can connect an LDAP to the Liferay. But, in that case to authenticate users to Liferay against the underlying LDAP it has to import all the users and groups to Liferay's underlying database, which is by default running on Hypersonic.

Since, we only need to keep the user data in a single LDAP, we wanted to avoid this duplication. But, it was not as easy as we thought. If you need to avoid that, then we need to write the complete persistence layer. To understand this better, I guess I need to elaborate more this. Let's take a step back and see how in fact Authentication and Authorization work in Liferay.

Liferay has a chain of authenticators. When you enter your username/password the chain of authenticators will get invoked. This is the place where we plugged in WSO2ISAuthenticator.

auth.pipeline.pre=org.wso2.liferay.is.authenticator.WSO2ISAuthenticator
auth.pipeline.enable.liferay.check=false 
wso2is.auth.service.endpoint.primary=https://localhost:9443/services/  

The above configuration [which should be in the liferay_home/tomcat/webapps/ROOT/WEB-INF/classes/portal-ext.properties] tells Liferay to load our custom authenticator. Also, the second entry says, once loaded our authenticator, do not invoke rest in the chain. Otherwise, the default Liferay authenticator will also get invoked. Third entry points to the AuthenticationAdmin service running in WSO2 Identity Server.

Now, the username/password go in to WSO2ISAuthenticator and it will talk to WSO2 Identity Server over SOAP to authenticate the user. Once authentication is done, the control once again will be passed in to the Liferay container.

Now is the tricky part. Liferay has it's own permission model -  who should be able to see portlets, who should be able to add portlets likewise. For this it needs to find which Liferay roles are attached to the logged in user or which Liferay roles are attached to any group the logged in user belongs to. To get these details it needs to talk to the underlying persistence layer - which will load details from Liferay's underlying database. This is why we wanted to have users imported here from the LDAP.

Even-though it's possible, we decided not to write a persistence layer - but only to override authentication and authorization.

Even in the case of authorization - there are two types. The authorization model governed by Liferay to display/add portlets to the portal. The authorization model used within the Portlet it self to display content within the portlet.

The first type is done by assigning portlet management permissions to a given Liferay role and assigning members [groups/users] to that role from the underlying LDAP. We did not want to do that. Because, that is very much on the portal administration side - and much specific to Liferay. But - the second model - is the one that directly deals with the business functions. That is what we wanted to do in a find-grained manner.

Let's dig more deep in to this...

Even the second model can be done with Liferay's roles and permission. Whenever you want to render something in the portlet that requires some restricted audience, then before rendering that you need to call req.isUserInRole("roleNme"). This is compliant with the JSR too. But the disadvantages are..

1. Our business functionalities in an SOA deployment should not be governed by Liferay roles. Liferay could only be a single channel to access the business functions.
2. We can achieve only the role based access control with this model.

Liferay, also has it's own way of permission checking, within a portlet via PermissionChecker API. You may have a look at this for further details.

Our approach was to write a utility function called hasPermission(). If you extend your portlet from org.wso2.liferay.xacml.connector.SecuredGenericPortlet then this will be automatically available for you. Or else - you can directly call it through AuthzChecker.hasPermission(). These functions are available from org.wso2.liferay.xacml.connector.jar file.

You can copy all jar dependencies from here and copy those to liferay_home/tomcat/lib/ext.
The connection between XACML connector deployed in Liferay and WSO2 XACML engine is through Thrift. You need to add following properties to portal-ext.properties file.

wso2is.auth.thrift.endpoint=localhost
wso2is.auth.thrift.port=10500
wso2is.auth.thrift.connection.timeout=10000
wso2is.auth.thrift.admin.user=admin
wso2is.auth.thrift.admin.user.password=admin
wso2is.auth.thrift.endpoint.login=https://localhost:9443/


Since by default Identity Server is using a self-signed certificate, either you have import it's public certificate to the trust store of Liferay or set the following two properties in portal-ext.properties file pointing to the Identity Server's key store.

wso2is.auth.thrift.system.trusstore=/wso2is-3.2.3/repository/resources/security/wso2carbon.jks
wso2is.auth.thrift.system.trusstore.password=wso2carbon


Please note that above configuration is tested with Liferay 6.1.1 and WSO2 Identity 3.2.3/4.0.0.