Testing my JWT token with jira rest api v3, and it returns an "requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://mydomain.atlassian.net/rest/api/3/issue/issue_id"

Hi, I am developing an Atlassian-connect addon. here I’m facing an issue while testing my JWT token to GET an issue with the rest API v3. I’m using python libraries of FLASK, atlassian-jwt and here is my code

i’m new to flask, python and atlassian app development. so please dont mind my noob skills :sweat_smile:

import atlassian_jwt
import requests
from flask import Flask, request, jsonify, redirect, render_template

ADDON_KEY = "ADDON_KEY "
clients = {}


if __name__ == '__main__':
    app = Flask(__name__)
# App descriptor
    descriptor = {
        "modules": {},
     "name": "my connect app",
     "description": "Atlassian Connect app",
     "key": "my connect app-V1",
     "baseUrl": "ngrok url",
     "vendor": {
         "name": "Example, Inc.",
         "url": "http://example.com"
     },
	 "lifecycle": {
		"installed": "/register",
	 },
     "authentication": {
         "type": "jwt"
     },
     "apiVersion": 1,
     "modules": {
         "generalPages": [
             {
                 "url": "/what.html",
                 "key": "hello-world",
                 "location": "system.top.navigation.bar",
                 "name": {
                     "value": "Index page"
                 }
             }
         ]
     },
	 "scopes": [
		"read",
		"write"
        ]
    }

    class SimpleAuthenticator(atlassian_jwt.Authenticator):
        def get_shared_secret(self, client_key):
            return clients[client_key]['sharedSecret']

    auth = SimpleAuthenticator()

    @app.route('/', methods=['GET'])
    def redirect_to_descriptor():
        return redirect('/descriptor')


    @app.route('/descriptor', methods=['GET'])
    def get_descriptor():
        
        print("descriptor triggered: \n",
              jsonify(descriptor))
        
        return jsonify(descriptor)

    @app.route('/register', methods=['POST'])
    def register():
        print("route triggered")
        
        global clients
        client = request.get_json()
        # clients[client['clientKey']] = client
        clients['clientKey'] = client['clientKey']
        clients['sharedSecret'] = client['sharedSecret']
        clients['baseUrl'] = client['baseUrl']
        
        print(clients['baseUrl'], " : ", client['baseUrl'])
        return ' ', 204
        
    
    @app.route('/what.html', methods=['GET','POST'])
    def main_page():
        print("\n What method triggeted")
        return render_template('ind.html')


    @app.route('/ping', methods=['GET','POST'])
    def ping():
        print("Ping method triggered")
        print("\n ", clients, "\n")
        
        ping_url =' /rest/api/3/issue/issue_id'
        jwt_authorization = atlassian_jwt.encode_token('GET', ping_url ,clientKey = 
            clients['clientKey'],sharedSecret= clients['sharedSecret'])
        print("\n JWT KEY: ", jwt_authorization,"\n")
        result = requests.get(clients['baseUrl'].rstrip('/') + ping_url, headers={'Authorization': 
            jwt_authorization})

        result.raise_for_status()
        print(result.status_code, result.json())
        print("end Ping method")
        return ' ', 204
    

    app.run(host='127.0.0.1',port=8000,debug=True)

Note that I took this code from the solution provided in this question. and modified it according to my needs. https://community.developer.atlassian.com/t/how-do-i-sign-my-jwt-requests-for-the-rest-interface/524/11

while this code returns a 404 client error, I have a couple of other questions as well.

  1. I’ve looked into some of the other solved questions and they suggested changes in the rest-API URL like
    1.a instead of this

/rest/api/3/issue/issue_id

add Jira/ in front of URL which looks something like this

/jira/rest/api/3/issue/issue_id

However, this didn’t work as it says it is a dead link.

1.b instead of this

/rest/api/3/issue/issue_id

replace version 3 with ‘latest’

/rest/api/latest/issue/issue_id

This returned the same error

  1. When I installed the app in Jira, it returns a JSON which I’m trying to store in the dictionary, for the future purposes, but it says the dictionary is empty after some time
    When installed:

{‘clientKey’: ‘value’, ‘sharedSecret’: ‘value’, ‘baseUrl’: ‘value’}

after some time:

{}

I want to know if it common or is there a way where I can store the JSON values until the app is down by me.

Hi @Harsha1, I haven’t had time to yet review this in depth but I did notice that the clients dictionary isn’t being used as intended. It should be a dictionary that stores a client against the client key:

clients[client['clientKey']] = client

Also the ping_url has a leading space, which it shouldn’t, and issue_id should be an actual issue ID or issue key, not the literal value issue_id. I also notice that you’re not using auth anywhere to extract the clientKey from the authorization header.

I hope that offers some helpful next steps.

Since you’re not familiar with Python or Flask already, depending on what you’re after, you might be better served by starting with one of the batteries-included frameworks: Connect frameworks and tools

Hope some of that helped.

Regards,
James

Hi @jhazelwood, Thank you for your reply.
Firstly, I haven’t used

clients[client[‘clientKey’]] = client

