Wednesday, May 20, 2015

Multi step authentication


Introduction


If you have a requirement to have an authentication flow which require multiple steps to be get executed before a user authenticates to your system. you can achieve this with multi step authentication mechanism in WSO2 IS. This feature is available from IS version 5.0.0.

For an example say in your system, your users have subscriptions for multiple tenant under many user roles. So the user experience would be user authenticates to the system using his/her username password, then user selects the tenant he wants to login, then user is redirected to that tenant with the required authorizations.
Lets see the authentication flow here. First he should be authenticated to a common ground which is super tenant. after he logged in as a user for super tenant he/she will be provided with the tenant list. Then user selects the tenant he/she wants to logged in. After that user get redirected to the required tenant with associated user roles.
With the conventional way of doing this would be user will get asked to provide username passwords or system has to keep them. This is not a good solution


Multi step authentication


I will provide some insight to this authentication mechanism via above example using the conventional authenticator BasicAuth first in the authentication flow and then a custom authenticator to get authenticated to the tenant.

This is my authenticator which authenticates the user to the selected tenant after successfully authenticated via BasicAuth in the first step.

public class LoginStepAuthenticator extends AbstractApplicationAuthenticator implements LocalApplicationAuthenticator {

    private static final long serialVersionUID = 1L;
    private static final Log log = LogFactory.getLog(LoginStepAuthenticator.class);

    /**
     * Process the authenticator response
     *
     * Additionally here it will set the subject, tenantDomain and the "user-tenant-domain"
     * property of the authenticator context.
     *
     * @param request Http Servlet Request
     * @param response Http Servlet Response
     * @param context Authentication Context
     * @throws AuthenticationFailedException
     */
    @Override
    protected void processAuthenticationResponse(HttpServletRequest request,
                                                 HttpServletResponse response,
                                                 AuthenticationContext context)
            throws AuthenticationFailedException {
        if (log.isDebugEnabled()) {
            log.debug("Login step authenticator processing the authentication response.");
        }

        String username = request.getParameter(LoginStepAuthenticatorConstants.USERNAME);
        String userTenant = request.getParameter(LoginStepAuthenticatorConstants.TENANT_DOMAIN);
        String sessionDataKey = request.getParameter(LoginStepAuthenticatorConstants.SESSION_DATA_KEY);

        removeTenantListFromCache(sessionDataKey);
        AuthenticatedUser authenticatedUser;
        //Here we will come only if the user is authenticated from local store
        if (!LoginStepAuthenticatorConstants.SUPER_TENANT.equals(userTenant)) {
            authenticatedUser=AuthenticatedUser.createLocalAuthenticatedUserFromSubjectIdentifier(username + "@" + userTenant);
        } else {
            authenticatedUser=AuthenticatedUser.createLocalAuthenticatedUserFromSubjectIdentifier(username);
        }
        context.setSubject(authenticatedUser);
        context.setTenantDomain(userTenant);
        context.setProperty(LoginStepAuthenticatorConstants.AUTH_CONTEXT_USER_TENANT_DOMAIN, userTenant);
    }

    /**
     * Process the authentication or logout request.
     *
     * @param request HttpServletRequest
     * @param response HttpServletResponse
     * @param context AuthenticationContext
     * @return the status of the flow
     * @throws AuthenticationFailedException
     * @throws LogoutFailedException
     */
    public AuthenticatorFlowStatus process(HttpServletRequest request,
                                           HttpServletResponse response, AuthenticationContext context)
            throws AuthenticationFailedException, LogoutFailedException {
        if (log.isDebugEnabled()) {
            log.debug("Login step authenticator processing the authentication request.");
        }
        if (context.isLogoutRequest()) {
            return AuthenticatorFlowStatus.SUCCESS_COMPLETED;
        } else {
            String isValidateStep = request.getParameter(LoginStepAuthenticatorConstants.STEP_AUTH);
            //if the step is not in 'validate' this is the first time request hits the authenticator.
            //should redirect to list the tenants
            if (isValidateStep != null && "validate".equalsIgnoreCase(isValidateStep)) {

                String username = request.getParameter(LoginStepAuthenticatorConstants.USERNAME);
                String userTenant = request.getParameter(LoginStepAuthenticatorConstants.TENANT_DOMAIN);
                String sessionDataKey = request.getParameter(LoginStepAuthenticatorConstants.SESSION_DATA_KEY);

                //if the tenant domain exists as user subscribed tenants (validated against the cached tenant list)
                if (tenantDomainExists(sessionDataKey, userTenant)) {
                    processAuthenticationResponse(request, response, context);
                    request.setAttribute(FrameworkConstants.REQ_ATTR_HANDLED, true);
                    return AuthenticatorFlowStatus.SUCCESS_COMPLETED;
                } else {
                    log.error("Login step authenticator in validation step without a valid subscribed tenantDomain " +
                              "for user: " + username + ". Redirecting to login step");
                    initiateAuthenticationRequest(request, response, context);
                    return AuthenticatorFlowStatus.INCOMPLETE;
                }
            } else {
                initiateAuthenticationRequest(request, response, context);
                return AuthenticatorFlowStatus.INCOMPLETE;
            }
        }
    }

