Tuesday, September 30, 2014

Custom Authenticator for WSO2 Identity Server (WSO2IS) with Custom Claims

WSO2IS is one of the best Identity Servers, which enables you to offload your identity and user entitlement management burden totally from your application. It comes with many features, supports many industry standards and most importantly it allows you to extent it according to your security requirements.

In this post I am going to show you how to write your own Authenticator, which uses some custom claim to validate users and how to invoke your custom authenticator with your web app.

Create your Custom Authenticator Bundle

WSO2IS is based OSGi, so if you want to add a new authenticator you have to crate an OSGi bungle. Following is the source of the OSGi bundle you have to prepare.

This bundle will consist of three files,
1. CustomAuthenticatorServiceComponent
2. CustomAuthenticator
3. CustomAuthenticatorConstants

CustomAuthenticatorServiceComponent is an OSGi service component it basically registers the CustomAuthenticator (service). CustomAuthenticator is an implementation of org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator, which actually provides our custom authentication.


1. CustomAuthenticatorServiceComponent


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package org.wso2.carbon.identity.application.authenticator.customauth.internal;

import java.util.Hashtable;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
import org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator;
import org.wso2.carbon.identity.application.authenticator.customauth.CustomAuthenticator;
import org.wso2.carbon.user.core.service.RealmService;

/**
 * @scr.component name="identity.application.authenticator.customauth.component" immediate="true"
 * @scr.reference name="realm.service"
 * interface="org.wso2.carbon.user.core.service.RealmService"cardinality="1..1"
 * policy="dynamic" bind="setRealmService" unbind="unsetRealmService"
 */
public class CustomAuthenticatorServiceComponent {

    private static Log log = LogFactory.getLog(CustomAuthenticatorServiceComponent.class);

    private static RealmService realmService;
    
    protected void activate(ComponentContext ctxt) {

        CustomAuthenticator customAuth = new CustomAuthenticator();
     Hashtable<String, String> props = new Hashtable<String, String>();
     
        ctxt.getBundleContext().registerService(ApplicationAuthenticator.class.getName(), customAuth, props);
        
        if (log.isDebugEnabled()) {
            log.info("CustomAuthenticator bundle is activated");
        }
    }

    protected void deactivate(ComponentContext ctxt) {
        if (log.isDebugEnabled()) {
            log.info("CustomAuthenticator bundle is deactivated");
        }
    }
    
    protected void setRealmService(RealmService realmService) {
        log.debug("Setting the Realm Service");
        CustomAuthenticatorServiceComponent.realmService = realmService;
    }

    protected void unsetRealmService(RealmService realmService) {
        log.debug("UnSetting the Realm Service");
        CustomAuthenticatorServiceComponent.realmService = null;
    }

    public static RealmService getRealmService() {
        return realmService;
    }

}


2. CustomAuthenticator

This is where your actual authentication logic is implemented


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package org.wso2.carbon.identity.application.authenticator.customauth;

import java.io.IOException;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.application.authentication.framework.AbstractApplicationAuthenticator;
import org.wso2.carbon.identity.application.authentication.framework.AuthenticatorFlowStatus;
import org.wso2.carbon.identity.application.authentication.framework.LocalApplicationAuthenticator;
import org.wso2.carbon.identity.application.authentication.framework.config.ConfigurationFacade;
import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext;
import org.wso2.carbon.identity.application.authentication.framework.exception.AuthenticationFailedException;
import org.wso2.carbon.identity.application.authentication.framework.exception.InvalidCredentialsException;
import org.wso2.carbon.identity.application.authentication.framework.exception.LogoutFailedException;
import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils;
import org.wso2.carbon.identity.application.authenticator.customauth.internal.CustomAuthenticatorServiceComponent;
import org.wso2.carbon.identity.base.IdentityException;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.core.UserStoreManager;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;

/**
 * Username Password based Authenticator
 * 
 */