In my program because it wasn’t giving a jwt token when I tried out the code for the first couple of times. It used to give me an exception at

client_key = auth.authenticate(request.method, request.url, request.headers).

Secondly, issue_id I just mentioned it so that people don’t get confused when I provide an actual issue ID.

Thirdly, there is no leading space in my original code in ping_url. It might be a typo :sweat_smile:

However, after your suggestion on extracting the clientkey from the authorization header, I tried your approach again and it worked really good, and 404 Error is gone now. But now I’m facing a new error of

requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://mydomain.atlassian.net/rest/api/latest/issue/TGWS-3

Here is my new updated code with no filters:

import sys

import atlassian_jwt
import requests
from flask import Flask, request, jsonify, redirect, render_template

ADDON_KEY = "ADDON_KEY "
clients = {}

if __name__ == '__main__':
    app = Flask(__name__)
    descriptor = {
        "modules": {},
     "name": "name",
     "description": "Atlassian Connect app",
     "key": "ADDON_KEY ",
     "baseUrl": "https://ea9f6fb8974e.ngrok.io",
     "vendor": {
         "name": "Example, Inc.",
         "url": "http://example.com"
     },
	 "lifecycle": {
		"installed": "/register",
        "enabled": "/ping",
	 },
     "authentication": {
         "type": "jwt"
     },
     "apiVersion": 1,
     "modules": {
         "generalPages": [
             {
                 "url": "/MainPage.html",
                 "key": "hello-world",
                 "location": "system.top.navigation.bar",
                 "name": {
                     "value": "Main Page"
                 }
             }
         ]
     },
	 "scopes": [
		"read",
		"write"
        ]
    }


    class SimpleAuthenticator(atlassian_jwt.Authenticator):
        def get_shared_secret(self, client_key):
            return clients[client_key]['sharedSecret']

    auth = SimpleAuthenticator()

    @app.route('/', methods=['GET'])
    def redirect_to_descriptor():
        return redirect('/descriptor')


    @app.route('/descriptor', methods=['GET'])
    def get_descriptor():        
        return jsonify(descriptor)

    @app.route('/register', methods=['POST'])
    def register():
        print("Register route triggered")
        
        global clients
        client = request.get_json()
        clients[client['clientKey']] = client

        print(clients[client['clientKey']], "\n ", clients.keys(), "\n")
        return '', 204
        
    
    @app.route('/what.html', methods=['GET','POST'])
    def main_page():
        return render_template('MainPage.html')


    @app.route('/ping', methods=['POST'])
    def ping():
        print("Ping method triggered")
        
        client_key = auth.authenticate(request.method, request.url, request.headers)        
        client = clients[client_key]
        
        ping_url = '/rest/api/latest/issue/TGWS-3'
        jwt_authorization = 'JWT %s' % atlassian_jwt.encode_token('GET', ping_url, ADDON_KEY, 
         client['sharedSecret'])
        print("\n JWT KEY: ", jwt_authorization,"\n")
        result = requests.get(client['baseUrl'].rstrip('/') + ping_url, headers={'Authorization': 
          jwt_authorization})

        result.raise_for_status()
        print(result.status_code, result.json())
        print("end Ping method ended")
        return '', 204
    

    app.run(host='127.0.0.1',port=8000,debug=True)

When I am the admin for my domain and the only person to use it, I don’t understand why it’s forbidden for me :neutral_face: , And i don’t find any official documentation in Atlassian for 403

EDIT:
The above code is the solution to my question. Right now it’s doing the job without any exception. Thank you @jhazelwood :smile:

Hi again, @Harsha1, I tweaked just the addon key and issue key and was able to run your code and see the issue response, so I’m not sure what’s going on now. Would you mind creating a new project with a new issue and testing against that issue, to see if it’s specific to the permissions of the project you’re trying to fetch the issue for? It’s worth noting that the app has its own user, so the request from the app won’t come through as being from you.

Hi @jhazelwood, Thank you for your help, I got my desired result. I don’t know how but apparently, now it is working. I just have 1 more question,
can you suggest any way to store the clientKey to use it for future purposes?

I’m not quite sure what you mean by storing it for use for future purposes. If you’re talking about saving it so it survives when the app is restarted or so your data can be shared amongst multiple instances of your app (i.e. not just in a dictionary in memory like the proof-of-concept does) then using a database via something like Flask-SQLAlchemy — Flask-SQLAlchemy Documentation (2.x) seems to be a well-trodden path in the Python ecosystem (I’m no expert here, though).

1 Like

Oh! I think I know what the issue was - sometimes the app user takes a little while to get set up with the required permissions, so maybe it lost the race that time you got a 403.

1 Like

hi @jhazelwood, Yes I want to use it for multiple instances. Your suggestion of using a database is working well. Thank you for all your assistance until now. Really appreciate it :grin:

If you are going to take this route of building a full-blown connect app in Python, please be sure not to let an installation callback overwrite an exsting installation unless it is signed with the existing secret - not doing so would open a big security hole.

Glad that was helpful and it’s coming along well so far :slight_smile: