From a1a18d219b096f36c60e1dbcb2310665263014c2 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 6 Oct 2021 19:58:25 +0000 Subject: [PATCH] refactor(server): use server from Docker image --- .devcontainer.json | 2 +- .vscode/launch.json | 15 -- localhost/data/server.json | 21 --- localhost/docker-compose.yml | 9 +- localhost/keys/server.key | 27 ---- localhost/server/Dockerfile | 9 -- localhost/server/app.py | 219 ---------------------------- localhost/server/requirements.txt | 5 - localhost/server/static/tokenize.js | 3 - 9 files changed, 2 insertions(+), 308 deletions(-) delete mode 100644 localhost/data/server.json delete mode 100644 localhost/keys/server.key delete mode 100644 localhost/server/Dockerfile delete mode 100644 localhost/server/app.py delete mode 100644 localhost/server/requirements.txt delete mode 100644 localhost/server/static/tokenize.js diff --git a/.devcontainer.json b/.devcontainer.json index 544f3056f..72f58a7aa 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -3,7 +3,7 @@ "name": "cal-itp/benefits", "dockerComposeFile": ["./localhost/docker-compose.yml"], "service": "dev", - "runServices": ["dev", "docs", "server"], + "runServices": ["dev", "docs"], "workspaceFolder": "/home/calitp/app", "postStartCommand": ["/bin/bash", "bin/init.sh"], "postAttachCommand": ["/bin/bash", "localhost/bin/init.sh"], diff --git a/.vscode/launch.json b/.vscode/launch.json index daf7b188c..c08db8c17 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,21 +14,6 @@ "0.0.0.0:8000" ], "django": true - }, - { - "name": "Flask: Eligibility Verification Server", - "type": "python", - "request": "attach", - "connect": { - "host": "localhost", - "port": 5789 - }, - "pathMappings": [ - { - "localRoot": "${workspaceFolder}/localhost/server", - "remoteRoot": "/usr/src/server" - } - ] } ] } diff --git a/localhost/data/server.json b/localhost/data/server.json deleted file mode 100644 index ecfc83784..000000000 --- a/localhost/data/server.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "auth_header": "X-Server-API-Key", - "auth_token": "server-auth-token", - "token_header": "Authorization", - "jwe_cek_enc": "A256CBC-HS512", - "jwe_encryption_alg": "RSA-OAEP", - "jws_signing_alg": "RS256", - "request_access": "REQUEST_ACCESS" - }, - "merchants": [ - "abc", - "deftl" - ], - "users": { - "A1234567": ["Garcia", ["type1"]], - "B2345678": ["Hernandez", ["type2"]], - "C3456789": ["Smith", []], - "D4567890": ["Jones", ["type1", "type2"]] - } -} diff --git a/localhost/docker-compose.yml b/localhost/docker-compose.yml index 420b70dd5..2d34750b7 100644 --- a/localhost/docker-compose.yml +++ b/localhost/docker-compose.yml @@ -54,16 +54,9 @@ services: - ../:/home/calitp/app:cached server: - build: ./server - image: eligibility_verification_server:dev - command: python -m debugpy --listen 0.0.0.0:5678 app.py - environment: - - FLASK_APP=app.py - - FLASK_DEBUG=1 - - FLASK_ENV=development + image: ghcr.io/machikoyasuda/eligibility-server:test ports: - "5000" - - "5678" volumes: - ./server:/usr/src/server:cached - ./data:/usr/src/server/data diff --git a/localhost/keys/server.key b/localhost/keys/server.key deleted file mode 100644 index f12a87705..000000000 --- a/localhost/keys/server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAyYo6Pe9OSfPGX0oQXyLAblOwrMgc/j1JlF07b1ahq1lc3FH0 -XEk3Dzqbt9NuQs8hz6493vBNtNWTpVmvbGe4VX3UjhpEARhN3m4jf/Z2OEuDt2A9 -q19NLSjgeyhieLkYLwN1ezYXrkn7cfOngcJMnGDXp45CaA+g3DzasrjETnKUdqec -CzJ3FJ/RRwfibrju7eS/8s6H03nvydzeAJzTkEv7Fic2JJEUhh2rJhyLxt+qKkIY -eBG+5fBri4miaS8FPnD/yjZzEAFsQc7n0dGqDAhSJS8tYNmXFmGlaCfRUBNV3mvO -x0vFPuH21WQ5KKZxZP0e64/uQdotbPIImiyRJwIDAQABAoIBAQCt0ezXe+yOtZQS -nSMvmh5TSRTogBMZZyxtrFdVeGcpDIKddoWFjpPRK6Af1FeVgWXM459zBthOLaIQ -iyBUI8SE32iSQq8CLr8CJwWxGJTvipmIb5XglupOF6I8NiFvs1vbOGV7pbSY2i/m -INoIfNZsTM3SMkytyUTYjhek6txMNtc2yi/3HIVhpEaP8sZrufVGXFbLBOUKgjZC -h7la/jeSOfb48xoZ8wRq/MHQ1dedH5M19voxEBAcrZlIYiqd4cTr724NQHOJZXNf -frVq89jKqRvqkPblCPaXqwk8wBfVQyH9LFLbnul2QTxbFRxLXNeoO9qc1ZDwqSXF -7uRam8y5AoGBAPWs0l2Iilbo+sCSuZK2numdXxnFiJTVuipHpmJXAbKZY/CvayAQ -pz/mwX34kpTqN9dotnSJYv8y+HQdfdMrPGKQ96RVsl0HJbWHtbiAPtHRXo6gJYho -th1BhBa1NjJfTXeO7ulPT7OMmKRWC9CEk/OX+rlcHOmpuuebOPKFiSLlAoGBANIC -kCPL1Ol4sP1RkcDEu06+bqUdi4QvKSgHBmzLb5w+0Ufl8ay3Zp64p4rGMd29L7IV -wTXPl/B4TdpDKYw84bcsXE2NWfdT6kDaIMWCuiB/iJXTpntHVejRyrd3dz7jwHfy -PaD5k+KbN2XROIkag0xg7IRmjhJLN5ZxIJIvgScbAoGBAPMmA+J8w+Z2mc7EqRQy -2J8AmWIpZh9gVOuJlHxZ/p0kQYyyIUVQFighm7mwrmriUThKM+KtIyTO7qYFlkXM -0ev/7IliI7D85O6AjXM4wnPpUzu39s3GTRAxiqjq2uQJ/OLqvTx+ubRL37suSm0q -+j+qWITiTN9alFisATXOwkadAoGBAL6mEwJcHZohtdMSBNZSApS2ri15B9nlEmDD -F+MWP+lA4a56og4gpKl8iqShzk01XSI3O6JFJfLo1AxLomEsN+CZBeZlZwHvjR54 -pv2G8r9j57PUYzNRDD2CjpxFeNx/149MOwRy7fzu2bi12bQlfIKPDsgXbexPmlQZ -uO7c70t3AoGAFrdmmr0Ygt8/b1/j7NwvdaDcQj2uadz0sbqmzBFQSEgbRpD9JRC2 -d21vhv00lZ5VwJ+Bgr35zZ2LeNna1+phlj+rySSHNtz/iDplMMZQvyIHpoUMaccp -Trt9yCdC1nTavTHbChT4AYkXR87g0EHFhs5w20ILFpPHT1NAARonkJo= ------END RSA PRIVATE KEY----- diff --git a/localhost/server/Dockerfile b/localhost/server/Dockerfile deleted file mode 100644 index a54ed9866..000000000 --- a/localhost/server/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.9-slim -ENV PYTHONUNBUFFERED 1 - -EXPOSE 5000 - -WORKDIR /usr/src/server - -COPY requirements.txt requirements.txt -RUN pip install -r requirements.txt diff --git a/localhost/server/app.py b/localhost/server/app.py deleted file mode 100644 index fbd07384b..000000000 --- a/localhost/server/app.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Simple Test Eligibility Verification API Server. -""" -import datetime -import json -import re -import time - -from flask import Flask -from flask_restful import Api, Resource, reqparse -from jwcrypto import jwe, jwk, jws, jwt - - -with open("./keys/server.key", "rb") as pemfile: - server_private_key = jwk.JWK.from_pem(pemfile.read()) -with open("./keys/client.pub", "rb") as pemfile: - client_public_key = jwk.JWK.from_pem(pemfile.read()) - - -class Database: - """Simple hard-coded server database.""" - - def __init__(self): - with open("data/server.json") as f: - data = json.load(f) - self._config = data["config"] - self._merchants = data["merchants"] - self._users = data["users"] - - def check_merchant(self, merchant_id): - """Check if the data matches a record in the database.""" - return merchant_id in self._merchants - - def check_user(self, key, user, types): - """Check if the data matches a record in the database.""" - if ( - len(types) < 1 - or key not in self._users - or self._users[key][0] != user - or len(set(self._users[key][1]) & set(types)) < 1 - ): - return [] - - return list(set(self._users[key][1]) & set(types)) - - @property - def auth_header(self): - return self._config["auth_header"] - - @property - def auth_token(self): - return self._config["auth_token"] - - @property - def token_header(self): - return self._config["token_header"] - - @property - def jwe_cek_enc(self): - return self._config["jwe_cek_enc"] - - @property - def jwe_encryption_alg(self): - return self._config["jwe_encryption_alg"] - - @property - def jws_signing_alg(self): - return self._config["jws_signing_alg"] - - @property - def request_access(self): - return self._config["request_access"] - - -app = Flask(__name__) -api = Api(app) - - -class MerchantAuthToken(Resource): - - db = Database() - - def _bad_request(self): - return {"status": "400", "message": "Invalid request payload"} - - def _check_payload(self): - """Ensure correct request payload""" - req_parser = reqparse.RequestParser() - req_parser.add_argument("request_access", required=True) - args = req_parser.parse_args() - - if args.get("request_access") == self.db.request_access: - return True - else: - return False - - def _not_found(self): - return {"status": "404", "message": "Merchant doesn't exist"} - - def _token(self): - return {"access_token": self.db.auth_token, "token_type": "Bearer", "expires_in": 60} - - def post(self, merchant_id): - """Respond to an auth token request.""" - if self.db.check_merchant(merchant_id): - if self._check_payload(): - return self._token(), 200 - else: - return self._bad_request(), 400 - else: - return self._not_found(), 404 - - -class Verify(Resource): - - db = Database() - - def _check_headers(self): - """Ensure correct request headers.""" - req_parser = reqparse.RequestParser() - req_parser.add_argument(self.db.token_header, location="headers", required=True) - req_parser.add_argument(self.db.auth_header, location="headers", required=True) - headers = req_parser.parse_args() - # verify auth_header's value - if headers.get(self.db.auth_header) == self.db.auth_token: - return headers - else: - return False - - def _get_token(self, headers): - """Get the token from request headers""" - token = headers.get(self.db.token_header, "").split(" ") - if len(token) == 2: - return token[1] - elif len(token) == 1: - return token[0] - else: - raise ValueError("Invalid token format") - - def _get_token_payload(self, token): - """Decode a token (JWE(JWS)).""" - try: - # decrypt - decrypted_token = jwe.JWE(algs=[self.db.jwe_encryption_alg, self.db.jwe_cek_enc]) - decrypted_token.deserialize(token, key=server_private_key) - decrypted_payload = str(decrypted_token.payload, "utf-8") - # verify signature - signed_token = jws.JWS() - signed_token.deserialize(decrypted_payload, key=client_public_key, alg=self.db.jws_signing_alg) - # return final payload - payload = str(signed_token.payload, "utf-8") - return json.loads(payload) - except Exception: - return False - - def _make_token(self, payload): - """Wrap payload in a signed and encrypted JWT for response.""" - # sign the payload with server's private key - header = {"typ": "JWS", "alg": self.db.jws_signing_alg} - signed_token = jwt.JWT(header=header, claims=payload) - signed_token.make_signed_token(server_private_key) - signed_payload = signed_token.serialize() - # encrypt the signed payload with client's public key - header = {"typ": "JWE", "alg": self.db.jwe_encryption_alg, "enc": self.db.jwe_cek_enc} - encrypted_token = jwt.JWT(header=header, claims=signed_payload) - encrypted_token.make_encrypted_token(client_public_key) - return encrypted_token.serialize() - - def get(self): - """Respond to a verification request.""" - # introduce small fake delay - time.sleep(2) - - headers = {} - - # verify required headers and API key check - try: - headers = self._check_headers() - except Exception: - return "Unauthorized", 403 - - # parse inner payload from request token - try: - token = self._get_token(headers) - token_payload = self._get_token_payload(token) - except Exception as ex: - return str(ex), 400 - - if token_payload: - try: - # craft the response payload using parsed request token - sub, name, eligibility = token_payload["sub"], token_payload["name"], list(token_payload["eligibility"]) - resp_payload = dict( - jti=token_payload["jti"], - iss=app.name, - iat=int(datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).timestamp()), - ) - # sub format check - if re.match(r"^[A-Z]\d{7}$", sub): - # eligibility check against db - resp_payload["eligibility"] = self.db.check_user(sub, name, eligibility) - code = 200 - else: - resp_payload["error"] = {"sub": "invalid"} - code = 400 - # make a response token with appropriate response code - return self._make_token(resp_payload), code - except Exception as ex: - return str(ex), 500 - else: - return "Invalid token format", 400 - - -api.add_resource(MerchantAuthToken, "//access-token") -api.add_resource(Verify, "/verify") - - -if __name__ == "__main__": - app.run(host="0.0.0.0", debug=True) # nosec diff --git a/localhost/server/requirements.txt b/localhost/server/requirements.txt deleted file mode 100644 index 2decdf59b..000000000 --- a/localhost/server/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -cryptography==3.4.7 -debugpy==1.3.0 -Flask==1.1.2 -Flask-RESTful==0.3.8 -jwcrypto==0.9.1 diff --git a/localhost/server/static/tokenize.js b/localhost/server/static/tokenize.js deleted file mode 100644 index c840c2208..000000000 --- a/localhost/server/static/tokenize.js +++ /dev/null @@ -1,3 +0,0 @@ -tokenize = function(args) { - console.log(args); -};