public class CustomAuthenticator extends AbstractApplicationAuthenticator
        implements LocalApplicationAuthenticator {

    private static final long serialVersionUID = 192277307414921623L;

    private static Log log = LogFactory.getLog(CustomAuthenticator.class);

 @Override
 public boolean canHandle(HttpServletRequest request) {
        String userName = request.getParameter("username");
  String password = request.getParameter("password");

  if (userName != null && password != null) {
   return true;
  }

  return false;
 }

    @Override
    public AuthenticatorFlowStatus process(HttpServletRequest request,
                                           HttpServletResponse response, AuthenticationContext context)
            throws AuthenticationFailedException, LogoutFailedException {

        if (context.isLogoutRequest()) {
            return AuthenticatorFlowStatus.SUCCESS_COMPLETED;
        } else {
            return super.process(request, response, context);
        }
    }

 @Override
 protected void initiateAuthenticationRequest(HttpServletRequest request,
   HttpServletResponse response, AuthenticationContext context)
   throws AuthenticationFailedException {

  String loginPage = ConfigurationFacade.getInstance().getAuthenticationEndpointURL();
  String queryParams = FrameworkUtils
    .getQueryStringWithFrameworkContextId(context.getQueryParams(),
      context.getCallerSessionKey(),
      context.getContextIdentifier());
  
  try {
      String retryParam = "";
            
            if (context.isRetrying()) {
                retryParam = "&authFailure=true&authFailureMsg=login.fail.message";
            }
      
            response.sendRedirect(response.encodeRedirectURL(loginPage + ("?" + queryParams))
                    + "&authenticators=" + getName() + ":" + "LOCAL" + retryParam);
  } catch (IOException e) {
   throw new AuthenticationFailedException(e.getMessage(), e);
  }
 }

 @Override
 protected void processAuthenticationResponse(HttpServletRequest request,
   HttpServletResponse response, AuthenticationContext context)
   throws AuthenticationFailedException {

  String username = request.getParameter("username");
  String password = request.getParameter("password");

  boolean isAuthenticated = false;

  // Check the authentication
  try {
   int tenantId = IdentityUtil.getTenantIdOFUser(username);
            UserRealm userRealm = CustomAuthenticatorServiceComponent.getRealmService()
                    .getTenantUserRealm(tenantId);
            
            if (userRealm != null) {
                UserStoreManager userStoreManager = (UserStoreManager)userRealm.getUserStoreManager();
                isAuthenticated = userStoreManager.authenticate(MultitenantUtils.getTenantAwareUsername(username),password);

                Map<String, String> parameterMap = getAuthenticatorConfig().getParameterMap();
                String blockSPLoginClaim = null;
                if(parameterMap != null) {
                    blockSPLoginClaim = parameterMap.get("BlockSPLoginClaim");
                }
                if (blockSPLoginClaim == null) {
                    blockSPLoginClaim = "http://wso2.org/claims/blockSPLogin";
                }
                if(log.isDebugEnabled()) {
                    log.debug("BlockSPLoginClaim has been set as : " + blockSPLoginClaim);
                }

                String blockSPLogin = userStoreManager.getUserClaimValue(MultitenantUtils.getTenantAwareUsername(username),
                        blockSPLoginClaim, null);

                boolean isBlockSpLogin = Boolean.parseBoolean(blockSPLogin);
                if (isAuthenticated && isBlockSpLogin) {
                    if (log.isDebugEnabled()) {
                        log.debug("user authentication failed due to user is blocked for the SP");
                    }
                    throw new AuthenticationFailedException("SPs are blocked");
                }
            } else {
                throw new AuthenticationFailedException("Cannot find the user realm for the given tenant: " + tenantId);
            }
  } catch (IdentityException e) {
   log.error("CustomAuthentication failed while trying to get the tenant ID of the use", e);
   throw new AuthenticationFailedException(e.getMessage(), e);
  } catch (org.wso2.carbon.user.api.UserStoreException e) {
   log.error("CustomAuthentication failed while trying to authenticate", e);
   throw new AuthenticationFailedException(e.getMessage(), e);
  }

  if (!isAuthenticated) {
   if (log.isDebugEnabled()) {
    log.debug("user authentication failed due to invalid credentials.");
            }

            throw new InvalidCredentialsException();
  }

  context.setSubject(username);
  String rememberMe = request.getParameter("chkRemember");

  if (rememberMe != null && "on".equals(rememberMe)) {
   context.setRememberMe(true);
  }
 }
 
 @Override
 protected boolean retryAuthenticationEnabled() {
  return true;
 }
 
 @Override
 public String getContextIdentifier(HttpServletRequest request) {
  return request.getParameter("sessionDataKey");
 }

 @Override
 public String getFriendlyName() {
  return CustomAuthenticatorConstants.AUTHENTICATOR_FRIENDLY_NAME;
 }

 @Override
 public String getName() {
  return CustomAuthenticatorConstants.AUTHENTICATOR_NAME;
 }
}

3. CustomAuthenticatorConstants

This is a helper class to just to hold the constants you are using in your authenticaator


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package org.wso2.carbon.identity.application.authenticator.customauth;

/**
 * Constants used by the CustomAuthenticator
 *
 */
public abstract class CustomAuthenticatorConstants {
 
