MicroStrategy ONE
Single Sign-on Sample: Custom ESM Code Explanation
The custom External Security Module (ESM) of the Single Sign-on (SSO) Sample illustrates how to implement single sign-on when MicroStrategy Web is used with an identity management application that is not supported out-of-the-box. The code in this custom ESM class retrieves a token that has been passed in the URL and validates it using the sample authentication server provided as part of the sample. (In a production environment, the token is validated using the API of the existing identity management application.) If the token is valid, the ESM creates a new MicroStrategy Intelligence Server session and returns the newly created session to MicroStrategy Web. If the token is invalid, the ESM tells MicroStrategy Web to redirect the user to a custom login page.
The custom ESM for this sample application is included in the plug-in calledSSOSample, which is located under theCustomizationPlugins/AdvancedScenariosfolder in the SDK installation directory.
The custom ESM class in this sample is packaged in com.microstrategy.sdk.samples.sso. It extends the AbstractExternalsSecurity class to take advantage of the default method implementations in that class. It imports the necessary MicroStrategy and non-MicroStrategy files and declares three variables to hold the values for the properties defined in the ssoesm.properties file. The custom ESM in this sample includes three public methods, five private helper methods, and two private inner classes. This topic explains how the code is constructed in each of these methods and classes.
The three public methods, which override the default methods in the AbstractExternalsSecurity class, include:
The five private helper methods, created just for this sample, include:
The two private inner classes, created just for this sample, include:
The code in each method or class is explained below. The code is explained in sections, with the explanation preceding each section of code.
Public methods
The handlesAuthenticationRequest method attempts to use an existing session if one is available and alive and, if not, causes the custom login page to be opened so that the user can enter his or her credentials.
-
Explanation: First, the handlesAuthenticationRequest method is overridden to do the following:
-
Create an errorContainer on thread local storage that can be used across method calls.
-
If a token exists in the RequestKeys parameter that was passed into the method, get the token and then immediately remove it from the RequestKeys for security purposes.
Copypublic inthandlesAuthenticationRequest(RequestKeys reqKeys,
ContainerServices cntSvcs, int reason) {
//create errorContainer obj
errorContainer = new ThreadLocal();
//get token from request
String token = reqKeys.getValue(ssoTokenName);
//remove token from reqKeys to disable token being displayed in the URL
reqKeys.remove(ssoTokenName);
-
-
Explanation: If there was no token in the RequestKeys, check to see if there is a token in the ContainerServices’ session. If there is also no token in the ContainerServices, then the user is redirected to a custom login page. This is accomplished by returning USE_CUSTOM_LOGIN_PAGE, which causes MicroStrategy Web to call the getCustomLoginURL method. This method, which is described below, provides the URL of the custom login page to which the user is directed in order to log in.
Copyif (StringUtils.isEmpty(token)) {
//in this branch, token is empty
///////////////////////////////////////////////
//Step 1. Check session variable for an old authentication server token
//and validate it if found.
//get old token from Session
String oldToken = (String) cntSvcs.getSessionAttribute(SSO_TOKEN_NAME);
//check if the old token exists
if (StringUtils.isEmpty(oldToken)) {
return USE_CUSTOM_LOGIN_PAGE;
} -
Explanation: If there was no token in the RequestKeys, but there was a token in the ContainerServices session, it must be validated to see if it can still be used. If validation fails, then USE_CUSTOM_LOGIN_PAGE is returned, which causes MicroStrategy Web to call the getCustomLoginURL method. However, before this happens, the existing token information is removed from the ContainerServices.
Copy//Check if old token is still valid.
if (validate(ssoURL, oldToken, cntSvcs) == null) {
//Old token is expired, so close the session, clear session state, clear old token, and redirect to custom login page.
String sessionState = (String) cntSvcs.getSessionAttribute(SESSION_STATE);
if (StringUtils.isEmpty(sessionState)) {
closeISSession(sessionState);
}
cntSvcs.setSessionAttribute(SSO_TOKEN_NAME, null);
cntSvcs.setSessionAttribute(SESSION_STATE, null);
return USE_CUSTOM_LOGIN_PAGE;
} -
Explanation: If there was no token in the RequestKeys, but there was a token in the ContainerService session and it was determined to be valid, the existence of this valid token confirms that the user is who he or she claims to be. The next step is to load the session state if it is available. If it is not available, then USE_CUSTOM_LOGIN_PAGE is returned, which causes MicroStrategy Web to call the getCustomLoginURL method. Otherwise, there is a valid token and a session state. Next, the session state must be checked to see if can be reconnected. If so, COLLECT_SESSION_NOW is returned, indicating that MicroStrategy Web can obtain the session right away. (COLLECT_SESSION_NOW causes the getWebIServerSession method to be called. The overridden version of this method is explained in a later step.) If the session state cannot be reconnected, then USE_CUSTOM_LOGIN_PAGE is returned, which causes MicroStrategy Web to call the getCustomLoginURL method that redirects the user to the custom login page.
Copy///////////////////////////////////////////////////////
//Step 2. Try to restore session state, if it exists, then ensure we can use state to create connection.
String sessionState = (String) cntSvcs.getSessionAttribute(SESSION_STATE);
if (StringUtils.isEmpty(sessionState)) {
//no session state saved, client need to request a token from Authentication Server
errorContainer.set(new ErrorInfo(ErrorInfo.NO_TOKEN_FOUND, "No token found and no session state available"));
return USE_CUSTOM_LOGIN_PAGE;
}
if (reconnectISSession(sessionState, cntSvcs, reqKeys)) {
return COLLECT_SESSION_NOW;
} else {
//Cannot use existing session state; clear session state and token
//and redirect to custom login page.
cntSvcs.setSessionAttribute(SESSION_STATE, null);
cntSvcs.setSessionAttribute(SSO_TOKEN_NAME, null);
return USE_CUSTOM_LOGIN_PAGE;
}
-
Explanation: If there is a token in the RequestKeys, any existing session information can be cleared out and the token can be checked to see if it is valid. If validation succeeds, the session can be rebuilt. if validation fails, then USE_CUSTOM_LOGIN_PAGE is returned, which causes MicroStrategy Web to call the getCustomLoginURL method. In this sample application, the validate method returns the name of the user, which is the same as the MicroStrategy user name. If the token validator returns name information that was different from the MicroStrategy user name, that name information has to be mapped to the MicroStrategy user name, as described in the Mapping Credentials using an External Repository scenario.
Copy} else {
//in this branch, token is not empty
//////////////////////////////////////////////////////////
//Step 1. close Intelligence Server session, clear session state and
// clear oldToken
String sessionState = (String) cntSvcs.getSessionAttribute(SESSION_STATE);
if (sessionState != null) {
closeISSession(sessionState);
cntSvcs.setSessionAttribute(SESSION_STATE, null);
cntSvcs.setSessionAttribute(SSO_TOKEN_NAME, null);
}
////////////////////////////////////////////////
//Step 2. validate token
String SSOUserID = validate(ssoURL, token, cntSvcs);
if (StringUtils.isEmpty(SSOUserID)) {
//invalid token
return USE_CUSTOM_LOGIN_PAGE;
} -
Explanation: After all the necessary information has been gathered, an attempt is made to create a session. If session creation is successful, COLLECT_SESSION_NOW is returned, which causes MicroStrategy Web to call the getWebIServerSession method. If session creation fails, then USE_CUSTOM_LOGIN_PAGE is returned, which causes MicroStrategy Web to call the getCustomLoginURL method.
Copy////////////////////////////////////////////
//Step 3. create session
boolean success = createISSession(SSOUserID, reqKeys, cntSvcs);
if (success) {
//session created successfully
cntSvcs.setSessionAttribute(SSO_TOKEN_NAME, token);
return COLLECT_SESSION_NOW;
} else {
//cannot create session
return USE_CUSTOM_LOGIN_PAGE;
}
} //end of if token
}
The getCustomLoginURL method builds the URL of the custom login page.
-
Explanation: The getCustomLoginURL method builds the URL of the custom login page to which the user is directed to log in when handlesAuthenticationRequest returns USE_CUSTOM_LOGIN_PAGE. (As explained in the earlier steps, this value is returned when no token is found or when a token is found but it is not valid.) If an Intelligence Server session cannot be created based on the information available, the user must be prompted to enter his or her credentials. The first step is to ensure that the original URL parameters passed in include all the parameters that are needed to send the user back into MicroStrategy Web from the custom login page. If the server, port, or project parameter is missing, it is added back.
Copypublic StringgetCustomLoginURL(String originalURL, String desiredServer,
int desiredPort, String desiredProject) {
//get errorInfo
ErrorInfo errorInfo = (ErrorInfo) errorContainer.get();
//clear errorContainer
errorContainer.set(null);
/////////////////////////////////////////////////////////
//Generate a URL for the custom logon URL and add URL parameters
//to this url by using Parameter builder
//add server parameter in the url if it is not there
if (desiredServer != null && originalURL.toLowerCase().indexOf("server=") == -1) {
originalURL = originalURL + "&server=" + desiredServer;
}
//add server port parameter in the originalURL if it is not there
if (originalURL.toLowerCase().indexOf("port=") == -1) {
originalURL = originalURL + "&port=" + desiredPort;
}
//add project name parameter in the originalURL if it is not there
if (desiredProject != null && originalURL.toLowerCase().indexOf("project=") == -1) {
originalURL = originalURL + "&project=" + desiredProject;
} -
Explanation: Now, ParameterBuilder is used to construct the URL to the custom login page. This is accomplished by adding all the parameters that are needed and then returning the URL as a string. For more information on using ParameterBuilder, see the Parameter Builder Infrastructure section of the MSDL.
CopyParameterBuilder pb = new DefaultURIBuilderImpl();
try {
URL url = new URL(customLoginURL);
pb.setTargetBase(url.getProtocol() + "://" + url.getAuthority() + url.getPath());
pb.setTargetSuffix(url.getQuery());
if (errorInfo != null) {
pb.addParameter("ErrorCode", Integer.toString(errorInfo.getCode()));
pb.addParameter("ErrorMessage", errorInfo.getMessage());
}
pb.addParameter("OriginalURL", originalURL);
pb.addParameter("Server", desiredServer);
pb.addParameter("Port", Integer.toString(desiredPort));
pb.addParameter("Project", desiredProject);
return pb.toString();
} catch (Exception e) {
//throw an runtime exception
throw new MSTRUncheckedException(e);
}
}
The getWebIServerSession method attempts to retrieve an existing session state.
-
Explanation: The getWebIServerSession method is called when COLLECT_SESSION_NOW is returned from the handlesAuthenticationRequest method, indicating that an Intelligence Server session is ready. In getWebIServerSession, an attempt is first made to get the session state from the ContainerServices. If a session state cannot be retrieved, there is nothing more that can be done and an error is thrown. If a session state can be retrieved, it is used to restore the session itself. If the session is already alive, it is returned. If the session is not alive, it is first reconnected and then the new state is set back to where it was.
Copypublic WebIServerSessiongetWebIServerSession(RequestKeys reqKeys,
ContainerServices cntSvcs) {
try {
///////////////////////////////////////////////////
//Step 1. Confirm session is non-empty - if empty, throw error.
String sessionState = (String) cntSvcs.getSessionAttribute(SESSION_STATE);
if (StringUtils.isEmpty(sessionState)) {
throw new MSTRUncheckedException("WebIServerSession.getWebIServerSession(): Unable to restore session");
}
//////////////////////////////////////////////
//Step 2. Restore session and ensure session is still live.
WebIServerSession userSession = WebObjectsFactory.getInstance().getIServerSession();
userSession.restoreState(sessionState);
if (!userSession.isAlive()) {
userSession.reconnect();
String newState = userSession.saveState(EnumWebPersistableState.MAXIMAL_STATE_INFO);
cntSvcs.setSessionAttribute(SESSION_STATE, newState);
}
return userSession;
-
Explanation: If something goes wrong during the session creation process, there is not much that can be done. About the only intelligent thing to do here is to clear the session information from the container and throw an exception back to MicroStrategy Web.
Copy} catch (WebObjectsException e) {
//In case of exception, most likely credential has been changed.
//clear session state and old session
cntSvcs.setSessionAttribute(SESSION_STATE, null);
cntSvcs.setSessionAttribute(SSO_TOKEN_NAME, null);
throw new MSTRUncheckedException("WebIServerSession.getWebIServerSession(): Unable to restore session");
}
}
Private methods
The following methods support the work of this sample, but do not illustrate MicroStrategy-specific functionality. To view the code explanations for any of these methods, simply click the "Click here" link for the appropriate method.
The private validate helper method is used to authenticate the user represented by the user token. If this succeeds, validate returns the MicroStrategy user login for the user. Depending on your particular external authentication system, the implementation of this method can vary considerably. The implementation in this sample is more for the sake of completeness than for instruction. Click here to see a simple implementation of the validate method and a high-level explanation for this method.
The private createIServerSession helper method does the actual task of creating an Intelligence Server session. This is a multi-step process because the user password must be randomized with every login for security purposes and new users must be created automatically in MicroStrategy if they do not already exist. In order to accomplish this, an administrator session is needed. Click here to see a detailed explanation of a simple implementation of the createIServerSession method.
The private reconnectISSession helper method does the job of reconnecting the Intelligence Server session. Click here to see the code and explanations for this helper method.
The private closeISSession helper method does the job of closing a session. Click here to see the code and explanation for this helper method.
The private getRandomString helper method provides the simple-minded logic to generate a random password string. Click here to see the code and explanation for this helper method.
Private inner classes
The following classes support the work of this sample, but do not illustrate MicroStrategy-specific functionality. To view the code explanations for either of these classes, simply click the "Click here" link for the apprpriate class.
The private inner ServerHint class holds the information necessary to create an administrator session. It is used internally in the process of finding and loading a cached administrator session. Its main purposes is to provide a key as part of this process. Click here to see the code and explanation for this inner class.
The private innerAdminSessionCacheclass creates an Intelligence Server administrator session.Click here to see the code and explanation for this inner class.