    /**
     * Check whether the authentication or logout request can be handled by the
     * authenticator
     *
     * @param httpServletRequest HttpServletRequest
     * @return boolean
     */
    public boolean canHandle(HttpServletRequest httpServletRequest) {
        // LoginStepAuthenticator is only used to list tenants.
        if (log.isDebugEnabled()) {
            log.debug("Login step authenticator picked the request to handle.");
        }
        return true;
    }

    /**
     * Get the Context identifier sent with the request. This identifier is used
     * to retrieve the state of the authentication/logout flow
     *
     * @param httpServletRequest HttpServletRequest
     * @return Session data key
     */
    public String getContextIdentifier(HttpServletRequest httpServletRequest) {
        return httpServletRequest.getParameter(LoginStepAuthenticatorConstants.SESSION_DATA_KEY);
    }

    /**
     * Get the name of the Authenticator
     *
     * @return name
     */
    public String getName() {
        return LoginStepAuthenticatorConstants.AUTHENTICATOR_NAME;
    }

    /**
     * Get the shortened version of the name as the friendly name
     *
     * @return friendly name
     */
    public String getFriendlyName() {
        return LoginStepAuthenticatorConstants.AUTHENTICATOR_FRIENDLY_NAME;
    }

    /**
     * Initializing the authenticator request and set the current authenticator as
     * the LoginStepAuthenticator.
     *
     * @param request HttpServletRequest
     * @param response HttpServletResponse
     * @param context AuthenticationContext
     * @throws AuthenticationFailedException
     */
    @Override
    protected void initiateAuthenticationRequest(HttpServletRequest request,
                                                 HttpServletResponse response, AuthenticationContext context)
            throws AuthenticationFailedException {
        if (log.isDebugEnabled()) {
            log.debug("Login step authenticator initiating the authentication request.");
        }
        try {
            forward(request, response, context);
            context.setCurrentAuthenticator(getName());
        } catch (UserStoreException e) {
            log.error("Login step authenticator authentication initialization request failed", e);
            throw new AuthenticationFailedException(e.getMessage(), e);
        }
    }