 public static final String AUTHENTICATOR_NAME = "CustomAuthenticator";
 public static final String AUTHENTICATOR_FRIENDLY_NAME = "custom";
 public static final String AUTHENTICATOR_STATUS = "CustomAuthenticatorStatus";
}

Once you are done with these files, your authenticator is ready. Now you can build you OSGi bundle and place the bundle inside <CRBON_HOME>/repository/components/dropins.

*sample pom.xml file [3]

Create new Claim

Now you have to create a new claim in WSO2IS. To do this, log into the management console of WSO2IS and do the steps described in [1]. In this example, I am going to create new claim "Block SP Login".

So, goto configuration section of the management console click on "Claim Management", then select "http://wso2.org/claims" Dialect

Click on "Add New Claim Mapping", and fill the details related to your claim.

Display Name   Block SP Login
Description   Block SP Login
Claim Uri http://wso2.org/claims/blockSPLogin
Mapped Attribute (s)  localityName
Regular Expression  
Display Order   0
Supported by Default  true
Required   false
Read-only   false

Now, your new claim is ready in WSO2IS. As you select "Supported by Default" as true, this claim will be available in your user profile. So you will see this field appear, when you try to create a user, but this field in not mandatory as you didn't specify it as "Required"

Change application-authentication.xml

There is another configuration change you have to do, as it is going to take the claim name from the configuration file (CustomAuthenticator.java, 107-114). Add the information about the your new claim into repository/conf/security/application-authentication.xml


1
2
3
<AuthenticatorConfig name="CustomAuthenticator" enabled="true">
<Parameter name="BlockSPLoginClaim">http://wso2.org/claims/blockSPLogin</Parameter>
</AuthenticatorConfig> 

If you check the code CustomAuthenticator.java line,107-128. You will see in the processAuthenticationResponse, in addition to authenticating the user from the user store, it checks for the new claim,

So, this finishes the, basic steps to setup your custom authentication. Now you have to setup new Service Provider in WSO2IS and set you custom authentication to it. So that when ever your SP try to authenticate a user from WSO2IS, it will use your custom authenticator.

Create Service Provider and set the Authenticator

Follow the basic steps given in [2] to create a new Service Provider.

Then, goto, "Inbound Authentication Configuration"->"SAML2 Web SSO Configuration", and make the following changes,


1
2
3
4
5
6
Issuer* = <name of you SP>
Assertion Consumer URL = <http://localhost:8080/your-app/samlsso-home.jsp>
Enable Response Signing = true
Enable Assertion Signing = true 
Enable Single Logout = true
Enable Attribute Profile = true

Then goto, "Local & Outbound Authentication Configuration" section,
select "Local Authentication" as the authentication type, and select your authenticator, here "custom".

Now you have completed all the steps needed to setup your custom autheticator with your custom claims

You can now start the WSO2IS, and start using your service. Meanwhile, change the value of the "Block SP Login" of a particular user and see the effect.


[1] https://docs.wso2.com/display/IS500/Adding+New+Claim+Mapping
[2] https://docs.wso2.com/display/IS500/Adding+a+Service+Provider
[3] https://drive.google.com/file/d/0B25Kjdxz8EhCQktfdG5MYkFnTUk/view?usp=sharing


9 comments:

  1. Hello Jayanga, my example doesn't work and I don't know why :(
    Can you give me an example of maven pom.xml?
    Regards

    ReplyDelete
  2. Hi Marco,
    I have attached the pom.xml file in the post. Please refer [3]
    Regards,

    ReplyDelete
  3. Hi Jayanga,

    I followed the steps but couldn't get the authenticator to working. The problem was when I built the jar, inside that OSGI-INF folder was missing. I could fix it by adding goals as below to the org.apache.felix plugin.


    org.apache.felix
    maven-scr-plugin
    1.7.2


    generate-scr-scrdescriptor

    scr





    Thank you,
    Tharindu

    ReplyDelete
    Replies
    1. Hi Tharindu,
      Thanks for highlighting this.
      I updated the pom.xml [3]

      Thanks,
      Jayanga

      Delete
  4. I followed instructions and it built fine and I put in dropins however it never shows up in Local & Outbound Authentication Configuration for my SP, it only ever has basic and iwa, what am I doing wrong. IS Version 5.0.0

    ReplyDelete
    Replies
    1. The same for me. Even adding bundle activator instruction does't do the trick. Te authenticator doesn't even load. I can see it while running IS in remote debug mode.

      Delete
    2. Did you ever fix this? I have the same problem

      Delete
  5. Hi! Do you have to make modifications in the login.jsp page for this to work? Thanks!!

    ReplyDelete