info@yenlo.com
eng
Menu
WSO2 Identity Server 14 min

WSO2 Identity Server: Post Authentication Handlers

Learn how to add a custom user consent page to your login flow with WSO2 Identity Server's Post Authentication Handlers. Optimize your authentication process and enhance user experience.

Denuwanthi De Silva
Denuwanthi De Silva
Solution Architect
WSO2 Identity Server Post Authentication Handlers

How to add an additional user agreement/ consent page in the login flow

In most applications used day to day, during the logging-in process you are prompted to provide consent to sharing of your information or terms & conditions of an end user licensing agreement. These kinds of consent requests inform users, about the usage limits of the application or indicate as to what extent, the software provider is liable when users are using the application. Specially with recent personal data protection regulations like GDPR and various media reports of personal data violation by well-known software service providers, getting the user consent for information sharing is important. Normally, if the users do not consent, the logging process will be aborted.

The applications may prompt consent requests, either during the user login flow, user registration flow, or the application installation time. There is no hard and fast rule when these additional, pages need to be shown. Some apps may not show terms & conditions as a separate page, rather it would have a check box within the login or signup page itself.

This blog is applicable for scenarios where you want to take various consent approvals from the end user during the login process.

Scenario: Adding an Additional Approval Page in Login Flow with WSO2 Identity Server

Need to prompt an additional page to get user consent, user agreement, agreement of terms and conditions or some disclaimer notification, before allowing the user to login to the consumer application.

Here we are making this additional step as part of the login process. Such cases need to be handled via the identity provider which is responsible for handling authentication/login of the application. In case the application is not using any identity provider for login, then it’s up to the application developers to maintain those additional logic and consent pages.

Today, we will see how WSO2 Identity server – which acts as an identity provider for 3rd party application, can be used to help you to add such an additional approval page to the login flow, easily.

Solution: Using Post Authentication Handlers in WSO2 Identity Server for Handling Additional Approval Pages in Login Flow

To handle this kind of scenarios, WSO2 Identity server provides an extension point called “Post Authentication Handler”.

Do not confuse this with the “Event Handler” extension point in WSO2 Identity Server.

In the WSO2 Identity Server event framework there are events that get fired before the authentication and after the authentication, respectively called “PRE_AUTHENTICATION” & “POST_AUTHENTICATION”. You can subscribe to those events and write handlers to trigger or execute custom logic.

But “Post Authentication Handlers” are not in the same category. They are triggered once all the authentication steps are completed.

Using Post Authentication Handlers in WSO2 Identity Server for Handling Additional Approval Pages in Login Flow

If you configure multiple authentication steps like username/password authentication + SMS OTP or configure a federated authenticators like Facebook/google authentication for your application, after all authentication steps are successfully completed only, the post authentication handlers will get executed.

3 Steps to Create a Post Authentication Handler

