Unable to "send a signed outbound HTTP request back to the host"

This is my first time creating an Atlassian Connect Express app so please excuse any noobness and correct me if I’m mis-stating or misunderstanding anything.

I created a very basic NodeJS app using ACE to sync Jira’s Description field with custom fields that I added (“Story Description” and “Bug Description”)

I installed the app using the ngrok URL upon server start when running npm start

atlassian-connect.json file for reference:

{
    "key": "takeoff-jira-app",
    "name": "Takeoff Technologies Jira App",
    "description": "Takeoff Technologies custom application for managing Jira.",
    "vendor": {
        "name": "David Chang",
        "url": "https://cartfresh.atlassian.net/people/5cc0638f4573b30ffbeb7443"
    },
    "baseUrl": "{{localBaseUrl}}",
    "links": {
        "self": "{{localBaseUrl}}/atlassian-connect.json",
        "homepage": "{{localBaseUrl}}/atlassian-connect.json"
    },
    "authentication": {
        "type": "jwt"
    },
    "lifecycle": {
        "installed": "/installed"
    },
    "scopes": [
        "READ",
        "WRITE"
    ],
    "modules": {
        "generalPages": [
            {
                "key": "takeoff-jira-app-jira",
                "location": "system.top.navigation.bar",
                "name": {
                    "value": "Takeoff Technologies Jira App"
                },
                "url": "/takeoff-jira-app",
                "conditions": [{
                    "condition": "user_is_logged_in"
                }]
            },
            {
                "key": "takeoff-jira-app-confluence",
                "location": "system.header/left",
                "name": {
                    "value": "Takeoff Technologies Jira App"
                },
                "url": "/takeoff-jira-app",
                "conditions": [{
                    "condition": "user_is_logged_in"
                }]
            }
        ]
    },
    "apiMigrations": {
        "gdpr": true
    }
}

I set up webhooks on issue create/update/delete to hit my ngrok endpoint with the following route configured

route/index.js:

    ...
    // Add issue handler for Jira webhooks                                                                                                                                                                        
    app.post('/issue', addon.authenticate(), (req, res) => {
        var httpClient = addon.httpClient(req);

        var issueKey            = req.body.issue.key;
        var issueType           = req.body.issue.fields.issuetype.name;
        var description         = req.body.issue.fields.description;
        var storyDescription    = req.body.issue.fields['customfield_10345'];
        var bugDescription      = req.body.issue.fields['customfield_10344'];
        var changelogItems      = req.body.changelog.items;

        changelogItems.forEach(function(change) {
            var issueUpdate = {};
            var options = {
                method: 'PUT',
                url: '/rest/api/2/issue/' + issueKey,
                //auth: { bearer: '<access_token>' },                                                                                                                                                             
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json'
                },
                body: {
                    fields: {}
                }
            };
            // Update Description if Story Description was updated and contents don't match                                                                                                                       
            if (change.field == "Story Description" && change.fieldId == "customfield_10345" && change.toString != description) {
                console.log("Updating", issueKey, "Description", "to", change.toString);
                options.body.fields.description = change.toString;
                httpClient.put(options, function(err, res, body) {
                    if (err) {
                        throw new Error(err);
                    }
                    console.log(
                        'Response: ' + res.statusCode + ' ' + res.statusMessage
                    );
                    console.log(body);
                });
            }
            // Update Story Description if Description was updated and issue type equals Story and contents don't match                                                                                           
            if (issueType == "Story" && change.field == "description" && change.toString != storyDescription) {
                console.log("Updating", issueKey, "Story Description", "to", change.toString);
                options.body.fields['customfield_10345'] = change.toString;
                httpClient.put(options, function(err, res, body) {
                    if (err) {
                        throw new Error(err);
                    }
                    console.log(
                        'Response: ' + res.statusCode + ' ' + res.statusMessage
                    );
                    console.log(body);
                });
            }
        });

        res.send(200);
    });
    ...

When I update an issue in my Jira Cloud instance, the webhook triggers and my server receives the request, however, it doesn’t seem to be getting past the addon.authenticate() middleware due to the following error: Authentication verification error (401): Could not find authentication data on request.

Server log:

$ npm start
...
Registered with host at https://cartfresh.atlassian.net/
Please note that timezone, locale, userId and userKey context parameters are deprecated.
See https://ecosystem.atlassian.net/browse/ACEJS-115
{ ctx: 
   { timestamp: 1562287225380,
     webhookEvent: 'jira:issue_updated',
     issue_event_type_name: 'issue_updated',
     user: 
      { self: 'https://cartfresh.atlassian.net/rest/api/2/user?accountId=5cc0638f4573b30ffbeb7443',
        name: 'david.chang',
        key: 'david.chang',
        accountId: '5cc0638f4573b30ffbeb7443',
        emailAddress: 'david.chang@takeoff.com',
        avatarUrls: [Object],
        displayName: 'David Chang',
        active: true,
        timeZone: 'America/New_York',
        accountType: 'atlassian' },
     issue: 
      { id: '19728',
        self: 'https://cartfresh.atlassian.net/rest/api/2/issue/19728',
        key: 'SND-6',
        fields: [Object] },
     changelog: { id: '166550', items: [Object] } } } Authentication verification error (401):  Could not find authentication data on request
POST /issue?user_id=david.chang&user_key=david.chang 401 16.587 ms - 45

I’ve tried running the server with AC_OPTS=no-auth,force-reg however, then httpClient comes up null (TypeError: Cannot read property 'put' of null).

I have both “Enable private listings” and " Enable development mode" checked.

What am I doing wrong?

Hi David,
I believe you are almost there, but I suggest you try the following when creating the httpClient object:

var httpClient = addon.httpClient({ clientKey: req.context.clientKey });

Hope that helps,
Oliver

Thanks for the response @osiebenmarck - it looks like req.context.clientKey is coming up null and so I’m still getting the following error Error: Http client options must specify clientKey.

When I console.log(req), I get the following in context:

  context: 
   { http: null,
     title: 'Takeoff Technologies Jira App',
     addonKey: 'takeoff-jira-app',
     clientKey: '',
     token: '',
     license: undefined,
     localBaseUrl: 'https://9c2ada73.ngrok.io/',
     userId: 'david.chang',
     hostBaseUrl: '',
     hostUrl: '',
     hostStylesheetUrl: '/atlassian-connect/all-debug.css',
     hostScriptUrl: '/atlassian-connect/all-debug.js' },

Did I set up the webhook incorrectly? Or maybe something is wrong with the way I installed the app?

How did you set up the webhook exactly? I just noticed that it is not in your app descriptor, so I guess you set it up yourself in the UI? In that case, you might want your app to register the webhook when it is installed. Basically, this means adding another module to the app descriptor, like so:

"modules": {
        "generalPages": [
            … // as before
        ],
"webhooks": [
      {
        "event": "jira:issue_updated",
        "url": "/issue",
        "excludeBody": false,
        "propertyKeys": [
          "propertyKey",
          "otherPropertyKey"
        ]
      }
    ]
  }
    },

Yes, I set up the webhook via the UI (Jira settings > WebHooks > Create a WebHook):
39%20PM

Getting the same result after having registered the webhook via the app descriptor:

{
    "key": "takeoff-jira-app",
    "name": "Takeoff Technologies Jira App",
    "description": "Takeoff Technologies custom application for managing Jira.",
    "vendor": {
        "name": "David Chang",
        "url": "https://cartfresh.atlassian.net/people/5cc0638f4573b30ffbeb7443"
    },
    "baseUrl": "{{localBaseUrl}}",
    "links": {
        "self": "{{localBaseUrl}}/atlassian-connect.json",
        "homepage": "{{localBaseUrl}}/atlassian-connect.json"
    },
    "authentication": {
        "type": "jwt"
    },
    "lifecycle": {
        "installed": "/installed"
    },
    "scopes": [
        "READ",
        "WRITE"
    ],
    "modules": {
        "webhooks": [
            {
                "event": "jira:issue_created",
                "url": "/issue",
                "excludeBody": false,
                "filter": "",
                "propertyKeys": []
            },
            {
                "event": "jira:issue_updated",
                "url": "/issue",
                "excludeBody": false,
                "filter": "",
                "propertyKeys": []
            }
        ],
        "generalPages": [
            {
                "key": "takeoff-jira-app-jira",
                "location": "system.top.navigation.bar",
                "name": {
                    "value": "Takeoff Technologies Jira App"
                },
                "url": "/takeoff-jira-app",
                "conditions": [{
                    "condition": "user_is_logged_in"
                }]
            },
            {
                "key": "takeoff-jira-app-confluence",
                "location": "system.header/left",
                "name": {
                    "value": "Takeoff Technologies Jira App"
                },
                "url": "/takeoff-jira-app",
                "conditions": [{
                    "condition": "user_is_logged_in"
                }]
            }
        ]
    },
    "apiMigrations": {
        "gdpr": true
    }
}

Hi David,
I just checked it on my dev env; whenever I set up a webhook inside the app’s descriptor, the requests all contain a clientKey. Webhook set up Jira Administration do not (as would be expected).
The last output from console.log(req) you post looks almost correct, but without the data in clientKey and token – which is really weird to say the least. Can you just verify that your latest output is from the following:

 app.post('/issue', addon.authenticate(), (req, res) => {
       
        console.log(req.context);
        var httpClient = addon.httpClient(req.context.clientKey);

        var issueKey            = req.body.issue.key;
        var issueType           = req.body.issue.fields.issuetype.name;

Figured it out! Huge thanks to you @osiebenmarck!!

Registering the webhooks via the app descriptor was what allowed the authentication data to be passed through (ie. Don’t register webhooks for connect apps via the UI).

The issue it didn’t work in the last pass was because ACE doesn’t like null values (and was actually causing my app to get disabled on install):

        "webhooks": [
            {
                ...
                "filter": "",
                "propertyKeys": []
                ...
            }

Simply removing the key/value pairs and reinstalling the app using the updated app descriptor fixed it and everything is working great now! :blush:

Here are the full contents of atlassian-connect.json for reference:

{
    "key": "takeoff-jira-app",
    "name": "Takeoff Technologies Jira App",
    "description": "Takeoff Technologies custom application for managing Jira.",
    "vendor": {
        "name": "David Chang",
        "url": "https://cartfresh.atlassian.net/people/5cc0638f4573b30ffbeb7443"
    },
    "baseUrl": "{{localBaseUrl}}",
    "links": {
        "self": "{{localBaseUrl}}/atlassian-connect.json",
        "homepage": "{{localBaseUrl}}/atlassian-connect.json"
    },
    "authentication": {
        "type": "jwt"
    },
    "lifecycle": {
        "installed": "/installed"
    },
    "scopes": [
        "READ",
        "WRITE"
    ],
    "modules": {
        "webhooks": [
            {
                "event": "jira:issue_created",
                "url": "/issue",
                "excludeBody": false
            },
            {
                "event": "jira:issue_updated",
                "url": "/issue",
                "excludeBody": false
            }
        ],
        "generalPages": [
            {
                "key": "takeoff-jira-app-jira",
                "location": "system.top.navigation.bar",
                "name": {
                    "value": "Takeoff Technologies Jira App"
                },
                "url": "/takeoff-jira-app",
                "conditions": [{
                    "condition": "user_is_logged_in"
                }]
            },
            {
                "key": "takeoff-jira-app-confluence",
                "location": "system.header/left",
                "name": {
                    "value": "Takeoff Technologies Jira App"
                },
                "url": "/takeoff-jira-app",
                "conditions": [{
                    "condition": "user_is_logged_in"
                }]
            }
        ]
    },
    "apiMigrations": {
        "gdpr": true
    }
}

Thank you again @osiebenmarck for taking time to help a random stranger on the internet! PMing you shortly to figure out how I can digitally buy you a coffee/drink as a token of my appreciation :smile:

1 Like