    /**
     * This will forward the initial request after getting authenticated
     * to list the tenants that the user has the subscription with.
     *
     * @param request HttpServletRequest
     * @param response HttpServletResponse
     * @param context AuthenticationContext
     * @throws AuthenticationFailedException
     * @throws UserStoreException
     */
    protected void forward(HttpServletRequest request,
                           HttpServletResponse response, AuthenticationContext context)
            throws AuthenticationFailedException, UserStoreException {

        if (log.isDebugEnabled()) {
            log.debug("Login step authenticator initiating the authentication request.");
        }

        String userName = request.getParameter(LoginStepAuthenticatorConstants.USERNAME);
        AuthenticatedIdPData authenticatedIdPData = getAuthenticatedIdPData(context);
        if (userName == null) {
            if (authenticatedIdPData != null) {
                userName = authenticatedIdPData.getUser().getUserName();
            } else {
                throw new AuthenticationFailedException("No Authenticated users found");
            }
        }

        String attributeWithValues = (String) request.getAttribute("attributewithvalues");

        //Directing to servlet endpoint to list the tenants
        String loginPage = LoginStepAuthenticatorConstants.TENANT_LIST_ENDPOINT;
        String queryParams = FrameworkUtils.getQueryStringWithFrameworkContextId(
                context.getQueryParams(), context.getCallerSessionKey(),
                context.getContextIdentifier());

        try {
            String retryParam = "";

            if (context.isRetrying()) {
                retryParam = "&authFailure=true&authFailureMsg=login.fail.message";
            }
            String rememberMe= request.getParameter("chkRemember");

            //stepauth parameter is set to authenticate.
            response.sendRedirect(response.encodeRedirectURL(loginPage + ("?" + queryParams + "&username=" + userName
                                                                          + "&attributewithvalues=" +
                                                                          attributeWithValues))
                                  + "&" + LoginStepAuthenticatorConstants.STEP_AUTH + "=authenticate" +
                                  "&authenticators="
                                  + getName() + ":" + "LOCAL" + retryParam+"&chkRemember="+rememberMe);
        } catch (IOException e) {
            log.error("Login step authenticator authentication response forwarding failed", e);
            throw new AuthenticationFailedException(e.getMessage(), e);
        }
    }

    /**
     * Get the information of the authenticated IdP
     *
     * @param context AuthenticationContext
     * @return AuthenticatedIdP data
     */
    private AuthenticatedIdPData getAuthenticatedIdPData(AuthenticationContext context) {
        Map<String, AuthenticatedIdPData> currentAuthenticatedIdPs = context.getCurrentAuthenticatedIdPs();
        if (currentAuthenticatedIdPs != null) {
            Set<String> keys = currentAuthenticatedIdPs.keySet();
            for (String key : keys) {
                if (currentAuthenticatedIdPs.get(key) != null) {
                    return currentAuthenticatedIdPs.get(key);
                }
            }
        }
        return null;
    }

    /**
     * check the tenant domain exists as user subscribed tenants (validated against the cached tenant list)
     *
     * @param sessionDataKey Session Data key
     * @param tenantDomain tenant domain user claims to be subscribed
     * @return boolean
     */
    private boolean tenantDomainExists(String sessionDataKey, String tenantDomain) {
        CacheManager cacheManager = Caching.getCacheManager();
        Cache<String, List<String>> cache = cacheManager.getCache(LoginStepAuthenticatorConstants.STEP_AUTH_CACHE);

        return (cache.get(sessionDataKey)).contains(tenantDomain);
    }

    /**
     * Removes the cached list of tenants under the specified session data key
     *
     * @param sessionDataKey Session data key
     */
    private void removeTenantListFromCache(String sessionDataKey) {
        CacheManager cacheManager = Caching.getCacheManager();
        Cache<String, List<String>> cache = cacheManager.getCache(LoginStepAuthenticatorConstants.STEP_AUTH_CACHE);
        cache.remove(sessionDataKey);

        if (log.isDebugEnabled()) {
            log.debug("Removing tenantDomain list from cache for the session : " + sessionDataKey);
            log.debug("Printing all tenantDomain lists in cache");

            Iterator<String> itTenantList = cache.keys();
            while (itTenantList.hasNext()) {
                List<String> tenantDomains = cache.get(itTenantList.next());
                log.debug("Associated tenantDomains for the session: " + sessionDataKey + " are : " + tenantDomains
                        .toString());
            }
        }
    }
}

Step authenticator is configured as follows

Authenticator gets enabled from <IS_HOME>/repository/conf/security/application-authentication.xml by adding
the following element under "AuthenticatorConfigs".

<AuthenticatorConfig name="LoginStepAuthenticator" enabled="true" />

And adding the following <IS_HOME>/repository/conf/identity/service-providers/default.xml as the step two of "AuthenticationSteps" in "LocalAndOutBoundAuthenticationConfig"


<AuthenticationStep>
       <StepOrder>2</StepOrder>
       <LocalAuthenticatorConfigs>
               <LocalAuthenticatorConfig>
                       <Name>LoginStepAuthenticator</Name>
                       <DisplayName>login-step</DisplayName>
                       <IsEnabled>true</IsEnabled>
               </LocalAuthenticatorConfig>
       </LocalAuthenticatorConfigs>
       <SubjectStep>true</SubjectStep>
       <AttributeStep>true</AttributeStep>