Let’s say you have an application which uses WSO2 Identity Server for user authentication. There is a recent requirement to introduce new terms & conditions page to get user’s consent. It is mandatory for your application to take consent from users on the terms & conditions related to accessing your application before allowing the login flow to complete. Following are the high-level steps you can use to achieve this requirement with help of WSO2 Identity Server.

  1. Create a jsp file with the page you want the users to see and give their consent. Ex: terms-and-conditions.jsp. Store it inside WSO2 IS/repository/deployment/server/webapps/authenticationendpoint folder.
  2. Write the post authentication hander java component (OSGi bundle).
    • Create a maven project.
    • You will need “org. wso2.carbon.identity.application. authentication.framework” as a main dependency in this project. Add correct versions of the authentication framework which matches the WSO2 IS distribution you are using.
    • Create a new java class extending the “AbstractPostAuthnHandler”.
    • Ex: public class TermsAndConditionsPostAuthenticationHandler extends AbstractPostAuthnHandler {
    • Now override the “handle” method inside the new class and implement the logic to prompt for the new terms & conditions page and if consent is given/not given by the user, to allow or reject login to the application.
@Override
public PostAuthnHandlerFlowStatus handle(HttpServletRequest httpServletRequest,
                                         HttpServletResponse httpServletResponse,
                                         AuthenticationContext authenticationContext)
        throws PostAuthenticationFailedException {
  • The returning type (PostAuthnHandlerFlowStatus) of this method is an Enum which consists of values

SUCCESS_COMPLETED – Indicates the tasks of post authentication handler was executed successfully.

INCOMPLETE – Indicates the tasks of the post authentication handler is not yet completed. Ex: the terms& conditions page is not yet popped up for the user.

UNSUCCESS_COMPLETED – Indicates the task of post authentication handler was completed, but execution was not successful. Ex: The authenticated user is ‘null’. Therefore, skip the execution of post authentication handler by sending out this value.

  • Within the “handle” method following logic needs to be implemented.
    • Check the authenticated user is not null.
    • Check whether the user is redirected to the intended page. Ex: terms-and-conditions.jsp
    • If user is on the newly added page, check whether approval is given or not. If approval is not given, throw an exception.
    • If user is still not in the intended page, redirect to the intended page by doing a http redirect. Ex: httpServletResponse.sendRedirect()
register your handler implementation
  • Now register your handler implementation class as a PostAuthenticationHandler OSGi service ex: context.getBundleContext().registerService(PostAuthenticationHandler.class.getName(),TermsAndConditionsPostAuthenticationHandler, null);
  1. Configure the new handler in deployment.toml file.

Ex:

[[event_listener]]

id = “custom_post_auth_listener”

type = “org.wso2.carbon.identity.core.handler.AbstractIdentityHandler”

name = “org.wso2.carbon.identity.post.authn.handler.custom.

TermsAndConditionsPostAuthenticationHandler

order = 100  

The handlers are executed first with the lowest number. Here I have put 100. So, my handler will execute after all other post authentication handlers (like the default consent management page) are completed.

The high-level flow during login will be like below:

high level flow during login

Using externally hosted pages: Redirecting to External Consent Pages from WSO2 Identity Server Post Authentication Handler Class Implementation

As you saw in the earlier flow diagram, it is within the handler class implementation that we redirect to the intended page using the sendRedirect method. So, instead of hosting your pages inside authenticationendpoint webapp, you can host the desired consent/disclaimer/terms & conditions pages, externally and from the handler implementation class redirect to the externally or separately hosted page.

Points to note:

  • Make sure to pass the “sessionDataKey”, parameter as a request parameter when doing the redirection within the handler implementation class.

Ex: httpServletResponse.sendRedirect(“https://myhost:8080/consent.jsp”+”?sessionDataKey=” + authenticationContext.getContextIdentifier());

  • Then make sure to pass the same seesionDataKey as a hidden input parameter from your custom jsp file, back to the WSO2 Identity server’s commonauth endpoint.

Ex: <input type=”hidden” name=”<%=”sessionDataKey“%>”
       value=”<%=Encode.forHtmlAttribute(request.getParameter(“sessionDataKey”)) %>”/> This parameter helps to maintain the correlation between various redirections happening within a request flow. Since HTTP is a stateless protocol, this parameter helps the WSO2 Identity Server to track the state of a particular request flow coming to the WSO2 Identity Server.

5 Reference Implementations of Post Authentication Handlers in WSO2 Identity Server

  1.  ConsentMgtPostAuthnHandler : Handles user consents upon successful authentication. Prompts for user consent page.
  2. JITProvisioningPostAuthenticationHandler : Handles Just-In-Time Provisioning after a user is authenticated against an external Identity provider (federated IDP). Prompts for Just-In-Time provisioning pages.
  3. PostAuthAssociationHandler : Responsible for associating federated users to local user accounts. In this implementation, it does not prompt for any page redirections. It only does the local user associations behind the scenes.
  4. PostAuthenticatedSubjectIdentifierHandler : Responsible of setting subject identifier for an authenticated user.
  5. PostAuthnMissingClaimHandler : Responsible for picking up missing mandatory claims. This prompts for a page, which asks the user to fill mandatory claims, if it’s not already filled.

Summary

Using Post-Authentication Handlers in WSO2 Identity Server to prompt additional pages during login request flow can be done for a variety of use cases such as prompting for user consents, approvals, or text inputs.

Post authentication handlers can also be used to execute custom logic during the login flow without necessarily doing any custom page redirections. It is important to note that these handlers are only executed upon the completion of the authentication sequence/steps.

The default implementation of the product offers additional functionality such as JIT provisioning and missing claim handlers. Feel free to use the following sample source code snippet to implement your own post-authentication handler in WSO2 Identity Server. And stay tuned for more examples and insights on our blog!

If you’re looking for more examples of how to work with WSO2 Identity Server, be sure to check out the official GitHub repository. Additionally, for detailed instructions on how to use these samples, check out this helpful blog post from Yenlo – Working with the WSO2 Identity Server Samples. Both resources can provide you with valuable insights and code snippets to help you get started with your WSO2 Identity Server projects.

package org.yenlo.carbon.identity.post.authn.handler.termsandconditions;

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.PostAuthenticationFailedException;
import org.wso2.carbon.identity.application.authentication.framework.handler.request.AbstractPostAuthnHandler;
import org.wso2.carbon.identity.application.authentication.framework.handler.request.PostAuthnHandlerFlowStatus;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser;

import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TermsAndConditionsPostAuthenticationHandler extends AbstractPostAuthnHandler {

    private String CONSENT_POPPED_UP = "consentPoppedUp";

    @Override
    public PostAuthnHandlerFlowStatus handle(HttpServletRequest httpServletRequest,
                                             HttpServletResponse httpServletResponse,
                                             AuthenticationContext authenticationContext)
            throws PostAuthenticationFailedException {

        if (getAuthenticatedUser(authenticationContext) == null) {
            return PostAuthnHandlerFlowStatus.SUCCESS_COMPLETED;
        }

        if (isConsentPoppedUp(authenticationContext)) {
            if (httpServletRequest.getParameter("consent").equalsIgnoreCase("approve")) {
                return PostAuthnHandlerFlowStatus.SUCCESS_COMPLETED;
            } else {
                throw new PostAuthenticationFailedException("Cannot access this application : Consent Denied",
                        "Consent denied");
            }
        } else {
            try {
                httpServletResponse.sendRedirect
                        (ConfigurationFacade.getInstance().getAuthenticationEndpointURL().replace("/login.do", ""
                        ) + "/termsandconditions" + ".jsp?sessionDataKey=" + authenticationContext.getContextIdentifier() +
                                "&application=" + authenticationContext
                                .getSequenceConfig().getApplicationConfig().getApplicationName());
                setConsentPoppedUpState(authenticationContext);
                return PostAuthnHandlerFlowStatus.INCOMPLETE;
            } catch (IOException e) {
                throw new PostAuthenticationFailedException("Invalid Consent", "Error while redirecting", e);
            }
        }
    }

    @Override
    public String getName() {

        return "DisclaimerHandler";
    }

    private AuthenticatedUser getAuthenticatedUser(AuthenticationContext authenticationContext) {

        AuthenticatedUser user = authenticationContext.getSequenceConfig().getAuthenticatedUser();
        return user;
    }

    private void setConsentPoppedUpState(AuthenticationContext authenticationContext) {

        authenticationContext.addParameter(CONSENT_POPPED_UP, true);
    }

    private boolean isConsentPoppedUp(AuthenticationContext authenticationContext) {

        return authenticationContext.getParameter(CONSENT_POPPED_UP) != null;
    }

}

OSGi Service Component class

package org.yenlo.carbon.identity.post.authn.handler.termsandconditions.internal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.wso2.carbon.identity.application.authentication.framework.handler.request.PostAuthenticationHandler;
import org.wso2.carbon.identity.core.util.IdentityCoreInitializedEvent;
import org.yenlo.carbon.identity.post.authn.handler.termsandconditions.TermsAndConditionsPostAuthenticationHandler;

@Component(
        name = "identity.post.authn.termsandconditions.handler",
        immediate = true
)
public class TermsAndConditionsPostAuthnHandlerServiceComponent {

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

    @Activate
    protected void activate(ComponentContext context) {

        try {
            TermsAndConditionsPostAuthenticationHandler termsAndConditionsPostAuthenticationHandler =
                    new TermsAndConditionsPostAuthenticationHandler();
            context.getBundleContext().registerService(PostAuthenticationHandler.class.getName(),
                    termsAndConditionsPostAuthenticationHandler, null);

        } catch (Throwable e) {
            log.error("Error while activating disclaimer post authentication handler.", e);
        }
    }

    protected void unsetIdentityCoreInitializedEventService(IdentityCoreInitializedEvent identityCoreInitializedEvent) {
        /* reference IdentityCoreInitializedEvent service to guarantee that this component will wait until identity core
         is started */
    }

    @Reference(
            name = "identity.core.init.event.service",
            service = IdentityCoreInitializedEvent.class,
            cardinality = ReferenceCardinality.MANDATORY,
            policy = ReferencePolicy.DYNAMIC,
            unbind = "unsetIdentityCoreInitializedEventService"
    )
    protected void setIdentityCoreInitializedEventService(IdentityCoreInitializedEvent identityCoreInitializedEvent) {
        /* reference IdentityCoreInitializedEvent service to guarantee that this component will wait until identity core
         is started */
    }
}

JSP file

<%@ page import="org.owasp.encoder.Encode" %>
<%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.Constants" %>

<%
    String app = request.getParameter("application");
    String[] missingClaimList = null;
    String appName = null;
    if (request.getParameter(Constants.MISSING_CLAIMS) != null) {
        missingClaimList = request.getParameter(Constants.MISSING_CLAIMS).split(",");
    }
%>

<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WSO2 Identty Server</title>
    
   <link rel="icon" href="images/favicon.png" type="image/x-icon"/>
    <link href="libs/bootstrap_3.3.5/css/bootstrap.min.css" rel="stylesheet">
    <link href="css/Roboto.css" rel="stylesheet">
    <link href="css/custom-common.css" rel="stylesheet">
    
   <!--[if lt IE 9]>
    <script src="js/html5shiv.min.js"></script>
    <script src="js/respond.min.js"></script>
    <![endif]-->
</head>

<body>

<script type="text/javascript">
    function approved() {
        document.getElementById('consent').value = "approve";
        document.getElementById("profile").submit();
    }
    function deny() {
        document.getElementById('consent').value = "deny";
        document.getElementById("profile").submit();
    }
</script>

<!-- header -->
<header class="header header-default">
    <div class="container-fluid"><br></div>
    <div class="container-fluid">
        <div class="pull-left brand float-remove-xs text-center-xs">
            <a href="#">
                <img src="images/logo-inverse.svg" alt="wso2" title="wso2" class="logo">
                <h1><em> Identity Server </em></h1>
            </a>
        </div>
    </div>
</header>

<!-- page content -->
<div class="container-fluid body-wrapper">
    
   <div class="row">
        <div class="col-md-12">
            
           <!-- content -->
            <div class="container col-xs-10 col-sm-6 col-md-6 col-lg-3 col-centered wr-content wr-login col-centered">
                <div>
                    <h2 class="wr-title uppercase blue-bg padding-double white boarder-bottom-blue margin-none">
                       Terms & Conditions
                    </h2>
                </div>
                
               <div class="boarder-all ">
                    <div class="clearfix"></div>
                    <form action="../commonauth" method="post" id="profile" name=""
                          class="form-horizontal" >
                        <div class="padding-double login-form">
                            <div class="form-group">
                                <p>By using this <strong><%=Encode.forHtml(request.getParameter("application"))%></strong> app, you agree to the terms and conditions outlined below, which are designed to ensure the security and privacy of your personal data and provide a seamless user experience.
                                </p>

                            
                           </div>
                            
                           <table width="100%" class="styledLeft">
                                <tbody>
                                <tr>
                                    <td class="buttonRow" colspan="2">
                                        
                                       <div style="text-align:left;">
                                            <input type="button" class="btn  btn-primary" id="approve" name="approve"
                                                   onclick="javascript: approved(); return false;"
                                                   value="Approve"/>
                                            <input class="btn" type="reset"
                                                   value="Deny"
                                                   onclick="javascript: deny(); return false;"/>
                                        </div>
                                        
                                       <input type="hidden" name="<%="sessionDataKey"%>"
                                               value="<%=Encode.forHtmlAttribute(request.getParameter("sessionDataKey"))%>"/>
                                        <input type="hidden" name="consent" id="consent"
                                               value="deny"/>
                                    </td>
                                </tr>
                                </tbody>
                            </table>
                        </div>
                    </form>
                
               </div>
            </div>
        
       
       </div>
        <!-- /content -->
    
   </div>
</div>
<!-- /content/body -->

</div>

<!-- footer -->
<footer class="footer">
    <div class="container-fluid">
        <p>WSO2 Identity Server | ©
            <script>document.write(new Date().getFullYear());</script>
            <a href="http://wso2.com/" target="_blank"><i class="icon fw fw-wso2"></i>
                Inc
            </a>. All Rights Reserved
        </p>
    </div>
</footer>

<script src="libs/jquery_1.11.3/jquery-1.11.3.js"></script>
<script src="libs/bootstrap_3.3.5/js/bootstrap.min.js"></script>
</body>
</html>
eng
Close