Integrating MSTR with OIDC
I previously wrote about integrating MSTR with SAML.
In some cases, OpenID Connect (OIDC) may be a better option than SAML. In particular, OIDC gives you a way to authenticate and simultaneously obtain an access token for API calls.
MSTR does not support OIDC officially, but it looks like you can use a similar approach to implementing SAML. The SAML support wraps MSTR with an open-source library (Spring Security) that intercepts requests, redirects users who are not authenticated, and propagates the authentication context to MSTR through the HTTP session.
I found Spring Security too complex to set up for OIDC via XML; more importantly I could not find an example on the Web that used Spring Security for OIDC to wrap existing servlets, without Spring Boot. I did find the pac4j library which is much easier to get started with.
Here’s what I did as a proof-of-concept:
- Add Maven dependencies for pac4j libraries (I’m using WAR overlays for customizations):
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>j2e-pac4j</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oidc</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
<scope>provided</scope>
</dependency>
- Add filters to web.xml:
<!-- BEGIN Pac4j config -->
<filter>
<filter-name>OidcFilter</filter-name>
<filter-class>org.pac4j.j2e.filter.SecurityFilter</filter-class>
<init-param>
<param-name>configFactory</param-name>
<param-value>bill.OidcConfigurationFactory</param-value>
</init-param>
<init-param>
<param-name>clients</param-name>
<param-value>OidcClient</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>OidcFilter</filter-name>
<url-pattern>/servlet/mstrWeb</url-pattern>
</filter-mapping>
<filter>
<filter-name>callbackFilter</filter-name>
<filter-class>org.pac4j.j2e.filter.CallbackFilter</filter-class>
<init-param>
<param-name>defaultUrl</param-name>
<param-value>/</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>callbackFilter</filter-name>
<url-pattern>/callback</url-pattern>
</filter-mapping>
<filter>
<filter-name>postLoginFilter</filter-name>
<filter-class>bill.ProfileFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>postLoginFilter</filter-name>
<url-pattern>/servlet/mstrWeb</url-pattern>
</filter-mapping>
<!-- END Pac4j config -->
- Create config class to set up pac4j OIDC client:
// bill.OidcConfigurationFactory
package bill;
public class OidcConfigurationFactory implements ConfigFactory {
@Override
public Config build(Object... arg0) {
OidcConfiguration oidcConfig = new OidcConfiguration();
oidcConfig.setClientId("YOUR CLIENT ID");
oidcConfig.setSecret("YOUR CLIENT SECRET");
oidcConfig.setDiscoveryURI("https://your-sso-server/.well-known/openid-configuration");
OidcClient<OidcProfile> client = new OidcClient<>(oidcConfig);
final Clients clients = new Clients("http://YOUR-URL/callback", client);
final Config config = new Config(clients);
return config;
}
}
- Create filter to intercept request post-authentication and put object in session for MSTR:
package bill;
import com.microstrategy.auth.SSOUserInfo;
public class ProfileFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// get current user from pac4j post-authentication
final WebContext context = new J2EContext((HttpServletRequest) request, (HttpServletResponse) response);
final ProfileManager<OidcProfile> pm = new ProfileManager<>(context);
final OidcProfile userProfile = pm.get(true).get();
final String oidcUsername = userProfile.getUsername();
HttpSession session = ((HttpServletRequest) request).getSession();
SSOUserInfo obj = ((SSOUserInfo)session.getAttribute("SSOUserInfo"));
if (obj == null || obj.getUserId() != oidcUsername) {
// need to create SSOUserInfo object
obj = new OidcSSOUserInfo(userProfile);
session.setAttribute("SSOUserInfo", obj);
}
chain.doFilter(request, response);
}
// + empty init, destroy
}
package bill;
import org.pac4j.oidc.profile.OidcProfile;
import com.microstrategy.auth.SSOUserInfo;
public class OidcSSOUserInfo implements SSOUserInfo {
private static final long serialVersionUID = -7806359799358590301L;
private String userId;
private List<Group> groups;
private String distinguishedName;
private boolean isValid;
private String displayName;
private String email;
public OidcSSOUserInfo(OidcProfile profile) {
this.userId = profile.getUsername();
this.groups = new ArrayList<Group>();
this.distinguishedName = "";
this.isValid = true;
this.displayName = profile.getDisplayName();
this.email = profile.getEmail();
}
@Override
public String getUserId() {
return this.userId;
}
@Override
public List<Group> getGroups() {
return this.groups;
}
@Override
public String getDistinguishedName() {
return this.distinguishedName;
}
@Override
public boolean isValid() {
return this.isValid;
}
@Override
public String getDisplayName() {
return this.displayName;
}
@Override
public String getEmail() {
return this.email;
}
}
- If you want to do any JIT provisioning of the user in MSTR, you can do it the same as with SAML: get the
SSOUserInfo
object from session and provision the user inhandlesAuthenticationRequest
:
public class ExternalSecurity extends DefaultExternalSecurity {
@Override
public int handlesAuthenticationRequest(RequestKeys reqKeys, ContainerServices cntSvcs, int arg2) {
SSOUserInfo obj = ((SSOUserInfo)cntSvcs.getSessionAttribute("SSOUserInfo"));
String userId = obj.getUserId();
// ... provision user ...
return super.handlesAuthenticationRequest(reqKeys, cntSvcs, arg2);
}
}