From 0884eb49435c6688494c1692acc1e0002ebbc657 Mon Sep 17 00:00:00 2001
From: Dan Jones <dan.jones@noc.ac.uk>
Date: Fri, 25 Nov 2022 16:11:07 +0000
Subject: [PATCH] refactor: client creds grant and bearer tokens

Use client creds to issue a bearer token
Authenticate subsequent endpoints with authorization header
---
 Pipfile                    |   1 +
 Pipfile.lock               | 116 +++++++++++++++++++++++++++++++++++--
 api.py                     |   8 ++-
 endpoints/auth_resource.py |  25 ++++++++
 endpoints/notify.py        |  24 ++++----
 endpoints/receive.py       |  27 ++++-----
 endpoints/send.py          |  30 ++++------
 endpoints/token.py         |  43 ++++++++++++++
 models/token.py            |  92 +++++++++++++++++++++++++++++
 9 files changed, 311 insertions(+), 55 deletions(-)
 create mode 100644 endpoints/auth_resource.py
 create mode 100644 endpoints/token.py
 create mode 100644 models/token.py

diff --git a/Pipfile b/Pipfile
index 9510ffc..3956649 100644
--- a/Pipfile
+++ b/Pipfile
@@ -12,6 +12,7 @@ flask-restful = "*"
 marshmallow = "*"
 bson = "*"
 flask-cors = "*"
+cryptography = "*"
 
 [dev-packages]
 
diff --git a/Pipfile.lock b/Pipfile.lock
index a8acc9b..5e5da41 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "1147de24391cc36d2688e229ffc68c4a093efa85ef0a957f89206ba6784dc2b3"
+            "sha256": "d216458acf3005c6f459eb1d74f7d5bbda1a7b5e1e042efe855739943c4674d4"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -38,6 +38,75 @@
             "index": "pypi",
             "version": "==0.5.10"
         },
