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 LoginClaim 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
Hello Jayanga, my example doesn't work and I don't know why :(
ReplyDeleteCan you give me an example of maven pom.xml?
Regards
Hi Marco,
ReplyDeleteI have attached the pom.xml file in the post. Please refer [3]
Regards,
Hi Jayanga,
ReplyDeleteI 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
Hi Tharindu,
DeleteThanks for highlighting this.
I updated the pom.xml [3]
Thanks,
Jayanga
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
ReplyDeleteThe 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.
Deletesame here
DeleteDid you ever fix this? I have the same problem
DeleteHi! Do you have to make modifications in the login.jsp page for this to work? Thanks!!
ReplyDelete