Default authenticator of WSO2 Identity Server, is the basic authenticator and it is also a local authenticator which authenticates the end user with connected user store using provided username and password. If a user needs to be authenticated with the user store and authorized based on a specific assigned role, you can write a custom local authenticator. Let's take following sample requirement.
You have an app called ‘playground’ that is used for importing/exporting photos. This app uses OpenID Connect to let the users log into it. During login, only the users who belong to the user role called 'photoSharingRole' should be allowed to be logged in. In other words, the users with other roles should not be able to log into the playground app.
To facilitate the above requirement, WSO2 Identity Server provides extension points that has the ability to plug custom local authenticators.
The following steps explain how you can write a custom local authenticator by extending the AbstractApplicationAuthenticator class and implementing the LocalApplicationAuthenticator class and how to this custom local authenticator can be plugged in to these extension points. Now let's begin.
Create a maven project to write the custom authenticator. (
pom.xml
example given below)<?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.wso2.custom.authenticator</groupId> <artifactId>org.wso2.custom.authenticator.local</artifactId> <packaging>bundle</packaging> <version>1.0.0</version> <name>WSO2 Carbon - BasicAuth Custom Authenticator</name> <dependencies> <dependency> <groupId>org.wso2.carbon</groupId> <artifactId>org.wso2.carbon.utils</artifactId> <version>4.4.11</version> </dependency> <dependency> <groupId>org.wso2.carbon</groupId> <artifactId>org.wso2.carbon.logging</artifactId> <version>4.4.11</version> </dependency> <dependency> <groupId>org.wso2.carbon.identity.framework</groupId> <artifactId>org.wso2.carbon.identity.application.authentication.framework</artifactId> <version>5.7.5</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.1</version> <inherited>true</inherited> <configuration> <encoding>UTF-8</encoding> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-scr-plugin</artifactId> <version>1.7.2</version> <executions> <execution> <id>generate-scr-scrdescriptor</id> <goals> <goal>scr</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <version>2.3.5</version> <extensions>true</extensions> <configuration> <instructions> <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName> <Bundle-Name>${project.artifactId}</Bundle-Name> <Axis2Module>${project.artifactId}-${project.version}</Axis2Module> <Import-Package> javax.servlet, javax.servlet.http, *;resolution:=optional </Import-Package> <Private-Package> org.wso2.custom.authenticator.local.internal, </Private-Package> <Export-Package> !org.wso2.custom.authenticator.local.internal, org.wso2.custom.authenticator.local.*; version="1.0.0" </Export-Package> </instructions> </configuration> </plugin> </plugins> </build> <repositories> <repository> <id>wso2-nexus</id> <name>WSO2 internal Repository</name> <url>http://maven.wso2.org/nexus/content/groups/wso2-public/</url> <releases> <enabled>true</enabled> <updatePolicy>daily</updatePolicy> <checksumPolicy>ignore</checksumPolicy> </releases> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>wso2-maven2-repository</id> <url>http://dist.wso2.org/maven2</url> </pluginRepository> </pluginRepositories> </project>
Write a custom local authenticator.
Sample custom authenticator class/* * Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.wso2.custom.authenticator.local; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.custom.authenticator.local.internal.BasicCustomAuthenticatorServiceComponent; import org.wso2.carbon.identity.application.authentication.framework.AbstractApplicationAuthenticator; 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.model.AuthenticatedUser; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.user.core.UserStoreException; import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.common.AbstractUserStoreManager; import org.wso2.carbon.utils.multitenancy.MultitenantUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Username Password based custom Authenticator */ public class BasicCustomAuthenticator extends AbstractApplicationAuthenticator implements LocalApplicationAuthenticator { private static final long serialVersionUID = 4345354156955223654L; private static final Log log = LogFactory.getLog(BasicCustomAuthenticator.class); @Override protected void initiateAuthenticationRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException { String loginPage = ConfigurationFacade.getInstance().getAuthenticationEndpointURL();//This is the // default WSO2 IS login page. If you can create your custom login page you can use // that instead. 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=BasicAuthenticator:" + "LOCAL" + retryParam); } catch (IOException e) { throw new AuthenticationFailedException(e.getMessage(), e); } } /** * This method is used to process the authentication response. * Inside here we check if this is a authentication request coming from oidc flow and then check if the user is * in the 'photoSharingRole'. */ @Override protected void processAuthenticationResponse(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException { String username = request.getParameter(BasicCustomAuthenticatorConstants.USER_NAME); boolean isAuthenticated = true; context.setSubject(AuthenticatedUser.createLocalAuthenticatedUserFromSubjectIdentifier(username)); boolean authorization = false; if(isAuthenticated) { if ("oidc".equalsIgnoreCase(context.getRequestType())) { // authorization only for openid connect requests try { int tenantId = BasicCustomAuthenticatorServiceComponent.getRealmService().getTenantManager(). getTenantId(MultitenantUtils.getTenantDomain(username)); UserStoreManager userStoreManager = (UserStoreManager) BasicCustomAuthenticatorServiceComponent.getRealmService(). getTenantUserRealm(tenantId).getUserStoreManager(); // verify user is assigned to role authorization = ((AbstractUserStoreManager) userStoreManager).isUserInRole(username, "photoSharingRole"); } catch (UserStoreException e) { log.error(e); } catch (org.wso2.carbon.user.api.UserStoreException e) { log.error(e); } } else { // others scenarios are not verified. authorization = false; } if (!authorization) { log.error("user authorization is failed."); throw new InvalidCredentialsException("User authentication failed due to invalid credentials", User.getUserFromUserName(username)); } } } @Override protected boolean retryAuthenticationEnabled() { return true; } @Override public String getFriendlyName() { //Set the name to be displayed in local authenticator drop down lsit return BasicCustomAuthenticatorConstants.AUTHENTICATOR_FRIENDLY_NAME; } @Override public boolean canHandle(HttpServletRequest httpServletRequest) { String userName = httpServletRequest.getParameter(BasicCustomAuthenticatorConstants.USER_NAME); String password = httpServletRequest.getParameter(BasicCustomAuthenticatorConstants.PASSWORD); if (userName != null && password != null) { return true; } return false; } @Override public String getContextIdentifier(HttpServletRequest httpServletRequest) { return httpServletRequest.getParameter("sessionDataKey"); } @Override public String getName() { return BasicCustomAuthenticatorConstants.AUTHENTICATOR_NAME; } }
Write a osgi service component class to register the custom authenticator.
/* * Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.wso2.custom.authenticator.local.internal; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.osgi.service.component.ComponentContext; import org.wso2.custom.authenticator.local.BasicCustomAuthenticator; import org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator; import org.wso2.carbon.user.core.service.RealmService; /** * @scr.component name="org.wso2.custom.authenticator.local.basic.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 BasicCustomAuthenticatorServiceComponent { private static Log log = LogFactory.getLog(BasicCustomAuthenticatorServiceComponent.class); private static RealmService realmService; public static RealmService getRealmService() { return realmService; } protected void activate(ComponentContext ctxt) { try { BasicCustomAuthenticator basicCustomAuth = new BasicCustomAuthenticator(); ctxt.getBundleContext().registerService(ApplicationAuthenticator.class.getName(), basicCustomAuth, null); if (log.isDebugEnabled()) { log.info("BasicCustomAuthenticator bundle is activated"); } } catch (Throwable e) { log.error("BasicCustomAuthenticator bundle activation Failed", e); } } protected void deactivate(ComponentContext ctxt) { if (log.isDebugEnabled()) { log.info("BasicCustomAuthenticator bundle is deactivated"); } } protected void unsetRealmService(RealmService realmService) { log.debug("UnSetting the Realm Service"); BasicCustomAuthenticatorServiceComponent.realmService = null; } protected void setRealmService(RealmService realmService) { log.debug("Setting the Realm Service"); BasicCustomAuthenticatorServiceComponent.realmService = realmService; } }
Build the project using maven. (Click to see the sample project here)
- Copy the .jar file
org.wso2.custom.authenticator.local-1.0.0.jar
inside<IS_HOME>/repository/components/dropins
folder. Start the server.
Log into Admin Console.
Create a new user (ex: ‘Alice’).
Create a new role (ex: photoSharingRole) and assign the user to it.
Create a service provider for the application.
(You must have the app deployed on the tomcat server.)In service provider configuration, under ‘Inbound Authentication Configuration’/‘Oauth/OpenID Connect Configuration’, click Configure.
Provide the Callback URL and register it as a OAuth2 client app.
Under ‘Local & Outbound Authentication Configuration’, select ‘Local authentication’ check box.
On the corresponding drop down list, you can see, ‘BasicCustom’ which is the display name of the custom authenticator that was written. From the above step, you can make sure your custom authenticator is there and ready for use.
- Visit the playground app and provide Client ID of the registered playground app and give the Scope as ‘openid’ to make sure it is in the OpenID Connect flow. Click Authorize.
Then you will be directed to WSO2 Identity Server login page. Provide the username and password of the user 'Alice' who is in the ‘photoSharingRole’. You are prompted to approve the app and logged in.
The following is a set of methods related to writing a custom local authenticator.
Method | Description |
---|---|
canHandle() | This method checks whether the authentication request is valid, according to the custom authenticator’s requirements. The user will be authenticated if the method returns 'true'. This method also checks whether the authentication or logout request can be handled by the authenticator. For example, you can check if the username and password is 'not null' in the canHandle method. if that succeeds, the authentication flow will continue (to have the user authenticated). |
process() | This method is used to process or carry out the user authentication process. It calls the super.process(), so that the super class will handle the authentication process, and it calls processAuthenticationResponse() method in the custom authenticator class to execute the custom authentication logic. |
processAuthenticationResponse() | Implementation of custom authentication logic happens inside this method. For example, you can call any API that can do authentication and authenticate the user or you can authenticate the user against the underlying user store. Then you can also do any other custom logic after authenticating the user. For example, you can check if a user belongs to a particular role and allow authentication accordingly. |
initiateAuthenticationrequest() | This method is used to redirect the user to the login page in order to authenticate. You can redirect the user to a custom login URL using this method or you can use default WSO2 Identity Server login page. |
retryAuthenticationEnabled() | This method returns a boolean value. If the authentication fails due to some issue like invalid credentials, this method will be called to know whether to retry the authentication flow. If this method returns 'true', the authentication flow will be retried. If returns 'false', the flow is stopped with an exception thrown. You need to override this method if you are calling the super.process() method. But if you are writing your own process() method, you can handle all the retrying logic accordingly within the method. |
getContextIdentifier() | This method gets the Context identifier sent with the request. This identifier is used to retrieve the state of the authentication/logout flow. |
getFriendlyName() | This method returns the name you want to display for your custom authenticator. This name will be displayed in the local authenticators drop down. |
getname() | This method is used to get the name of the authenticator. |