Custom Security for Add-ons: Building "Upwork Job Post for JIRA"

August 25th 2016 Sameer Shah and Lance Caraccioli (guest author) in Atlassian Connect, Jira

Upwork's mission is to make it easy to get work done and hire freelance help - including development help. Many Upwork customers and freelancers use JIRA Software for development planning. This post shares our lessons learned from building an integration between Upwork and JIRA, including using our own security framework.

Due to the complexity of Upwork's marketplace architecture and the need to ensure proper security and authentication, the team at Upwork needed to create a customized authentication mechanism in Java. This approach allowed us to ensure the smoothest customer experience while maintaining security throughout the job posting flow.

To see what we built check out Upwork Job Post for JIRA.

Connect and OAuth

We used Atlassian Connect, JIRA Software for Cloud APIs, and Upwork Developers OAuth APIs to create the add-on. For building on Atlassian Connect, you need to host your own proxy service which serves the JSON descriptor file as well as HTML. This HTML gets rendered in an iframe in JIRA Software Cloud. Authentication between your proxy service and JIRA Software occurs using JSON Web Tokens.

At Upwork we have an in-house microservice architecture called Agora where different microservices communicate with each other to form the backend for the entire Upwork site. The Agora framework is a wrapper around the dropwizard framework and provides an easy way to deploy and scale services on Amazon Web Services. Because of that, it was an easier choice to create an Agora proxy microservice for hosting the JIRA Software add-on.

Dropwizard provides support for the mustache framework to render HTML and an asset servlet to serve different static assets.

Finally, MongoDB was selected as the database of choice to store the client credentials, user tokens, and the association between the JIRA Software issues and Upwork jobs. We went with MongoDB because it was the most straightforward choice given the existing infrastructure.

Authentication between Proxy Service and JIRA

Atlassian provides a Node.js framework called Atlassian Connect Express which takes care of the whole JWT authentication mechanism for you. However, since we decided to use our own Agora framework, which is written in Java, we had to replicate the authentication mechanism in Java as well.

When an add-on is installed for the first time in JIRA, JIRA sends some basic information like client ID, shared secret, etc. The proxy service stores this information in Mongo after verifying that it's coming from the JIRA cloud server.

After the client's shared secret is stored in the database, every subsequent request from the JIRA Service, made through the Connect framework, contains the JWT token either as a param or as an authorization header. This token is verified using the authorization filter and this filter is called only if the resource API method is annotated with an annotation called @JWTAuthorizationRequired. Also JWT tokens contain other useful information like the client ID and user which gets added in the request context so that APIs can use that info if they need it.

Here is how the JWT token can be extracted:

public String extractJwt(ContainerRequest request) {
String jwt = request.getQueryParameters().getFirst(JwtConstants.JWT_PARAM_NAME);
    if (jwt == null) {
        Iterable<String> headers = request.getRequestHeader(JwtConstants.HttpRequests.AUTHORIZATION_HEADER);
        if (headers != null && headers.iterator().hasNext()) {
            for (String header : headers) {
                String authzHeader = header.trim();
                String first4Chars = authzHeader.substring(0, Math.min(4, authzHeader.length()));
                    if (JWT_AUTH_HEADER_PREFIX.equalsIgnoreCase(first4Chars)){
                        return authzHeader.substring(4);
                    }
                }
            }
        }
        return jwt;
    }

For JWT token verification, we used the Nimbus Jose JWT library. Atlassian Connect provides a good example to verify and extract JWT tokens.

AJAX Requests and Session Tokens

Finally, in some cases when the request is not made through the Atlassian Connect framework, JWT tokens won't get added to the request. For these cases we need to create the session token and add it to the request. Session tokens could be sent as the query param or authorization header. The following code shows how to create a session JWT token that is valid for some time. We kept the validity for only 30 minutes. This expiry time is embedded inside the session token. Apart from that much more useful information can be embedded inside the JWT session token as a claims object. I embedded information like issueId, projectId, userKey etc. which can be used to identify the information about the user and JIRA on the server side when the session token is passed in AJAX.

import com.atlassian.jwt.core.writer.JsonSmartJwtJsonBuilder;
import com.atlassian.jwt.writer.JwtJsonBuilder;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.atlassian.jwt.core.NimbusUtil;
import com.nimbusds.jose.Payload;
import com.atlassian.jwt.SigningAlgorithm;


public final static String JWT_CLAIMS_CONTEXT = "context";

