Saturday, April 2, 2016

JIRA REST API: Cookie-based Authentication

Three authentication methods are proposed by the JIRA REST API documentation:
  • Basic Authentication is a simple but not very safe approach. Credentials are sent in the header on every request and encoding to Base64 is not a proper protection in this case; HTTPS connection is required.
  • OAuth authentication - looks a bit complex and requires additional configuration at the JIRA server that is not always possible.
  • Cookie-based Authentication - this approach seems to be the most convinient one: credentials are checked once, then the authentication cookie only is sent on subsequent requests.

However, trying to use the cookie-based authentication I encountered an issue. The approach described in the documentation worked partially: I was able to create a new session and get the response containing the session cookie but all subsequent requests using this session cookie were rejected as unauthorized. Spending some time investigating this I found the cause of the issue: JSESSIONID is not the only cookie which is required.

I am testing on a demo account which JIRA provides for trials on Atlassian Cloud. In my case for the proper authentication are also required:

  • atlassian.xsrf.token - Atlassian Cloud service's Server ID, a securely-generated random string (i.e. token) and a flag that indicates whether or not the user was logged in at the time the token was generated.
  • studio.crowd.tokenkey - authentication cookie which is used for single sign-on (SSO) between Atlassian Cloud applications. Like the JSESSIONID cookie, this cookie also contains a random string and the cookie expires at the end of every session or when the browser is closed.
More details on JIRA API cookies used by Atlassian Cloud are here: https://confluence.atlassian.com/cloud/cookies-744721661.html. I suppose in case of internal hosting of the JIRA server the cookies set may differ so a safe approach seems to be to re-send all cookies the method got during creation of the session.

Below is the example of usage of the cookie-based authentication to retrieve data from JIRA API.

    var baseUrl = //URL to your JIRA server, e.g.  https://<YOUR ACCOUNT>.atlassian.net
    var userName = ...
    var password = ...

    var dataProvider = new JiraApi();
    IDictionary<string, string> cookies;
    if (dataProvider.TryLogin(baseUrl, userName, password, out cookies))
    {
         //just for example: retrieves all issues of the specified type
         dataProvider.SearchForIssues(baseUrl, cookies, "type=Story"); 
         dataProvider.Logout(baseUrl, cookies);
    }

Actually the logout is not a required step; the cookies can be saved somewhere and used until they expired (30 days).

I used RestSharp to keep the below code simple, however the approach should also work with the plain HttpWebRequest. Also for simplicity of the example I used client.Execute instead of more convinient client.Execute<T> which allows to deserialize the response content from JSON to entities.

    
    public class JiraApi
    {
        public bool TryLogin(string baseUrl, string userName, string password, 
              out IDictionary<string, string> cookies)
        {
            var client = new RestClient(baseUrl);

            var request = new RestRequest("/rest/auth/1/session", Method.POST);
            request.RequestFormat = DataFormat.Json;
            request.AddBody(new { username = userName, password = password });

            var response = client.Execute(request);
            if (response.StatusCode == HttpStatusCode.OK)
            {
                cookies = response.Cookies.ToDictionary(x => x.Name, x => x.Value);
                return true;
            }
            else
            {
                //handle the cause of the failure based on information provided 
                //by response.StatusCode and response.Content
                //...

                cookies = null;
                return false;
            }
        }

        public void SearchForIssues(string baseUrl, IDictionary<string, string> cookies, string jql)
        {
            var client = new RestClient(baseUrl);
            var request = new RestRequest("/rest/api/2/search", Method.GET);
            request.RequestFormat = DataFormat.Json;
            request.AddBody(new { jql = jql });

            foreach (var cookie in cookies)
                request.AddCookie(cookie.Key, cookie.Value);

            var response = client.Execute(request);
            if (response.StatusCode == HttpStatusCode.OK)
            {
                var content = response.Content;

                //handle results here
                //...

            }
            else
            {
                //handle the cause of the failure based on information provided 
                //by response.StatusCode and response.Content
                //...
            }
        }

        public void Logout(string baseUrl, IDictionary<string, string> cookies)
        {
            var client = new RestClient(baseUrl);
            var request = new RestRequest("/rest/auth/1/session", Method.DELETE);
            foreach (var cookie in cookies)
                request.AddCookie(cookie.Key, cookie.Value);

            var response = client.Execute(request);
            if (response.StatusCode == HttpStatusCode.NoContent)
            {
                //handle results here
                //...

            }
            else
            {
                //handle the cause of the failure based on information provided 
                //by response.StatusCode and response.Content
                //...
            }
        }

Note that the successful deletion of the session returns NoContent status code instead of OK.

2 comments:

  1. Do I need the paid version to work with the API. Is it possible to do it for free?

    ReplyDelete
    Replies
    1. I tested on a free trial Atlassian Cloud account.

      Delete