import base64 import hashlib import os import sqlite3 import time import falcon import requests import waitress import config from config import REDIRECT_URL, AUTH_URL, TOKEN_URL, PROPER_USER_AGENT, CLIENT_ID, REDIRECT_HTML_TEMPLATE from refresher import refresh_one_token # import logging # from sys import stdout def generate_challenge(_verifier): """ It takes code verifier and return code challenge""" return base64.urlsafe_b64encode(hashlib.sha256(_verifier).digest())[:-1].decode('utf-8') """ # setting up logging logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) stdout_handler = logging.StreamHandler(stdout) formatter = logging.Formatter( '%(asctime)s - %(levelname)s - %(process)d:%(thread)d: %(module)s:%(lineno)d: %(message)s') stdout_handler.setFormatter(formatter) logger.addHandler(stdout_handler) """ """ Typical workflow: 1. User open Authorize endpoint 2. Authorize building link for FDEV's /auth, sending link to user 3. Authorize write code_verifier, state, timestamp_init (by DBMS) to DB 4. User approving client, redirecting to FDEV_redirect endpoint 5. Searching in DB if we have record with this state if don't have: raise error for user, exit 6. Write received code to DB 7. Build Token Request if not success: remove all related info from DB raise error for user, exit 8. Store access_token, refresh_token, expires_in, timestamp_got_expires_in 9. Raise "all right" for user DB Tables: 1. authorizations code_verifier text - store code_verifier, don't storing code_challenge because we can get it from verifier state text - state timestamp_init text - unix timestamp when authorization was started, can be used to count 25 days until next auth code text - code (from frontier redirect) access_token text - access token aka bearer refresh_token text - refresh token expires_in text - expires in for access token (in secs) timestamp_got_expires_in text - unix timestamp, when we got expires_in for access token nickname text - nickname of tokens owner """ sql_conn = sqlite3.connect(database="companion-api.sqlite") with sql_conn: sql_conn.execute( "create table if not exists authorizations (code_verifier text , state text, " "timestamp_init text , code text, access_token text, refresh_token text, expires_in text, " "timestamp_got_expires_in text, nickname text unique);") sql_conn.close() class Authorize: def on_get(self, req, resp): sql_connection = sqlite3.connect(database="companion-api.sqlite") code_verifier = base64.urlsafe_b64encode(os.urandom(32)) code_challenge = generate_challenge(code_verifier) state_string = base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8") redirect_user_to = f"{AUTH_URL}?audience=all&scope=capi&response_type=code&client_id={CLIENT_ID}" + \ f"&code_challenge={code_challenge}&code_challenge_method=S256&state={state_string}" + \ f"&redirect_uri={REDIRECT_URL}" with sql_connection: sql_connection.execute( "insert into authorizations (code_verifier, state, timestamp_init) values " "(?, ?, ?);", [code_verifier.decode('utf-8'), state_string, int(time.time())]) sql_connection.close() resp.content_type = falcon.MEDIA_HTML resp.text = REDIRECT_HTML_TEMPLATE.format(link=redirect_user_to) class FDEV_redirect: def on_get(self, req, resp): error_id = hashlib.sha256(os.urandom(32)).hexdigest() code = req.get_param('code') state = req.get_param('state') if not code or not state: raise falcon.HTTPBadRequest(description=f"No code or no state") sql_connection = sqlite3.connect(database="companion-api.sqlite") with sql_connection: code_verifier = sql_connection.execute("select code_verifier from authorizations where state = ?;", [state]).fetchone() if code_verifier is None: # Somebody got here not by frontier redirect raise falcon.HTTPBadRequest(description="Don't show here without passing frontier authorization!") code_verifier = code_verifier[0] with sql_connection: sql_connection.execute("update authorizations set code = ? where state = ?;", [code, state]) token_request = requests.post( url=TOKEN_URL, headers={'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': PROPER_USER_AGENT}, data=f'redirect_uri={REDIRECT_URL}&code={code}&grant_type=authorization_code&code_verifier={code_verifier}' f'&client_id={CLIENT_ID}') if token_request.status_code != 200: # Something went wrong print(f"error_id: {error_id}\n{token_request.text}") with sql_connection: sql_connection.execute("delete from authorizations where state = ?;", [state]) sql_connection.close() raise falcon.HTTPBadRequest(description=f'Something went wrong, your error id is {error_id}') token_request = token_request.json() access_token = token_request["access_token"] refresh_token = token_request["refresh_token"] expires_in = token_request["expires_in"] timestamp_got_expires_in = int(time.time()) with sql_connection: sql_connection.execute( "update authorizations set access_token = ?, refresh_token = ?, expires_in = ?, " "timestamp_got_expires_in = ? where state = ?", [access_token, refresh_token, expires_in, timestamp_got_expires_in, state]) try: nickname = requests.get( url='https://companion.orerve.net/profile', headers={'Authorization': f'Bearer {access_token}', 'User-Agent': PROPER_USER_AGENT}).json()["commander"]["name"] try: sql_connection.execute("update authorizations set nickname = ? where state = ?;", [nickname, state]) sql_connection.commit() except sqlite3.IntegrityError: already_saved_state = sql_connection.execute( "select state from authorizations where nickname = ?", [nickname]).fetchone()[0] resp.text = f"We already have your token (CMDR {nickname}) in DB, your key: {already_saved_state}" with sql_connection: sql_connection.execute("delete from authorizations where state = ?;", [state]) sql_connection.close() return except Exception as e: resp.text = f"Something went wrong, your error id is {error_id}" print(f"error_id: {error_id}\n{e}") with sql_connection: sql_connection.execute("delete from authorizations where state = ?;", [state]) else: # all fine resp.text = f"All right, {nickname}, we have your tokens\nSave and keep this key: {state}" sql_connection.close() class User: def on_get(self, req, resp, user): sql_connection = sqlite3.connect(database="companion-api.sqlite") if sql_connection.execute('select timestamp_got_expires_in, expires_in from authorizations where state = ?;', [user]).fetchone() is None: sql_connection.close() raise falcon.HTTPNotFound(description="Couldn't find you in DB") refresh_one_token(user) access_token, expires_timestamp, nickname = sql_connection.execute( 'select access_token, timestamp_got_expires_in + expires_in, nickname from authorizations where state = ?', [user]).fetchone() import json resp.text = json.dumps({'access_token': access_token, 'expires_over': expires_timestamp - int(time.time()), 'expires_on': expires_timestamp, 'nickname': nickname}) resp.content_type = falcon.MEDIA_JSON sql_connection.close() def on_delete(self, req, resp, user): sql_connection = sqlite3.connect(database="companion-api.sqlite") with sql_connection: sql_connection.execute('delete from authorizations where state = ?;', [user]) sql_connection.close() class Admin: def on_get(self, req, resp, _state): sql_connection = sqlite3.connect(database="companion-api.sqlite") try: if sql_connection.execute( 'select nickname from authorizations where state = ?;', [_state]).fetchone()[0] != 'Aleksey31': sql_connection.close() raise falcon.HTTPNotFound(description="You aren't an admin") except TypeError: raise falcon.HTTPNotFound(description="You aren't an admin") users_html = "" for user in sql_connection.execute( 'select nickname, state from authorizations where nickname is not null;').fetchall(): user_name = user[0] user_state = "/users/" + user[1] users_html = users_html + config.ADMIN_USER_TEMPLATE.format(link=user_state, desc=user_name) + "\n
\n" resp.content_type = falcon.MEDIA_HTML resp.text = config.ADMIN_USERS_TEMPLATE.format(users_html) sql_connection.close() class RandomToken: def on_get(self, req: falcon.request.Request, resp: falcon.response.Response): access_key = req.headers.get('AUTH') if access_key != 'hEjesv79YUZNeQgJ': raise falcon.HTTPNotFound sql_connection = sqlite3.connect(database="companion-api.sqlite") random_token = sql_connection.execute( 'select state from authorizations where nickname is not null limit 1;',()).fetchone() if random_token is None: sql_connection.close() raise falcon.HTTPNotFound(description="DB is empty") state = random_token[0] refresh_one_token(state) access_token, expires_timestamp, nickname = sql_connection.execute( 'select access_token, timestamp_got_expires_in + expires_in, nickname from authorizations where state = ?', [state]).fetchone() import json resp.text = json.dumps({'access_token': access_token, 'expires_over': expires_timestamp - int(time.time()), 'expires_on': expires_timestamp, 'nickname': nickname}) resp.content_type = falcon.MEDIA_JSON sql_connection.close() app = falcon.App() # routing app.add_route('/authorize', Authorize()) app.add_route('/fdev-redirect', FDEV_redirect()) app.add_route('/users/{user}', User()) app.add_route('/admin/{_state}', Admin()) app.add_route('/random_token', RandomToken()) waitress.serve(app, host='127.0.0.1', port=9000)