private String createSessionToken(JwtClaims jwtClaims, ClientInfo clientInfo) {
        long issuedAt = System.currentTimeMillis() / 1000L;
        long expiresAT = issuedAt + 1800L; // 30 minutes of token expiration
        JwtJsonBuilder jwtJsonBuilder = new long issuedAt = System.currentTimeMillis() / 1000L;
        long expiresAT = issuedAt + 1800L; // 30 minutes of token expiration
        JwtJsonBuilder jwtJsonBuilder = new JsonSmartJwtJsonBuilder().issuedAt(issuedAt).expirationTime(expiresAT)
                .issuer(jwtClaims.getIss()).subject(jwtClaims.getSub())
                .claim(JWT_CLAIMS_CONTEXT, jwtClaims.getContext());
        String jwtbuilt = jwtJsonBuilder.build();
        String jwtToken = generateJwsObject(jwtbuilt, clientInfo.getSharedSecret()).serialize();
        return jwtToken;

    }
private JWSObject generateJwsObject(String payload, String sharedSecret) {
    JWSHeader header = new JWSHeader.Builder(NimbusUtil.asNimbusJWSAlgorithm(SigningAlgorithm.HS256))
                .type(new JOSEObjectType(JWT)).build();
        // Create JWS object
        JWSObject jwsObject = new JWSObject(header, new Payload(payload));

        try {
            jwsObject.sign(new MACSigner(sharedSecret));
        } catch (JOSEException e) {
            throw new JwtSigningException(e);
        }
        return jwsObject;
    }

This is how the JwtClaims class and the ClientInfo class above are POJO objects, containing information about the user, JIRA and client instance of JIRA.

Once the session token is generated we need to pass it in each and every AJAX request made by the add-on so that we can validate the request. We decided to use the layout template to embed the session token. This layout template is part of each and every other template and hence the session token was available in each and every AJAX request as the Authorization header. The following code snippet shows how a session token can be added in each and every AJAX request.

<script type="text/javascript">
  (function() {
    $.ajaxSetup({
      beforeSend : function(xhr) {
        xhr.setRequestHeader('Authorization',
            "JWT {{sessionToken}}");
      }
    });
  })();
</script>

Now once you have the session token at the server, you can extract useful information like user and client information to associate them in the database with various other objects - like an Upwork job post in this case.

Creating the Upwork job post

Upwork APIs are exposed through OAuth. You can request the OAuth key and the secret for using the APIs at the following link. Once the key and secret are obtained, we use the Upwork Java OAuth APIs to make requests. Here's the API link we used to create a job post on Upwork. https://developers.upwork.com/?lang=java#jobs\_post-job

When a user logs in and views the issue details, the Atlassian JIRA framework makes a request to the proxy service to get the details about the add-on. A Jwt token is automatically added to the request which contains other useful information like userKey, clientKey and validation information for the request.

Once Proxy service receives the request for the webPanel HTML, it tries to find whether there are any access tokens associated with the userKey and clientKey in the database. If not, the "Connect to Upwork" button is presented to the user to share the access tokens with the proxy service.

OAuth integration details

The add-on uses OAuth 1.0 (Three Legged flow) to obtain authorization on behalf of Upwork users. Only the request token is exposed on the client side. The final leg of the OAuth flow is completed by the add-on's server side implementations where it receives the OAuth access token. Following is the flow on how the tokens can be associated with the user.

  1. When a User clicks on "Connect to Upwork" on the issue details screen, a request is made to the proxy service through AJAX for the Authorization url and other relevant data. Proxy service uses Upwork Java Oauth APIs to create the authorization url. You can see the relevant code here.
  2. Once the authorization url is retrieved a window popup is created with the authorization url.
  3. The User enters their credential which finally results in the calling of callback Url with the oauthVerifier data. You can use this oauthVerifier data to get back the access token and access secret from Upwork. Relevant example to do that is here.
  4. Once the access token and access secret is obtained then all the information is associated with the clientKey, userKey and stored in a Mongo database. You can easily obtain the clientKey and userKey from the session token information that every AJAX requests carries with it. Please read the way to pass session token in each AJAX requests here.

Posting a job to Upwork

Once authorization has been granted, the access token is then associated with the JIRA user by storing that association in MongoDB. Now whenever a request for webPanel HTML is received, the user is presented with "Post this Job To Upwork" button. When the user clicks on the button a dialog box is created which will ask the user to put all the relevant data related to that job.

Once a user fills out all the details in the job post dialog box and submits, an AJAX request is made to proxy service to post job to Upwork and associate the job details to issueKey and clientKey so that next time the user is shown the job posted to corresponding issue.

Viewing the job posted to Upwork

When the request to webPanel HTML is made and Proxy Service finds that there is an associated job reference to issueKey and clientKey, it retrieves that job reference from the database. Now the HTML that is returned back for the webPanel is "View Job" link. This link is created using the jobPublicUrl that you see in the UpworkJobReference collection above.

Check it out

Check it out

Interested in trying the Upwork Job Post for JIRA? You can add it to your JIRA environment on the Atlassian Marketplace.