Integrate MSTR with Okta via SAML
MicroStrategy’s directions for enabling single sign-on with SAML
are actually pretty good. MSTR bundles Spring Security with SAML support and provides directions for how to
enable it by editing web.xml
.
On the Okta side, a few things that were non-obvious. First, configuring a SAML application in Okta requires going switching to the “classic” UI, which you might miss if you’re reading the directions too quickly.
Second, Okta does not give you a way to upload SP metadata. So I had to copy the fields from the MSTR-generated SP metadata into the Okta form fields:
- “Single sign on URL” in Okta should come from the
AssertionConsumerService
in the SP metadata. This will be a URL ending in/saml/SSO
. - “Audience URI” in Okta will come from the entity ID in the SP metadata and will look like the base URL of your application.
- Under “advanced settings” the signature algorithm should be set to “RSA-SHA256” to match
MstrSamlConfig.xml
Okta does give you IdP metadata that you can paste into the IDPMetadata.xml
, which will include the Okta IdP
URLs and the certificate containing the public key to validate the SAML response signature.
Admin screens
Another important thing to remember: MSTR SAML integration will take over all authentication including admin
screens (/mstrWebAdmin
). All requests are intercepted by Spring Security before they get to MSTR. This means
-
your MSTR Web admin users need to be full-fledged Okta users and authenticate through Okta rather than
tomcat-users.xml
(this is on the whole, an improvement in terms of security) -
MSTR Web admins need to belong to a group called “MSTRAdmin” in Okta and you need to add the same group name to the
adminGroups
element inMstrSamlConfig.xml
-
Additionally, you need to make sure Okta exposes the user groups under “group attribute statements”, and you need to edit the
groupAttributeName
element inMstrSamlConfig.xml
to match the attribute coming from Okta.
These points about configuring groups were not so well documented.
Customization
I found that I could continue to use ESM modules with SAML to inject custom behavior at login, such as creating or
updating MSTR users on demand – for example, changing group memberships dynamically. I overloaded
handlesAuthenticationRequest
to inject my custom behavior, and return super.handlesAuthenticationRequest
at the
end. The default security handler will create the session properly based on the SSO user; MSTR’s handlers registered
with Spring Security (SAMLProcessingFilterWrapper
) will set the SSO user in the HTTP session for MSTR’s default
trusted auth handlers to see.
I also experimented with dynamically changing the IdP URL in Okta to delegate authentication to a separate
third-party IdP (federated SSO) instead of Okta handling the username and password. Effectively what I was trying to
do was replace the URL in the IDPMetadata.xml
at runtime with a slightly different URL - pre-pending
/sso/saml2/:idpId
into the Okta URL before /app/:app-location/:appId/sso/saml
In order to do this, I made a subclass of WebSSOProfileImpl
and overloaded sendMessage
to look at the
HTTP request parameters and conditionally replace the Endpoint
with a wrapped object that replaces
the URL specified in the IdP metadata. There is probably a better way to do this, though.
public class WebSSOProfile extends org.springframework.security.saml.websso.WebSSOProfileImpl {
@Override
protected void sendMessage(SAMLMessageContext context, boolean sign)
throws MetadataProviderException, SAMLException, MessageEncodingException {
String param = ((HttpServletRequestAdapter)context.getInboundMessageTransport()).getParameterValue("foo");
final Endpoint endp = context.getPeerEntityEndpoint();
if (param != null && param.equals("bar")) {
String newUrl = endp.getLocation().replace("/app", "/sso/saml2/my_idp_id/app");
context.setPeerEntityEndpoint(new EndpointWrapper(endp, newUrl));
}
super.sendMessage(context, sign);
}
private static class EndpointWrapper implements Endpoint {
private final Endpoint delegate;
private final String overrideLocation;
public EndpointWrapper(Endpoint delegate, String overrideLocation) {
this.delegate = delegate;
this.overrideLocation = overrideLocation;
}
public String getLocation() {
return this.overrideLocation;
}
// ... more delegate methods ...
}
}