</AuthenticationStep>

And set the "SubjectStep", "AttributeStep" to "false" in StepOrder '1' authenticator.

Additionally
To list the tenants you can use org.wso2.carbon.identity.application.authentication.endpoint.util.TenantDataManager's TenantDataManager.getAllActiveTenantDomains() method

Friday, May 1, 2015

Get authenticated cookie with SAML2 response


Introduction

In a WSO2 integration sometimes you might want to get authenticated to admin services from you Webapps to another Carbon server admin service without requesting the credentials or keeping the credentials in memory. For this you can use the authenticated SAML response to get the authenticated admin cookie and invoke those admin services in another carbon server.

Following code chunk explains how authenticated SAMLresponse is added as a Note in Catalina server session when the session is authenticated.

protected void authenticateSession(Request request, String sessionIndex, String username)
        throws Exception {
    //Register the specified Principal as being associated with the specified value for the sessionIndex on identifier.
    Principal principal = WebappTomcatUtils.getUserPrincipal(request, username);

    SingleSignOnEntry sso = lookup(sessionIndex);
    if (sso == null) {
        register(sessionIndex, principal, SAML_METHOD, username, "");
    }

    //Associate the specified single sign on identifier with the specified Session
    associate(sessionIndex, request.getSessionInternal(true));

    // Save the authenticated Principal in our session
    Session session = request.getSessionInternal(false);
    if (session != null) {
        session.setAuthType(SAML_METHOD);
        session.setPrincipal(principal);
        session.setNote(Constants.SAML_RESPONSE, request.getSession().getAttribute("saml_token"));
    }

    // set user is authenticated.
    HttpSession httpSession = request.getSession();
    this.setSessionAuthInfo(httpSession, (GenericPrincipal) principal, getTenantDomainFromUsername(username), SAML_METHOD);
}

Following method explains how we can get the authenticated cookie using SAML response.


public String authenticateWithSAML2Response(ConfigurationContext configContext,
                                                HttpServletRequest httpRequest,
                                            String backendServerURL,
                                            String username) throws AuthenticationException {
    if (username == null) {
        throw new AuthenticationException("Username is null. Failed to retrieve the username from the http request");
    }

    SAML2SSOAuthenticationServiceStub stub = null;
    try {
        stub = new SAML2SSOAuthenticationServiceStub(configContext, backendServerURL + SSO_AUTH_SERVICE);
        AuthnReqDTO authnReqDTO = new AuthnReqDTO();
        if (session == null) {
            throw new AuthenticationException("Session object is null");
        }
        authnReqDTO.setResponse((String) session.getNote(Constants.SAML_RESPONSE));
        // authnReqDTO.setUsername(username);
        boolean loggedIn = stub.login(authnReqDTO);
        String cookie;
        if (loggedIn) {
            cookie = (String) stub._getServiceClient().getServiceContext().getProperty(HTTPConstants.COOKIE_STRING);
            if (log.isDebugEnabled()) {
                log.debug("Logged in Cookie : " + cookie);
            }
            return cookie;
        } else {
            log.error("Login failure.");
            throw new AuthenticationException("Login failure.");
        }
    } catch (RemoteException e) {
        String errorMsg = "SAML2SSO Authentication Service Stub initialization failed. ";
        log.error(errorMsg, e);
        throw new AuthenticationException(errorMsg, e);
    } finally {
        if (stub != null) {
            try {
                stub.cleanup();
            } catch (RemoteException e) {
                log.error("Error while cleaning up SAML2SSOAuthenticationServiceStub", e);
            }
        }
    }
}

If this is failing with the following exception that means when passing SAML token to get admin cookie, Identity Server do audience restriction validation against the value inside  authenticators.xml ServiceProviderID parameter to make sure client is intended to use given SAML response.

[2015-04-06 16:22:24,179] ERROR {org.wso2.carbon.identity.authenticator.saml2.sso.SAML2SSOAuthenticator} -  Authentication Request is rejected. SAMLResponse AudienceRestriction validation failed.

To solve this you have to make sure when registering the service provider, add all the necessary audiences to it.