cenobites / flask-jsonrpc

Basic JSON-RPC implementation for your Flask-powered sites

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

API browser does not work with flask-jwt

Talkless opened this issue · comments

We use Flask-JWT for authentication. Every JSON-RPC call needs JWT token in Authorization header.

Are there any ideas how to make API Browser work with this kind of authentication?

Maybe it's possible to make it extendable, to be able to add some extra form fields where JWT username/password could be provided?

Also, to extention to perform something extra (/auth request) before main request seems needed. Also modifying request for adding Authorization header...

Once the API Browser has the schema definition, we can overload the UI Template and extends that with some code that will help the API Browser put the JWT Token in the Authorization header. For example,

app.py

browse = JSONRPCBrowe(
   extensions = ['myst_parser'],
   parser = 'markdown',
   ....,
   template_index='./templates/index.html'
)

app = Flask('docstring')
jsonrpc_v1 = JSONRPC(app, '/api/v1', browse=browse)

./templates/index.html

{% extends "index.html" %}
{% block include_script %}
  <script src="//unpkg.com/something@x/some-jwt-bundle.js"></script>
  <script>
    let JWTToken = {};

    // Create an adapter to use the JWT Flask, seems like keycloak-adapter-js

    window.JSONRPCBrowse = JSONRPCBrowse({
        requestInterceptor: (request) => {
          request.headers['Authorization'] = JWTToken.accessToken;
          return request;
        }
      });
  </script>
{% endblock %}
{% endblock %}

It is the generic form, but the API Browse can support the specific provider by extensions, as simple as is:

app.py

browse = JSONRPCBrowe(
   extensions = ['myst_parser', 'flask-jwt'],
   parser = 'markdown',
   ....,
  {'flask_jwt': {
     public_key: '...',
     secret_key: '...',
     ....,
  }}
)

app = Flask('docstring')
jsonrpc_v1 = JSONRPC(app, '/api/v1', browse=browse)

What do you think about that?

It's a bit hard to get grok of your suggestion, but I guess generally ability to extend templates might work, I guess.

This is some quick-and-dirty hack (without understanding how AngularJS magic works) to make it work, by adding authentication form fields and modifying controllers:

diff --git a/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js b/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js
index 5fcd9f3..4df5d7a 100644
--- a/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js
+++ b/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js
@@ -20,6 +20,10 @@
         $scope.response = responseExample;
         $scope.response_object = responseObjectExample;
 
+        window.username = "";
+        window.password = "";
+        window.customer = "";
+
         $scope.$on('App:displayFakeIntro', function(event, display) {
             $scope.showFakeIntro = display;
         });
@@ -108,31 +112,56 @@
         };
     }]);
 
