Unverified Commit 0884eb49 authored by Dan Jones's avatar Dan Jones
Browse files

refactor: client creds grant and bearer tokens

Use client creds to issue a bearer token
Authenticate subsequent endpoints with authorization header
parent 410dde69
...@@ -12,6 +12,7 @@ flask-restful = "*" ...@@ -12,6 +12,7 @@ flask-restful = "*"
marshmallow = "*" marshmallow = "*"
bson = "*" bson = "*"
flask-cors = "*" flask-cors = "*"
cryptography = "*"
[dev-packages] [dev-packages]
......
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "1147de24391cc36d2688e229ffc68c4a093efa85ef0a957f89206ba6784dc2b3" "sha256": "d216458acf3005c6f459eb1d74f7d5bbda1a7b5e1e042efe855739943c4674d4"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
...@@ -38,6 +38,75 @@ ...@@ -38,6 +38,75 @@
"index": "pypi", "index": "pypi",
"version": "==0.5.10" "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": { "click": {
"hashes": [ "hashes": [
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
...@@ -46,6 +115,38 @@ ...@@ -46,6 +115,38 @@
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==8.1.3" "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": { "flask": {
"hashes": [ "hashes": [
"sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b", "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b",
...@@ -87,11 +188,11 @@ ...@@ -87,11 +188,11 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab", "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b",
"sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43" "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"
], ],
"markers": "python_version < '3.10'", "markers": "python_version < '3.10'",
"version": "==5.0.0" "version": "==5.1.0"
}, },
"itsdangerous": { "itsdangerous": {
"hashes": [ "hashes": [
...@@ -195,6 +296,13 @@ ...@@ -195,6 +296,13 @@
"index": "pypi", "index": "pypi",
"version": "==2.3.0" "version": "==2.3.0"
}, },
"pycparser": {
"hashes": [
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
"sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
],
"version": "==2.21"
},
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
......
from flask import Flask from flask import Flask
from flask_restful import Api from flask_restful import Api
from endpoints.hello import HelloWorld
from endpoints.clients import Client, ClientList from endpoints.clients import Client, ClientList
from endpoints.receive import Receive from endpoints.receive import Receive
from endpoints.send import Send from endpoints.send import Send
from endpoints.notify import Notify from endpoints.notify import Notify
from endpoints.token import Token
from flask_cors import CORS from flask_cors import CORS
from models.token import TokenModel
token = TokenModel()
token.setSecret()
app = Flask(__name__) app = Flask(__name__)
api = Api(app) api = Api(app)
CORS(app, resources={r"*": {"origins": "http://localhost:8086"}}) CORS(app, resources={r"*": {"origins": "http://localhost:8086"}})
api.add_resource(HelloWorld, "/")
api.add_resource(ClientList, "/client") api.add_resource(ClientList, "/client")
api.add_resource(Client, "/client/<client_id>") api.add_resource(Client, "/client/<client_id>")
api.add_resource(Receive, "/receive") api.add_resource(Receive, "/receive")
api.add_resource(Send, "/send") api.add_resource(Send, "/send")
api.add_resource(Notify, "/notify") api.add_resource(Notify, "/notify")
api.add_resource(Token, "/token")
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, port=8087) app.run(debug=True, port=8087)
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
from flask_restful import Resource, request, abort import json
from flask_restful import request, abort
from marshmallow import Schema, fields from marshmallow import Schema, fields
import pika import pika
import json from endpoints.auth_resource import AuthResource
class NotifySchema(Schema): class NotifySchema(Schema):
client_id = fields.Str(required=True)
secret = fields.Str(required=True)
body = fields.Str(required=True) body = fields.Str(required=True)
class Notify(Resource): class Notify(AuthResource):
clients = None clients = None
schema = None schema = None
def __init__(self): def __init__(self):
super().__init__()
self.schema = NotifySchema() self.schema = NotifySchema()
with open("clients.json", "r") as clients_file:
self.clients = json.load(clients_file)
def post(self): def post(self):
args = request.get_json() args = request.get_json()
errors = self.schema.validate(args) errors = self.schema.validate(args)
...@@ -29,14 +28,11 @@ class Notify(Resource): ...@@ -29,14 +28,11 @@ class Notify(Resource):
'topic': 'broadcast', 'topic': 'broadcast',
'message': body, '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: if allow:
notify_queue = self.client['client_id'] + "-broadcast"
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost")) connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
channel = connection.channel() channel = connection.channel()
channel.queue_declare(queue=notify_queue, durable=True) channel.queue_declare(queue=notify_queue, durable=True)
......
from flask_restful import Resource, request, abort from flask_restful import request, abort
from marshmallow import Schema, fields from marshmallow import Schema, fields
import pika import pika
import json import json
from models.token import TokenModel
from endpoints.auth_resource import AuthResource
class ReceiveQuerySchema(Schema): class ReceiveQuerySchema(Schema):
client_id = fields.Str(required=True)
secret = fields.Str(required=True)
max_messages = fields.Int(required=False) max_messages = fields.Int(required=False)
class Receive(Resource): class Receive(AuthResource):
clients = None clients = None
schema = None schema = None
def __init__(self): def __init__(self):
super().__init__()
self.schema = ReceiveQuerySchema() self.schema = ReceiveQuerySchema()
with open("clients.json", "r") as clients_file:
self.clients = json.load(clients_file)
def get(self): def get(self):
errors = self.schema.validate(request.args) errors = self.schema.validate(request.args)
...@@ -25,15 +24,12 @@ class Receive(Resource): ...@@ -25,15 +24,12 @@ class Receive(Resource):
abort(400, message=str(errors)) abort(400, message=str(errors))
messages = [] messages = []
allow = False
max_messages = request.args.get("max_messages", 10) max_messages = request.args.get("max_messages", 10)
client_id = request.args.get("client_id")
inbox_queue = client_id + "-inbox" allow = self.auth(request)
if client_id in self.clients: if allow:
client = self.clients.get(client_id) inbox_queue = self.client['client_id'] + "-inbox"
if request.args.get("secret") == client.get("secret"):
allow = True
if allow: if allow:
connection = pika.BlockingConnection( connection = pika.BlockingConnection(
pika.ConnectionParameters(host="localhost") pika.ConnectionParameters(host="localhost")
...@@ -51,7 +47,4 @@ class Receive(Resource): ...@@ -51,7 +47,4 @@ class Receive(Resource):
break break
channel.close() channel.close()
connection.close() connection.close()
else:
abort(403, message="Invalid client credentials")
return messages return messages
from flask_restful import Resource, request, abort import json
from flask_restful import request, abort
from marshmallow import Schema, fields from marshmallow import Schema, fields
import pika import pika
import json from endpoints.auth_resource import AuthResource
class SendSchema(Schema): class SendSchema(Schema):
client_id = fields.Str(required=True)
secret = fields.Str(required=True)
body = fields.Str(required=True) body = fields.Str(required=True)
topic = fields.Str(required=True) topic = fields.Str(required=True)
class Send(Resource): class Send(AuthResource):
clients = None clients = None
schema = None schema = None
def __init__(self): def __init__(self):
super().__init__()
self.schema = SendSchema() self.schema = SendSchema()
with open("clients.json", "r") as clients_file:
self.clients = json.load(clients_file)
def post(self): def post(self):
args = request.get_json() args = request.get_json()
errors = self.schema.validate(args) errors = self.schema.validate(args)
if errors: if errors:
abort(400, message=str(errors)) abort(400, message=str(errors))
allow = False allow = self.auth(request)
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
if allow: if allow:
body = args.get("body")
topic = args.get("topic")
outbox_queue = self.client['client_id'] + "-outbox"
connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost")) connection = pika.BlockingConnection(pika.ConnectionParameters(host="localhost"))
channel = connection.channel() channel = connection.channel()
channel.queue_declare(queue=outbox_queue, durable=True) channel.queue_declare(queue=outbox_queue, durable=True)
......
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
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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment