Jira Rest API with OAuth in .NET

TL;DR > Working example / Show me the code

Link to github

It only took a pandemic to start blogging again. My last blog post is from 5 years ago, at that time I still was a student. Currently I’m a software engineer @ intigriti. This is also the reason why I’m writing this article. At intigriti we need JIRA integration with our customers. We are currently in a POC phase with a very nice customer.

This will be written for JIRA Server (but I assume it’s also usable for JIRA Cloud). There are 3 possible ways to authenticate with JIRA:

  • Username / password
  • Cookie
  • OAuth 1.0a

The first one is not usable @ intigriti since we can’t just ask to the customer to send their username & password 😉

The second one is also not usable since we need M2M communication, so we don’t have access to the JIRA cookies of the user.

So the only viable option was OAuth. I have a lot of experience with OAuth 2 and Open Id Connect but I never touched OAuth 1. Let’s say it’s something from before my time, OAuth 2 is already from 2013 🙂

And it doesn’t look like that Atlassian will add OAuth 2.0 any time soon. There is an issue from 2015 that still receives a lot of comments to ‘plz add this’…

Okay let’s get started with the docs: https://developer.atlassian.com/server/jira/platform/oauth/
This all seems to make sense but a .NET example is missing. Also the Atlassian.SDK nuget doesn’t really have a nice explanation on how to use it properly (side note: they even don’t support it)

I found a lot of people struggling to make it work in .NET. Most people just implemented username / password authentication as a result. Imho Atlassian should do something about this since it’s less secure that way. So I’ll write down my findings and a working example 😉

Okay the JIRA side is rather well explained on how to set it up. So I assume you have a public and private key (both in PEM format). And that you configured the AppLink correctly. The only thing you will need is the consumerKey and make sure you uploaded the correct public key.

Step 1: Get consumer secret

var privateKey = File.ReadAllText("jira_privatekey.pem");
// You could also do it without third party nuget
// https://vcsjones.dev/2019/10/07/key-formats-dotnet-3/
var decoder = new OpenSSL.PrivateKeyDecoder.OpenSSLPrivateKeyDecoder();
var keyInfo = decoder.Decode(privateKey);
_consumerSecret = keyInfo.ToXmlString(true);

For this code to work you will need: OpenSSL.PrivateKeyDecoder
You can do it without the nuget checkout: https://vcsjones.dev/2019/10/07/key-formats-dotnet-3/.

This code basically loads the private key and coverts it in a XML file which we need later.

Step 2: Redirect to JIRA

You will need the Atlassian.SDK
This includes a custom created authenticator for RestSharp.

var settings = new OAuthRequestTokenSettings(_url, _consumerKey, _consumerSecret,
                        $"{context.Request.Scheme}://{context.Request.Host}/callback");

