Saturday, December 26, 2015

WSO2 Cloud jaggery.js coding practices


Introduction


WSO2 Cloud, CloudMgt web app is built on jaggery.js. [1] It's a complete framework to write web apps and http-focused web services for all the aspects of the web application. Front-end, communication, server side back end logic in pure javascript.

Directory structure of the CloudMgt web app. As in general if you are writing a web app using jaggery.js you might have to have an understanding what each of the following described components do.

Figure 1: Directory structure of the cloudmgt app



























jaggery.conf : Jaggery configuration file specifies the application specific configurations such as welcome page, URL mappings, security constraints and etc.[2]. Basically this configurations are for the jaggery.js framework.
templates : Templates can be considered as reusable UI blocks. A single template is a single view of the web app. This is described in details how the UI blocks taken into play with all the css, client side javascript and html later in this article.
blocks : Blocks are the controller part of the web application. Basically blocks are responsible for handling respective template dynamically. There is a one-to-one mapping with template and blocks.
modules : This is responsible for maintaining the state of the application. Basically this is the "Model" of the application. It contains and maintain all the references plugged through java modules to the CloudMgt web app.
jagg : This directory holds all the functionality required for maintain the interactions between modules, blocks and templates.
pages : Pages holds the definitions of the individual pages rendering one or more blocks and templates combination.
conf : conf directory is consists with the cloudmgt app specific configurations files. cloud-mgt.json, email templates and etc.

Basically the above described templates, blocks and modules defines the MVC architecture of the jaggery.js framework.
Common utilities such as common styles, images. logos, fonts and etc are resides in directories such as images, css, fonts in the themes default directory.

CloudMgt app request flow


Figure 2: CloudMgt app request flow


































Coding practices

Basically as best practices we uses common basic javascript best practices that any developer practices currently ex: [3]. In this section I'm going to give some guidance on what should do/ should not do in jaggery.js framework perspective and code maintainability/readability/performance perspective of the CloudMgt app.

Templates

As described earlier a template is a reusable UI block, and a particular template holds a single view of the webapp. For an example see the following directory structure of a template. it contains custom css files specific to that template. client side javascript in the "js" directory. initializer.jag and the template.jag files.

├── subscriptions
│   ├── css
│   │   └── custom-styles.css
│   ├── initializer.jag
│   ├── js
│   │   └── subscriptions.js
│   └── template.jag

template.jag should contain only the raw html which needs to build a view. Developer can add the functionality to be executed at the initialization time of a template to the initializer.jag. Mostly it's consists with the includes of the javascript files and the css files to a template.

<%
jagg.initializer("subscriptions", {

    preInitialize:function () {
        jagg.addHeaderJS("subscriptions", "subscriptions", "templates/subscriptions/js/subscriptions.js");
        jagg.addHeaderCSS("subscriptions", "custom-styles", "templates/subscriptions/css/custom-styles.css");
    }
});
%>


Blocks

As described earlier blocks are controller of the framework. it manipulates the respective template according to the incoming http requests. Following is the respective directory structure of the subscriptions block. Point to keep in mind is the ajax directory which acts as the API for that view. This is not belongs to the blocks. This is the API layer which uses the client side to interact dynamically with the server. ex: form POST requests.

├── subscriptions
│   ├── ajax
│   │   └── subscriptions.jag
│   └── block.jag

Please see the example block.jag

<%
jagg.block("subscriptions", {
    initializer:function (data) {

    },

    getOutputs:function (inputs) {
        var user, result, mod, tenant;

        user = request.getParameter("user");
        tenant=inputs.tenant;

        if (null == user) {
            return {
                "result": null
            }
        }

        mod = jagg.module("billing");
        result = mod.getBillingInfo({"user":user});

        return {
            "result":result,
            "tenant":tenant
        }
    }

});
%>

As you can see block manipulates the inputs given from the pages with the request parameters and it uses the required modules to derive the output required for the template to become a view. this outputs are passed to the template to populate the dynamic values of the view.
Do not plug java modules or services in blocks or the apis. you are required only to use the respective jaggery modules in this layer.
Keep in my when you are exposing functionality to the client side with the APIs, please check the user is authorized to execute the functionality by filtering the requests using the functions as follows.

var loginStatus = jagg.isUserLoggedIn();
if (loginStatus.error) {
    response.status = 401;
    print(loginStatus);
    return;
}