-    App.controller('ResponseObjectCtrl', ['$scope', '$window', '$modal', 'RPC', 'module', function($scope, $window, $modal, RPC, module) {
+    App.controller('ResponseObjectCtrl', ['$scope', '$window', '$modal', 'RPC', 'module', 'serviceUrl', function($scope, $window, $modal, RPC, module, serviceUrl) {
         $scope.module = module;
+        $scope.module.username = $window.username;
+        $scope.module.password = $window.password;
+        $scope.module.customer = $window.customer;
         $scope.$emit('App:displayToolbar', true);
         $scope.$emit('App:breadcrumb', module.name);
 
         var RPCCall = function(module) {
-            var payload = RPC.payload(module);
-            $scope.request_object = payload;
-            $scope.response = undefined;
-            $scope.response_object = undefined;
-            RPC.callWithPayload(payload).success(function(response_object, status, headers, config) { // success
-                var headers_pretty = headers();
-                headers_pretty.data = config.data;
-
-                $scope.response = {status: status, headers: headers_pretty, config: config};
-                $scope.response_object = response_object;
-                $scope.$emit('App:displayContentLoaded', false);
-            }).error(function(response_object, status, headers, config) { // error
-                var headers_pretty = headers();
-                headers_pretty.data = config.data;
 
-                $scope.response = {status_code: status, headers: headers_pretty, config: config};
-                $scope.response_object = response_object;
-                $scope.$emit('App:displayContentLoaded', false);
+            window.username = module.username;
+            window.password = module.password;
+            window.customer = module.customer;
+
+            RPC.generateJWT(module.username, module.password, module.customer).success(function(response_object, status, headers, config) {
+
+                const jwt = response_object['access_token'];
+                console.log("JWT:", jwt);
+
+                var payload = RPC.payload(module);
+                $scope.request_object = payload;
+                $scope.response = undefined;
+                $scope.response_object = undefined;
+
+                RPC.callWithPayload(payload, {method: 'POST', url: serviceUrl, headers: { 'Authorization': "JWT " + jwt }} ).success(function(response_object, status, headers, config) { // success
+                    var headers_pretty = headers();
+                    headers_pretty.data = config.data;
+
+                    $scope.response = {status: status, headers: headers_pretty, config: config};
+                    $scope.response_object = response_object;
+                    $scope.$emit('App:displayContentLoaded', false);
+
+                    }).error(function(response_object, status, headers, config) { // error
+                        var headers_pretty = headers();
+                        headers_pretty.data = config.data;
+
+                        $scope.response = {status_code: status, headers: headers_pretty, config: config};
+                        $scope.response_object = response_object;
+                        $scope.$emit('App:displayContentLoaded', false);
+                });
+
+            }).error(function(response_object, status, headers, config) { // JWT error
+                    var headers_pretty = headers();
+                    headers_pretty.data = config.data;
+
+                    $scope.response = {status_code: status, headers: headers_pretty, config: config};
+                    $scope.response_object = response_object;
+                    $scope.$emit('App:displayContentLoaded', false);
             });
+
         },
         RPCCallModal = function(module) {
             $modal.open({
diff --git a/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/services.js b/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/services.js
index 9b0b287..7a45e59 100644
--- a/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/services.js
+++ b/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/services.js
@@ -116,6 +116,14 @@
 
                     return payload;
                 },
+                generateJWT: function(username, password, customer) {
+                    const options = {
+                        method: 'POST',
+                        url: '/auth',
+                        data: { "username" : customer + ";" + username, "password" : password },
+                    };
+                    return $http(options);
+                },
                 callWithPayload: function(data, options) {
                     var options = options || {method: 'POST', url: serviceUrl};
                     options.data = data;
diff --git a/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html b/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html
index 9b57a5e..b8075bc 100644
--- a/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html
+++ b/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html
@@ -9,7 +9,14 @@
       <div class="modal-body">
         <h5><b>Summary:</b> <span ng-if="!module.summary">None</span><span style="white-space: pre-wrap;">{{module.summary}}</span></h5>
         <ng-form name="nameDialog" novalidate role="form">
-          <div class="form-group input-group-lg">
+          <div class="form-group input-group-sm">
+            <label for="username">Username</label>
+            <input type="text" class="form-control" autocomplete="on" name="username" id="username" ng-model="module.username" ng-keyup="hitEnter($event)" required>
+            <label for="password">Password</label>
+            <input type="password" class="form-control" autocomplete="on" name="password" id="password" ng-model="module.password" ng-keyup="hitEnter($event)" required>
+            <label for="customer">Customer</label>
+            <input type="text" class="form-control" autocomplete="on" name="customer" id="customer" ng-model="module.customer" ng-keyup="hitEnter($event)" required>
+            <hr/>
             <span ng-repeat="param in module.params">
               <label class="control-label" for="course">{{param.name}} -> {{param.type}}: </label><input type="text" class="form-control" name="{{param.name}}" id="{{param.name}}" ng-model="param.value" ng-keyup="hitEnter($event)" required>
               <span class="help-block"></span>

It seems to work. One question, Is every request the client needs to ask for a new JWT Token?

For your example, I think the good decision is the approach of API Browser extension to support that.

It seems to work. One question, Is every request the client needs to ask for a new JWT Token?

No, it does need new token for each request. Simply in order not to check for JWT expiration error (and re-generate transparenlty), I've just made it get a new one on every request. This behavior is only in the Browser of course, not in production. Again, it's a quick-and-very-dirty hack :) .