+        "cffi": {
+            "hashes": [
+                "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5",
+                "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef",
+                "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104",
+                "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426",
+                "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405",
+                "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375",
+                "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a",
+                "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e",
+                "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc",
+                "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf",
+                "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185",
+                "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497",
+                "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3",
+                "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35",
+                "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c",
+                "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83",
+                "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21",
+                "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca",
+                "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984",
+                "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac",
+                "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd",
+                "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee",
+                "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a",
+                "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2",
+                "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192",
+                "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7",
+                "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585",
+                "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f",
+                "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e",
+                "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27",
+                "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b",
+                "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e",
+                "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e",
+                "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d",
+                "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c",
+                "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415",
+                "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82",
+                "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02",
+                "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314",
+                "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325",
+                "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c",
+                "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3",
+                "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914",
+                "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045",
+                "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d",
+                "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9",
+                "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5",
+                "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2",
+                "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c",
+                "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3",
+                "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2",
+                "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8",
+                "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d",
+                "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d",
+                "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9",
+                "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162",
+                "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76",
+                "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4",
+                "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e",
+                "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9",
+                "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6",
+                "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b",
+                "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01",
+                "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"
+            ],
+            "version": "==1.15.1"
+        },
         "click": {
             "hashes": [
                 "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
@@ -46,6 +115,38 @@
             "markers": "python_version >= '3.7'",
             "version": "==8.1.3"
         },
+        "cryptography": {
+            "hashes": [
+                "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d",
+                "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd",
+                "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146",
+                "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7",
+                "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436",
+                "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0",
+                "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828",
+                "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b",
+                "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55",
+                "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36",
+                "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50",
+                "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2",
+                "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a",
+                "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8",
+                "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0",
+                "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548",
+                "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320",
+                "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748",
+                "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249",
+                "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959",
+                "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f",
+                "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0",
+                "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd",
+                "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220",
+                "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c",
+                "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"
+            ],
+            "index": "pypi",
+            "version": "==38.0.3"
+        },
         "flask": {
             "hashes": [
                 "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b",
@@ -87,11 +188,11 @@
         },
         "importlib-metadata": {
             "hashes": [
-                "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab",
-                "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"
+                "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b",
+                "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"
             ],
             "markers": "python_version < '3.10'",
-            "version": "==5.0.0"
+            "version": "==5.1.0"
         },
         "itsdangerous": {
             "hashes": [
@@ -195,6 +296,13 @@
             "index": "pypi",
             "version": "==2.3.0"
         },
+        "pycparser": {
+            "hashes": [
+                "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
+                "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
+            ],
+            "version": "==2.21"
+        },
         "pyparsing": {
             "hashes": [
                 "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
diff --git a/api.py b/api.py
index fad511c..0645556 100644
--- a/api.py
+++ b/api.py
@@ -1,22 +1,26 @@
 from flask import Flask
 from flask_restful import Api
-from endpoints.hello import HelloWorld
 from endpoints.clients import Client, ClientList
 from endpoints.receive import Receive
 from endpoints.send import Send
 from endpoints.notify import Notify
+from endpoints.token import Token
 from flask_cors import CORS
+from models.token import TokenModel
+
+token = TokenModel()
+token.setSecret()
 
 app = Flask(__name__)
 api = Api(app)
 CORS(app, resources={r"*": {"origins": "http://localhost:8086"}})
 
-api.add_resource(HelloWorld, "/")
 api.add_resource(ClientList, "/client")
 api.add_resource(Client, "/client/<client_id>")
 api.add_resource(Receive, "/receive")
 api.add_resource(Send, "/send")
 api.add_resource(Notify, "/notify")
+api.add_resource(Token, "/token")
 
 if __name__ == "__main__":
     app.run(debug=True, port=8087)
diff --git a/endpoints/auth_resource.py b/endpoints/auth_resource.py
new file mode 100644
index 0000000..5ebd201
--- /dev/null
+++ b/endpoints/auth_resource.py
@@ -0,0 +1,25 @@
+import json
+from flask_restful import Resource, abort
+from models.token import TokenModel
+
+class AuthResource(Resource):
+
+    def __init__(self): 
+        self.token = TokenModel()
+        with open("clients.json", "r") as clients_file:
+            self.clients = json.load(clients_file)
+
+    def auth(self, request): 
+        allow = False
+        auth = request.headers.get('Authorization', False)
+        if auth:
+            token = auth.split(' ').pop()
+            parsed = self.token.validate(token)
+            if parsed['valid']:
+                client = self.clients.get(parsed['client_id'])
+                if client: 
+                    self.client = client
+                    allow = True
+        if not allow:
+            abort(403, message="Invalid token")
+        return allow
\ No newline at end of file
diff --git a/endpoints/notify.py b/endpoints/notify.py
index ae750ab..a953db8 100644
--- a/endpoints/notify.py
+++ b/endpoints/notify.py
@@ -1,22 +1,21 @@
-from flask_restful import Resource, request, abort
+import json
+from flask_restful import request, abort
 from marshmallow import Schema, fields
 import pika
-import json
+from endpoints.auth_resource import AuthResource
+
 
 class NotifySchema(Schema):
-    client_id = fields.Str(required=True)
-    secret = fields.Str(required=True)
     body = fields.Str(required=True)
 
-class Notify(Resource):
+class Notify(AuthResource):
     clients = None
     schema = None
 
     def __init__(self):
+        super().__init__()
         self.schema = NotifySchema()
-        with open("clients.json", "r") as clients_file:
-            self.clients = json.load(clients_file)
-
+   
     def post(self):
         args = request.get_json()
         errors = self.schema.validate(args)
@@ -29,14 +28,11 @@ class Notify(Resource):
             'topic': 'broadcast',
             'message': body,
         }
-        client_id = args.get("client_id")
-        notify_queue = client_id + "-broadcast"
-        if client_id in self.clients:
-            client = self.clients.get(client_id)
-            if args.get("secret") == client.get("secret"):
-                allow = True
 
+        allow = self.auth(request)
+        
         if allow:
+            notify_queue = self.client['client_id'] + "-broadcast"
             connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
             channel = connection.channel()
             channel.queue_declare(queue=notify_queue, durable=True)
diff --git a/endpoints/receive.py b/endpoints/receive.py
index 86d2487..bc1feba 100644
--- a/endpoints/receive.py
+++ b/endpoints/receive.py
@@ -1,23 +1,22 @@
-from flask_restful import Resource, request, abort
+from flask_restful import request, abort
 from marshmallow import Schema, fields
 import pika
 import json
+from models.token import TokenModel
+from endpoints.auth_resource import AuthResource
 
 
 class ReceiveQuerySchema(Schema):
-    client_id = fields.Str(required=True)
-    secret = fields.Str(required=True)
     max_messages = fields.Int(required=False)
 
 
-class Receive(Resource):
+class Receive(AuthResource):
     clients = None
     schema = None
 
     def __init__(self):
+        super().__init__()
         self.schema = ReceiveQuerySchema()
-        with open("clients.json", "r") as clients_file:
-            self.clients = json.load(clients_file)
 
     def get(self):
         errors = self.schema.validate(request.args)
@@ -25,15 +24,12 @@ class Receive(Resource):
             abort(400, message=str(errors))
 
         messages = []
-        allow = False
         max_messages = request.args.get("max_messages", 10)
-        client_id = request.args.get("client_id")
-        inbox_queue = client_id + "-inbox"
-        if client_id in self.clients:
-            client = self.clients.get(client_id)
-            if request.args.get("secret") == client.get("secret"):
-                allow = True
-
+        
+        allow = self.auth(request)
+        if allow:
+            inbox_queue = self.client['client_id'] + "-inbox"
+    
         if allow:
             connection = pika.BlockingConnection(
                 pika.ConnectionParameters(host="localhost")
@@ -51,7 +47,4 @@ class Receive(Resource):
                     break
             channel.close()
             connection.close()
-
-        else:
-            abort(403, message="Invalid client credentials")
         return messages
diff --git a/endpoints/send.py b/endpoints/send.py
index 17a958e..900e8a5 100644
--- a/endpoints/send.py
+++ b/endpoints/send.py
@@ -1,40 +1,34 @@
-from flask_restful import Resource, request, abort
+import json
+from flask_restful import request, abort
 from marshmallow import Schema, fields
 import pika
-import json
+from endpoints.auth_resource import AuthResource
 
 class SendSchema(Schema):
-    client_id = fields.Str(required=True)
-    secret = fields.Str(required=True)
     body = fields.Str(required=True)
     topic = fields.Str(required=True)
 
-class Send(Resource):
+class Send(AuthResource):
     clients = None
     schema = None
 
     def __init__(self):
+        super().__init__()
         self.schema = SendSchema()
-        with open("clients.json", "r") as clients_file:
-            self.clients = json.load(clients_file)
-
+    
     def post(self):
         args = request.get_json()
         errors = self.schema.validate(args)
         if errors:
             abort(400, message=str(errors))
 
-        allow = False
-        body = args.get("body")
-        topic = args.get("topic")
-        client_id = args.get("client_id")
-        outbox_queue = client_id + "-outbox"
-        if client_id in self.clients:
-            client = self.clients.get(client_id)
-            if args.get("secret") == client.get("secret"):
-                allow = True
-
+        allow = self.auth(request)
+        
         if allow:
+            body = args.get("body")
+            topic = args.get("topic")
+            outbox_queue = self.client['client_id'] + "-outbox"
+        
             connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
             channel = connection.channel()
             channel.queue_declare(queue=outbox_queue, durable=True)
diff --git a/endpoints/token.py b/endpoints/token.py
new file mode 100644
index 0000000..01dbedf
--- /dev/null
+++ b/endpoints/token.py
@@ -0,0 +1,43 @@
+import json 
+from flask_restful import Resource, request, abort
+from marshmallow import Schema, fields
+import pika
+from models.token import TokenModel
+
+
+class TokenQuerySchema(Schema):
+    client_id = fields.Str(required=True)
+    secret = fields.Str(required=True)
+
+
+class Token(Resource):
+    clients = None
+    schema = None
+    model = None
+
+    def __init__(self):
+        self.schema = TokenQuerySchema()
+        self.model = TokenModel()
+        with open("clients.json", "r") as clients_file:
+            self.clients = json.load(clients_file)
+    
+    def get(self):
+        errors = self.schema.validate(request.args)
+        if errors:
+            abort(400, message=str(errors))
+
+        token = None
+        allow = False
+        max_messages = request.args.get("max_messages", 10)
+        client_id = request.args.get("client_id")
+        if client_id in self.clients:
+            client = self.clients.get(client_id)
+            if request.args.get("secret") == client.get("secret"):
+                allow = True
+
+        if allow:
+            token = self.model.get(client_id)
+
+        else:
+            abort(403, message="Invalid client credentials")
+        return token
\ No newline at end of file
diff --git a/models/token.py b/models/token.py
new file mode 100644
index 0000000..21efd85
--- /dev/null
+++ b/models/token.py
@@ -0,0 +1,92 @@
+from cryptography.fernet import Fernet,InvalidToken
+import datetime
+import os
+import json
+
+
+TOKENS = {}
+
+
+class TokenModel():
+    clients = None
+    schema = None
+    key = None
+    fernet = None
+    token_lifetime_hours = None
+    env_lifetime = 'SOAR_TOKEN_LIFETIME'
+    env_secret = 'SOAR_TOKEN_SECRET'
+
+    def __init__(self):
+        self.getFernet()
+        self.token_lifetime_hours = os.getenv(self.env_lifetime, 24)
+    
+    def getFernet(self): 
+        self.fernet = Fernet(self.getKey().encode()) 
+
+    def getKey(self): 
+        key = os.getenv(self.env_secret)
+        print(key)
+        if not key: 
+            key = Fernet.generate_key().decode()
+            os.environ[self.env_secret] = key
+        self.key = key    
+        return self.key
+
+    def setSecret(self): 
+        if not os.getenv(self.env_secret):
+            os.environ[self.env_secret] = self.getKey() 
+
+    def getExpiry(self): 
+        now = datetime.datetime.utcnow()
+        expires = now + datetime.timedelta(hours=self.token_lifetime_hours)
+        return expires.isoformat()
+    
+    def encrypt(self, client_id):
+        try:
+            expiry = self.getExpiry()
+            token_content = {
+                'client_id': client_id,
+                'expiry': expiry 
+            }
+            token = self.fernet.encrypt(json.dumps(token_content).encode()).decode()
+            print(f"Encode: {token}")
+            return {
+                'token': token,
+                'expiry': expiry
+            }
+        except KeyError as e: 
+            return None
+    
+    def decrypt(self, token):
+        try:  
+            print(f"Decode: {token}")
+            content = json.loads(self.fernet.decrypt(token.encode()).decode())
+            return content 
+        except (InvalidToken,KeyError) as e:
+            return None
+
+    def get(self, client_id):
+        response = self.encrypt(client_id)
+        TOKENS[response['token']] = client_id
+        return response 
+
+    def validate(self, token):
+        response = {
+            'valid': False
+        }
+        if token in TOKENS:
+            content = self.decrypt(token)
+            if content:
+                now = datetime.datetime.utcnow()
+                expires = datetime.datetime.fromisoformat(content['expiry'])
+                response['valid'] = expires > now
+                if response['valid']:
+                    response.update(content)
+                else: 
+                    del TOKENS[token]
+            else:
+                del TOKENS[token]
+        return response 
+
+
+
-- 
GitLab