If it's tenant admin related please use jagg.module("util").isUserInAdminRole(); methods and etc.
When getting the request parameters keep in mind to use "org.wso2.carbon.ui.util.CharacterEncoder" 's getSafeText() method. In here too follow the return object format specified in module practices section in this article.


Modules

There is nothing much to specify there in modules except the return objects should be as in following format, do not throw exceptions. Log and handle the exceptions at that level. Please use user friendly self explanatory error messages.

return {
    error: false,
    statusCode: 200,
    params: clientParams
}

return {
    error: true,
    statusCode: 500,
    message: "Internal error. Please retry..."
};


Pages

Here you can render different views according to the incoming http requests within a same page. see the example page.

<%
include("/jagg/jagg.jag");
include("../header.jag");
var site = require("/site/conf/site.json");
var i18n = require("i18n");
var localeResourcesBasePath = "/site/conf/locales/jaggery/";
i18n.init(request, localeResourcesBasePath);
var tenantDomain = jagg.getTenantDomain();
var errorPageUri = "/site/pages/error-pages/404.html";
var encoder = Packages.org.wso2.carbon.ui.util.CharacterEncoder;

(function () {
    var user = jagg.getUser(), isMonetizationEnabledObj, selectedApp, middleObj;

    isMonetizationEnabledObj = jagg.module("billing").isMonetizationEnabled(tenantDomain);
    isTenantAllowed();
    //if user is not logged in
    if (!user) {
        response.sendRedirect(getRedirectPathForNonAuthenticatedUser());
        return;
    }

    //if monetization is not available for tenant
    if (isMonetizationEnabledObj == null || isMonetizationEnabledObj.error || !isMonetizationEnabledObj.monetizationEnabled) {
        response.sendRedirect(jagg.getAbsoluteUrl("/site/pages/list.jag") + jagg.getTenantURLPrefix("?"));
        return;
    }

    var action = encoder.getSafeText(request.getParameter("action"));
    var workflowReference = encoder.getSafeText(request.getParameter("workflowReference"));

    var inputsObj = {
        "tenant": tenantDomain
    };

    if ("paymentMethod".equals(action)) {

        //When returned from a unsuccessful payment
        var paymentSuccess = encoder.getSafeText(request.getParameter("success"));
        var errorObj = {
            "error": false
        };

        if (paymentSuccess != null && !"".equals(paymentSuccess) && !paymentSuccess) {
            errorObj.error = true;
            errorObj.errorMessage = encoder.getSafeText(request.getParameter("errorMessage"));
        }

        inputsObj.errorObj = errorObj;

        middleObj = [{
            "name": "billing/payment-method/add",
            "inputs": inputsObj
        }];
    } else if ("createAccount".equals(action)) {
        var signature = encoder.getSafeText(request.getParameter("signature"));
        var refId = encoder.getSafeText(request.getParameter("refId"));

        inputsObj.refId = refId;
        inputsObj.signature = signature;

        middleObj = [{
            "name": "billing/account/create",
            "inputs": inputsObj
        }];
    } else {
        response.sendRedirect(jagg.getAbsoluteUrl("/site/pages/list.jag") + jagg.getTenantURLPrefix("?"));
        return;
    }

    jagg.render({
        "name": "page/base",
        "inputs": {
            "title": "My Account",
            "pagePath": "/site/pages/billing/manage.jag",
            "body": [
                {
                    "name": "layout/base",
                    "title": "My Account",
                    "inputs": {
                        "top": [

                            {
                                "name": "search",
                                "inputs": null
                            }
                        ],
                        "left": [
                            {
                                "name": "subscriptions",
                                "inputs": {"tenant": tenantDomain}
                            }
                        ],
                        "middle": middleObj,
                        "right": null
                    }
                }
            ]
        }
    });
}());

%>

Here the inputs that passed for the particular block specified. You can manipulate those inputs as needed for your templates.

Url mappings in jaggery.conf


"urlMappings": [{
    "url": "/billing/account",
    "path": "/admin/billing/account-info.jag"
}]

As above you can create paths to a page by simply adding a the mapping object to the jaggery.conf 's urlMapping object array.

Note that the code chunks specified here are not the exact way they are there in the cloudmgt app.

[1] http://jaggeryjs.org
[2] http://jaggeryjs.org/documentation.jag?api=jagconf
[3] http://www.w3schools.com/js/js_best_practices.asp


Saturday, October 17, 2015