A small explanation of the variables:

 _url: The url where your Jira server is running (I'm currently using one through docker-compose)
_consumerKey: The key you used to setup the JIRA AppLink
_consumerSecret: Generated from private key => Step 1

We will also need to add to which url JIRA needs to redirect when the user allowed the integration. In my case this is http://localhost:1234/callback

When we have our settings setup correctly you can now do the following:

var requestToken = await JiraOAuthTokenHelper.GenerateRequestTokenAsync(settings);

This will generate a requestToken that we will need later and a AuthorizeUri. This ‘AuthorizeUri’ is the link were you need the redirect the user to. Since this is a POC and certainly not production ready I’ll save the requestToken in a static dictionary and set the key of the dictionary as cookie value.

 var requestTokenId = Guid.NewGuid();

_requestTokenDb.Add(requestTokenId, requestToken);

context.Response.Cookies.Append("JiraRequestTokenCookie", requestTokenId.ToString());
context.Response.Redirect(requestToken.AuthorizeUri);

Result should be this:

Step 3: Callback

var exists = context.Request.Cookies.TryGetValue("JiraRequestTokenCookie", out var stringRequestTokenId);
if (!exists || !Guid.TryParse(stringRequestTokenId, out var requestTokenId) || !_requestTokenDb.ContainsKey(requestTokenId))
{
    // Incorrect cookie value => Redirect to (/)
    // Code omitted since no real value 😉     
}

var requestToken = _requestTokenDb[requestTokenId];
var verifier = context.Request.Query["oauth_verifier"].ToString();

When JIRA redirects the user back to your application it will add a query param called “oauth_verifier” we will need this param later to request an access token.

And this is where the problem starts. The Atlassian.SDK doesn’t have support to add the “oauth_verifier” param. So I copied the code over from the bitbucket and added a parameter “OAuthVerfier”

public class JiraOAuthAccessTokenSettings
{
    // Copied from https://bitbucket.org/farmas/atlassian.net-sdk/src/master/Atlassian.Jira/OAuth/OAuthAccessTokenSettings.cs

    /// <summary>
    /// Initializes a new instance of the <see cref="Atlassian.Jira.OAuth.OAuthAccessTokenSettings"/> class.
    /// </summary>
    /// <param name="url">The URL of the Jira instance to request to.</param>
    /// <param name="consumerKey">The consumer key provided by the Jira application link.</param>
    /// <param name="consumerSecret">The consumer private key in XML format.</param>
    /// <param name="oAuthRequestToken">The OAuth request token generated by Jira.</param>
    /// <param name="oAuthTokenSecret">The OAuth token secret generated by Jira.</param>
    /// <param name="oAuthVerifier">TODO</param>
    /// <param name="signatureMethod">The signature method used to sign the request.</param>
    /// <param name="accessTokenUrl">The relative URL to request the access token to Jira.</param>
    public JiraOAuthAccessTokenSettings(
        string url,
        string consumerKey,
        string consumerSecret,
        string oAuthRequestToken,
        string oAuthTokenSecret,
        string oAuthVerifier,
        JiraOAuthSignatureMethod signatureMethod = JiraOAuthSignatureMethod.RsaSha1,
        string accessTokenUrl = DefaultAccessTokenUrl)
    {
        Url = url;
        ConsumerKey = consumerKey;
        ConsumerSecret = consumerSecret;
        OAuthRequestToken = oAuthRequestToken;
        OAuthTokenSecret = oAuthTokenSecret;
        OAuthVerifier = oAuthVerifier;
        SignatureMethod = signatureMethod;
        AccessTokenUrl = accessTokenUrl;
    }

And now we can edit the actual implementation to be able to request an access token

/// <summary>
/// Obtain the access token from an authorized request token.
/// </summary>
/// <param name="oAuthAccessTokenSettings">The settings to obtain the access token.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The access token from Jira.
/// Return null if the token was not returned by Jira or the token secret for the request token and the access token don't match.</returns>
public static Task<string> ObtainAccessTokenAsync(JiraOAuthAccessTokenSettings oAuthAccessTokenSettings, CancellationToken cancellationToken)
{
    var authenticator = OAuth1Authenticator.ForAccessToken(
        oAuthAccessTokenSettings.ConsumerKey,
        oAuthAccessTokenSettings.ConsumerSecret,
        oAuthAccessTokenSettings.OAuthRequestToken,
        oAuthAccessTokenSettings.OAuthTokenSecret,
        oAuthAccessTokenSettings.OAuthVerifier);
    authenticator.SignatureMethod = oAuthAccessTokenSettings.SignatureMethod.ToOAuthSignatureMethod();
    
    var restClient = new RestClient(oAuthAccessTokenSettings.Url)
    {
        Authenticator = authenticator
    };
    
    return ObtainAccessTokenAsync(
        restClient,
        oAuthAccessTokenSettings.AccessTokenUrl,
        oAuthAccessTokenSettings.OAuthTokenSecret,
        cancellationToken);
}

It’s also strange to see that RestSharp doesn’t have a constructor that recieves both the OAuthVerifier and the SignatureMethod. I used their integration tests to check out how I needed to implement it and apparently they never needed this scenario in their tests. Since the OAuthVerifier parameter is internal but the SignatureMethod isn’t I still could do what I needed.

var settings = new JiraOAuthAccessTokenSettings(_url, _consumerKey, _consumerSecret, requestToken.OAuthToken, requestToken.OAuthTokenSecret, verifier);
var accessToken = await JiraOAuthTokenHelper.ObtainAccessTokenAsync(settings, CancellationToken.None);

When we finally have the access token:

var jira = Jira.CreateOAuthRestClient(_url, _consumerKey, _consumerSecret, accessToken, requestToken.OAuthTokenSecret);

// JSS is the project key from JIRA 
// The name is for nostalgia reasons
var result = await jira.Issues.GetIssuesFromJqlAsync("project = JSS");
var vm = result.Select(issue => new
{
    Created = issue.Created, Description = issue.Description, Title = issue.Summary,
    Reporter = issue.Reporter, Type = issue.Type.Name, Priority = issue.Priority.Name
});

await context.Response.WriteAsJsonAsync(vm);

This gets all the JIRA issues of the “JSS Project”.

Conclusion

Link to full code: https://github.com/ErazerBrecht/jira_oauth

In the end it isn’t that difficult to do, but I almost spend a whole day searching for it. I hope this will help other devs that have the same problem.

The JAVA and NodeJS example from Atlassian really helped:
JAVA
NodeJS

The NodeJS example is however really outdated, Atlassian also didn’t add the package.json…

The following integrations tests from RestSharp also helped me:
https://github.com/restsharp/RestSharp/blob/dev/test/RestSharp.IntegrationTests/OAuth1Tests.cs