Thursday, July 30, 2015

Integrate Atlassian Jira with SSO

Today we will see how can we integrate Jira with our corp SSO. 


JIRA integrate with SSO system Seraph, the Atlassian authentication library. Seraph is a very simple, pluggable J2EE web application security framework developed by Atlassian and used in our products.
Seraph allows you to write custom authenticators which will accept the login credentials of your existing single sign-on system.
So on a very high level, we need to write a custom Authenticator class which should extend DefaultAuthenticator. And then we need to override login() to check if user has a valid Jira session or is authenticated by our corp SSO.
Next how do we do that?? Here are step by step details


  • Create a jira plugin project:
Although this is not mandatory but I prefer this way, by creating plugin project we have access to all Atlassian libraries.
  • Create CustomSSOAuthenticator class which will handle login authentication of Jira: 
CustomSSOAuthenticator class will extend  com.atlassian.seraph.auth.DefaultAuthenticator abstract class and thus have to implement 2 methods: a) Principal getUser(String username) and b) boolean authenticate(Principal user, String password)
Apart from this we also have to override Principal getUser(HttpServletRequest request,
HttpServletResponse response) method which gets called for every authentication

Now some code:

package com.pratik.jira.sso;

import java.security.Principal;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.atlassian.crowd.embedded.api.CrowdService;
import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.seraph.auth.AuthenticatorException;
import com.atlassian.seraph.auth.DefaultAuthenticator;

public class CustomSSOAuthenticator extends DefaultAuthenticator {

//For Logging purpose
private final static Log log = LogFactory
.getLog(CustomSSOAuthenticator.class);

// Randomly generated serial id
private static final long serialVersionUID = -7673520003580149993L;

@Override
protected boolean authenticate(Principal user, String password)
throws AuthenticatorException {
// This method probably never gets called for SSO use case
log.info("Inside custom authenticate. User is " + user + "Password is "
+ password);
return true;
}

@Override
protected Principal getUser(String username) {
// Validating username
log.info("Inside custom getUser. User is " + username);
return getCrowdService().getUser(username);
}

private CrowdService getCrowdService() {
return ComponentAccessor.getCrowdService();
}

public Principal getUser(HttpServletRequest request,
HttpServletResponse response) {
log.info("Inside getUser of CustomSSOAuthenticator");
Principal user = null;

try {

//Check if user is already logged in and has a valid session
if (request.getSession() != null
&& request.getSession().getAttribute(
DefaultAuthenticator.LOGGED_IN_KEY) != null) {
log.info("Session found; user already logged in");
user = (Principal) request.getSession().getAttribute(
DefaultAuthenticator.LOGGED_IN_KEY);
else {

//Get Cookie from SSO (Will write SSOnCookie class in a while)
SSOnCookie ssoCookie = SSOnCookie.getSSOCookie(request);
log.info("Got SSOnCookie " + ssoCookie);

//Check if it is a valid cookie
if (ssoCookie != null && !ssoCookie.isExpired()) {
log.info("Trying seamless Single Sign-on...");

String username = ssoCookie.getLoginId();
log.info("Got username " + username);

if (username != null) {
user = getUser(username);
log.info("Logged in via SSO, with User " + user);

request.getSession().setAttribute(
DefaultAuthenticator.LOGGED_IN_KEY, user);
request.getSession().setAttribute(
DefaultAuthenticator.LOGGED_OUT_KEY, null);
}
else {
log.warn("SSOCookie is null; redirecting");

// user was not found, or not currently valid
return null;
}
}
} catch (Exception e) // catch class cast exceptions
{
log.error("Exception: " + e, e);
}
return user;
}

}

And SSOnCookie class which retrieves cookie values set by Corp SSO. Usually SSO will set authenticated user information as either Domain Cookie or will return to redirected call as encrypted token, what ever be the case we need to read cookie information set by SSO

Code here:


package com.pratik.jira.sso;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class SSOnCookie {

//For logging
private final static Log log = LogFactory.getLog(SSOnCookie.class);

//Variable to hold if cookie is expired
boolean isExpired;

//Login ID of user
String loginId;

public SSOnCookie(String loginId,boolean isExpired){
this.loginId=loginId;
this.isExpired=isExpired;
}

public static SSOnCookie getSSOCookie(HttpServletRequest request)
{
try{
//From request object get cookie details and from cookie get username/email
//Jira takes username and not email for authentication
log.debug("Username----"+userName);

return new SSOnCookie(userName, false);
catch(Exception e){
log.error("Exception -- "+e);

return null;
}
    

public void setExpired(boolean isExpired) {
this.isExpired = isExpired;
}

public void setLoginId(String loginId) {
this.loginId = loginId;
}

public boolean isExpired()
{
return this.isExpired;
}

    /** Return the username implied by the cookie in the request. */
public String getLoginId()
{
return this.loginId;
}

@Override
public String toString() {
return "SSOnCookie [isExpired=" + isExpired + ", loginId=" + loginId
+ "]";

}

}

  • Build/Deploy project. This will create a jar file. Copy jar file to <Jira Installation Dir>/atlassian-jira/WEB-INF/lib
  • Change seraph-config.xml
seraph-config.xml file is located under - <Jira Installation Dir>/atlassian-jira/WEB-INF/classes
Make following changes in file:


<param-name>login.url</param-name> 
<param-value>Your SSO URL</param-value>

<param-name>link.login.url</param-name>
<param-value>Your SSO URL</param-value>

<authenticator class="com.inmobi.pratik.sso.CustomSSOAuthenticator"/>

If you have to implement logout from SSO as well just change logout.url in seraph-config.xml

And then restart your Jira Server... Thats All!!! SSO is now configured with Jira


Some other important points:

  • If you want all login to happen via SSO only then you will have to disable login gadget in jpm.xml (<Jira Installation Dir>/atlassian-jira/WEB-INF/classes/jpm.xml): 
<property>            
<key>jira.disable.login.gadget</key>
<default-value>true</default-value>
<type>boolean</type>
<admin-editable>false</admin-editable>
<sysadmin-editable>false</sysadmin-editable>
</property>

  • In Jira if user directly opens login url which is <jira.corp.abc.com/login.jsp> then SSO does not work and it will display Jira login page to user which will block login via SSO. To stop this we need to put a small hack in login.jsp. In login.jsp just before <html> tag put a check to see if user is logged in. If user is logged in do nothing else redirect user to your SSO page. Something like this:
if (request.getSession() != null
&& request.getSession().getAttribute(
DefaultAuthenticator.LOGGED_IN_KEY) != null) {
//Do nothing
}
else{
response.sendRedirect(redirectURL);
}


Thats all. Do let me know if you have any comments and Happy Coding!!

6 comments:

  1. Very Nice Article. Could you please let me know what are the dependencies required in pom.xml?

    ReplyDelete
  2. Glad that you liked it. It required jira-api and jira-core dependecies, However I would strongly recommend to create plugin project which takes care of all dependencies you require. Creating plugin project is fairly easy:
    Install Atlassian SDK
    Create plugin project
    Import project in eclipse

    ReplyDelete
    Replies
    1. atlassian-seraph and embedded-crowd-api dependencies are required to be added in pom.xml.

      in getSSOCookie method, how can we get the username? looks like some code is missed.

      Delete
    2. Our existing authentication framework is MS AD (LDAP) and we are using saml for sso. Will this plugin work for SAML?

      Delete
  3. Hi Kumar
    I'm a beginner with Jira and wanted to test your classes. When I copy your code I have an issue with the function:

    public static SSOnCookie getSSOCookie(HttpServletRequest request)

    There seems to be an bug in the copied code. Could you please help and send me the corrected one?

    Thank you so much

    ReplyDelete
  4. Every word in this article is well-framed and has answered all the questions before I wanted to ask. I appreciate your eagerness and interest to know more about our organization. Pegasi Media Group provides you the email list of the decision-makers in all types of domains and industries that you are planning to target, which helps you to approach the prospects that are interested and authorized to buy your products. Atlassian Users Email List

    ReplyDelete