Add secure vault for configurations which read from your carbon components


Introduction


If your WSO2 Carbon component needs a configuration which required to hold some sensitive information, you might probably want to consider on adding secure vault compatibility to your component.


WSO2 Carbon Secure Vault


Following are the key components of a WSO2 secure vault [1]

Secret Manager: Initializes the secret repository and the keystores. secret repository stores the encrypted values. keystore is used to generate the description crypto which will be used to resolve the encrypted passwords. Those can be configured from secret-conf.properties file. By default carbon server's primary keystore will be used.

Secret Repository: This uses to store the encrypted passwords. Currently the secure vault implementation is FileBaseSecretRepository and it uses cipher-text.properties file which is in the <PRODUCT_HOME>/repository/conf/security directory. It stores the alias and the the respective encrypted value of a particular secret.

Secret callback: This provides the actual secret for a given alias. SecretManagerSecretCallbackHandler is combined with Secret Manager to resolve a secret.

Secret resolver: Secret Resolver keeps a list of secured elements that need to be defined in the configuration file with secret aliases


DefaultSecretCallbackHandler

This callback handler resolves the keystores and require the private key of the primary keystore. So this act as a root password to initialize WSO2 Secure Vault. As described in [1] you will be able to provide this password by either providing it via command line or creating a password text file. Keep in mind about the permissions of this file. Otherwise this file will not be get deleted after a read. 

Encrypting passwords

Lets move on to encrypting your passwords in your own configuration and how the carbon component can decrypt those passwords in-order to use them in your component.

First you need to add your alias and the file name//xpath to the property value to be secured. Also add true if xml elements start with capital letter. Make sure you are using a unique alias here.
ex: Cloud.Billing.Password=billing.xml//CloudBilling/Password,true

Then add your plain-text password into cipher-text.properties file with an alias. Note that the Cipher Tool identifies plain text defined within square brackets as the plain text passwords
ex: Cloud.Billing.Password=[admin]

Then you can run the Cipher tool as described in [1] you can see following logs printed in the console.

"Primary KeyStore of Carbon Server is initialized Successfully
Encryption is done Successfully
Secret Configurations are written to the property file successfully"

Also you will see the default configuration to secret-conf.properties file

Decrypting passwords in carbon components

For decrypting passwords you can use the following code chunks.
SECRET_ALIAS_ATTRIBUTE_NAME_WITH_NAMESPACE = "secretAlias"
SECURE_VAULT_NS = "http://org.wso2.securevault/configuration"

In here I'm reading configuration in xml by resolving with the encrypted secrets. As default configuration, most importantly you can use SecretCallbackHandlerService to resolve the secrets. interface (org.wso2.carbon.securevault.SecretCallbackHandlerService)

    /**
     * secure vault enable documents securely resolve elements
     *
     * @param doc DOC
     */
    private static void secureResolveDocument(Document doc) {
        Element element = doc.getDocumentElement();
        if (element != null) {
            secureLoadElement(element);
        }
    }


    /**
     * Securely load elements
     *
     * @param element xml element
     */
    private static void secureLoadElement(Element element) {
        Attr secureAttr =
                element.getAttributeNodeNS(BillingConstants.SecureValueProperties.SECURE_VAULT_NS,
                                           BillingConstants.SecureValueProperties
                                                   .SECRET_ALIAS_ATTRIBUTE_NAME_WITH_NAMESPACE);
        if (secureAttr != null) {
            element.setTextContent(loadFromSecureVault(secureAttr.getValue()));
            element.removeAttributeNode(secureAttr);
        }
        NodeList childNodes = element.getChildNodes();
        int count = childNodes.getLength();
        Node tmpNode;
        for (int i = 0; i < count; i++) {
            tmpNode = childNodes.item(i);
            if (tmpNode instanceof Element) {
                secureLoadElement((Element) tmpNode);
            }
        }
    }


    /**
     * Load from secure vault
     *
     * @param alias alias used
     * @return CallBackHandler
     */
    private static String loadFromSecureVault(String alias) {
        if (secretResolver == null) {
            secretResolver = SecretResolverFactory.create((OMElement) null, false);
            secretResolver.init(ServiceDataHolder.getInstance().getSecretCallbackHandlerService()
                                        .getSecretCallbackHandler());
        }
        return secretResolver.resolve(alias);
    }


References

[1] https://docs.wso2.com/display/IS500/WSO2+Carbon+Secure+Vault

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.