diff --git a/.gitignore b/.gitignore index fd7a688..3cfc8d6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .vscode/ node_modules/ .env -*.env \ No newline at end of file +*.env +repo_* \ No newline at end of file diff --git a/services/client/src/App.vue b/services/client/src/App.vue index 28dcdeb..ec7de01 100644 --- a/services/client/src/App.vue +++ b/services/client/src/App.vue @@ -29,6 +29,11 @@ >GitHub +
  • + 3Bot +
  • Logout -
    -
    -
    - - - -
    - - -
    - {{ todo.text }} -
    - -
    - -
    ×
    -
    - -
    - - - - - diff --git a/services/client/src/main.js b/services/client/src/main.js index e181884..9f96b02 100644 --- a/services/client/src/main.js +++ b/services/client/src/main.js @@ -36,3 +36,13 @@ new Vue({ store, render: (h) => h(App), }).$mount("#app"); + +// Does it help? +const allowCrossDomain = function (req, res, next) { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Methods", "*"); + res.header("Access-Control-Allow-Headers", "*"); + next(); +}; + +Vue.use(allowCrossDomain); diff --git a/services/client/src/router/index.js b/services/client/src/router/index.js index 37f3f91..dfbbeaa 100644 --- a/services/client/src/router/index.js +++ b/services/client/src/router/index.js @@ -6,6 +6,7 @@ import Todo from "@/views/Todo.vue"; import Login from "@/views/Login.vue"; import Register from "@/views/Register.vue"; import Github from "@/views/Github.vue"; +import Tribot from "@/views/Tribot.vue"; import Logout from "@/views/Logout.vue"; Vue.use(VueRouter); @@ -21,9 +22,9 @@ const routes = [ path: "/todo", name: "Todo", component: Todo, - // meta: { - // requiresAuth: true, - // }, + meta: { + requiresAuth: true, + }, }, { path: "/login", @@ -49,6 +50,14 @@ const routes = [ requiresVisitor: true, }, }, + { + path: "/tribot", + name: "Tribot", + component: Tribot, + meta: { + requiresVisitor: true, + }, + }, { path: "/logout", name: "Logout", diff --git a/services/client/src/store/index.js b/services/client/src/store/index.js index 53b1d4e..6a1bc47 100644 --- a/services/client/src/store/index.js +++ b/services/client/src/store/index.js @@ -8,7 +8,7 @@ export default new Vuex.Store({ state: { token: localStorage.getItem("access_token") || null, // access_token_cookie: Vue.$cookies.get("access_token_cookie") || null, - api_url: "http://localhost:8080", + api_url: "http://127.0.0.1:8080", }, mutations: { updateToken(state, token) { diff --git a/services/client/src/views/Home.vue b/services/client/src/views/Home.vue index 12175cb..22be9bb 100644 --- a/services/client/src/views/Home.vue +++ b/services/client/src/views/Home.vue @@ -7,7 +7,21 @@ diff --git a/services/client/src/views/Logout.vue b/services/client/src/views/Logout.vue index cc9d175..7afc230 100644 --- a/services/client/src/views/Logout.vue +++ b/services/client/src/views/Logout.vue @@ -6,7 +6,9 @@ import Vue from "vue"; import axios from "axios"; import VueAxios from "vue-axios"; +import VueCookie from "vue-cookie"; +Vue.use(VueCookie); Vue.use(VueAxios, axios); export default { name: "Logout", @@ -16,16 +18,30 @@ export default { }; }, created() { - this.distroyToken(); + this.logout(); }, methods: { + logout() { + // remove token from cookie + this.$cookie.delete("access_token_cookie"); + + // remove token from localStorage + localStorage.clear(); // not the best practice but workes for now + + // remove the token from store state + this.$store.commit("distroyToken"); + + // redirect + this.$router.push({ name: "Login" }); + }, + distroyToken() { const path = this.baseUrl + "/auth/logout"; - axios.defaults.headers.common["Authorization"] = + Vue.axios.defaults.headers.common["Authorization"] = "Bearer " + this.$store.state.token; - axios.get(path).then((response) => { + Vue.axios.get(path).then((response) => { // remove the cookie from server console.log(response.data.message); diff --git a/services/client/src/views/Todo.vue b/services/client/src/views/Todo.vue index c1a88cf..b9fa1bb 100644 --- a/services/client/src/views/Todo.vue +++ b/services/client/src/views/Todo.vue @@ -60,13 +60,13 @@ Vue.use(VueAxios, axios); export default { name: "Todo", beforeCreate() { - var token = this.$cookie.get("access_token_cookie"); - console.log(token); - // add to local storage - localStorage.setItem("access_token", token); // token stores in cookies + var token = this.$cookie.get("access_token_cookie"); + console.log(token); + // add to local storage + localStorage.setItem("access_token", token); // token stores in cookies - // update the store state token - this.$store.commit("updateToken", token); + // update the store state token + this.$store.commit("updateToken", token); }, data() { @@ -86,7 +86,6 @@ export default { }, }, - created() { // this.loadToken(); this.getTodos(); diff --git a/services/client/src/views/Tribot.vue b/services/client/src/views/Tribot.vue new file mode 100644 index 0000000..6681433 --- /dev/null +++ b/services/client/src/views/Tribot.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/services/client/vue.config.js b/services/client/vue.config.js index 7dcefbb..c69132f 100644 --- a/services/client/vue.config.js +++ b/services/client/vue.config.js @@ -1,5 +1,5 @@ module.exports = { devServer: { - proxy: 'http://127.0.0.1:5000', + proxy: 'https://127.0.0.1:5000', } } \ No newline at end of file diff --git a/services/server/3bot/proxy_oauth.py b/services/server/3bot/proxy_oauth.py new file mode 100644 index 0000000..c1d8011 --- /dev/null +++ b/services/server/3bot/proxy_oauth.py @@ -0,0 +1,70 @@ +from flask import Flask, request, json, session, redirect +from beaker.middleware import SessionMiddleware +from urllib.parse import urlencode +from uuid import uuid4 +import requests +import os + + +app = Flask(__name__) +app.config["SECRET_KEY"] = "secret" + + +# APIs URL +PROXY_OAUTH_SERVER_URL = "http://127.0.0.1:9000" +HOST_URL = 'https://login.threefold.me' + +# App info +APP_ID = '127.0.0.1:5000' +REDIRECT_ROUTE = "/callback" + +def generate_login_url(): + + state = str(uuid4()).replace("-", "") + session["state"] = state + + response = requests.get(f"{PROXY_OAUTH_SERVER_URL}/pubkey") + response.raise_for_status() # will raise an HTTPError if the HTTP request returned an unsuccessful status code + data = response.json() + pubkey = data["publickey"].encode() + + params = { + "state": state, + "appid": APP_ID, + "scope": json.dumps({"user": True, "email": True}), + "redirecturl": REDIRECT_ROUTE, + "publickey": pubkey, + } + + url_params = urlencode(params) + return f"{HOST_URL}?{url_params}" + +@app.route("/login") +def login(): + return redirect(generate_login_url()) + +@app.route(REDIRECT_ROUTE) +def callback(): + state = session["state"] + signed_attempt_val = request.args.get("signedAttempt") + + data = { + "signedAttempt": signed_attempt_val, + "state": state + } + + response = requests.post(f"{PROXY_OAUTH_SERVER_URL}/verify", data=data) + response.raise_for_status() + + return response.json() + +# session_opts = { +# 'session.type': 'file', +# "session.data_dir": "./data", +# "session.auto": True +# } + +# wsgi_app = SessionMiddleware(app, session_opts) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/services/server/3bot/proxy_server.py b/services/server/3bot/proxy_server.py new file mode 100644 index 0000000..3e2f5d5 --- /dev/null +++ b/services/server/3bot/proxy_server.py @@ -0,0 +1,150 @@ +import nacl.encoding +import nacl.exceptions +import nacl.signing +import requests +import json +import base64 + +from bottle import Bottle, request, abort, response +from nacl.public import Box + + +PUBKEY_URL = "/pubkey" +VERIFY_URL = "/verify" +KEY_PATH = "/opt/priv.key" + + +with open(KEY_PATH) as kp: + PRIV_KEY = nacl.signing.SigningKey(kp.read(), encoder=nacl.encoding.Base64Encoder) + +app = application = Bottle() + + +def enable_cors(fn): + def _enable_cors(*args, **kwargs): + # set CORS headers + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, OPTIONS, DELETE" + response.headers[ + "Access-Control-Allow-Headers" + ] = "Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token" + + return fn(*args, **kwargs) + + return _enable_cors + + +@app.route("/checks/readiness") +def readiness(): + with open("/opt/priv.key", "r") as f: + priv_key = f.read() + if priv_key: + return {"readiness": "success"} + else: + return abort(400, "Private key not created") + + +@app.route("/checks/liveness") +def liveness(): + return {"liveness": "success"} + + +@app.route(PUBKEY_URL) +@enable_cors +def pubkey(): + public_key = PRIV_KEY.verify_key + + return {"publickey": public_key.to_curve25519_public_key().encode(encoder=nacl.encoding.Base64Encoder).decode()} + + +@app.post(VERIFY_URL) +@enable_cors +def verify(): + + is_json = "application/json" in request.headers["Content-Type"] + if is_json: + request_data = request.json + else: + request_data = request.params + + data = request_data.get("signedAttempt") + state = request_data.get("state") + + if not data: + return abort(400, "signedAttempt parameter is missing") + + if not is_json: + try: + data = json.loads(data) + except json.JSONDecodeError: + return abort(400, "signedAttempt not in correct format") + + if "signedAttempt" not in data: + return abort(400, "signedAttempt value is missing") + + username = data.get("doubleName") + + if not username: + return abort(400, "DoubleName is missing") + + res = requests.get(f"https://login.threefold.me/api/users/{username}", {"Content-Type": "application/json"}) + if res.status_code != 200: + return abort(400, "Error getting user pub key") + + pub_key = nacl.signing.VerifyKey(res.json()["publicKey"], encoder=nacl.encoding.Base64Encoder) + + # verify data + signedData = data["signedAttempt"] + + verifiedData = pub_key.verify(base64.b64decode(signedData)).decode() + + data = json.loads(verifiedData) + + if "doubleName" not in data: + return abort(400, "Decrypted data does not contain (doubleName)") + + if "signedState" not in data: + return abort(400, "Decrypted data does not contain (state)") + + if data["doubleName"] != username: + return abort(400, "username mismatch!") + + # verify state + signed_state = data.get("signedState", "") + if state != signed_state: + return abort(400, "Invalid state. not matching one in user session") + + nonce = base64.b64decode(data["data"]["nonce"]) + ciphertext = base64.b64decode(data["data"]["ciphertext"]) + + try: + box = Box(PRIV_KEY.to_curve25519_private_key(), pub_key.to_curve25519_public_key()) + decrypted = box.decrypt(ciphertext, nonce) + except nacl.exceptions.CryptoError: + return abort(400, "Error decrypting data") + + try: + result = json.loads(decrypted) + except json.JSONDecodeError: + return abort(400, "3bot login returned faulty data") + + if "email" not in result: + return abort(400, "Email is not present in data") + + email = result["email"]["email"] + + sei = result["email"]["sei"] + res = requests.post( + "https://openkyc.live/verification/verify-sei", + headers={"Content-Type": "application/json"}, + json={"signedEmailIdentifier": sei}, + ) + + if res.status_code != 200: + return abort(400, "Email is not verified") + + return {"email": email, "username": username} + + +if __name__ == "__main__": + app.run(port=9000) diff --git a/services/server/3bot/tfl_auth.py b/services/server/3bot/tfl_auth.py new file mode 100644 index 0000000..b1321c5 --- /dev/null +++ b/services/server/3bot/tfl_auth.py @@ -0,0 +1,78 @@ +import json +from flask import Flask, redirect, request +from ThreefoldLoginPkg import ThreefoldLogin +from repo.ThreefoldLoginPkg.threefold_login import ThreefoldLogin +import string +from urllib.parse import urlencode +import random + + +app = Flask(__name__) + + +def generate_authenticator(): + api_url = 'https://login.threefold.me' + kyc_backend_url = 'https://openkyc.staging.jimber.org' + + email_identifier = 'omarabdul3ziz@gmail.com' + seed_phrase = "dinner test old limit mass brief desk decline clarify scene strike accident olympic meadow click nuclear avocado outside share excite rookie snow adapt blast" + + app_id = '127.0.0.1:5000' + redirect_url = "/callback" + + authenticator = ThreefoldLogin(api_url, app_id, seed_phrase, redirect_url, kyc_backend_url) + + allowed = string.ascii_letters + string.digits + state = ''.join(random.SystemRandom().choice(allowed) for _ in range(32)) + login_url = authenticator.generate_login_url(state) + + callback_url = f"{app_id}{redirect_url}" + + return authenticator, state, login_url, callback_url, email_identifier + +AUTHENTICATOR, STATE, LOGIN_URL, CALLBACK_URL, EMAIL_ID = generate_authenticator() + +@app.route('/login') +def login(): + # DoneTODO: cause a http bad + return redirect(LOGIN_URL) + +@app.route('/callback') +def callback(): + #DoneTODO: get valid callback_url + + query = request.args.get("signedAttempt") + params = urlencode({"signedAttempt": query}) + callback_url = f"http://{CALLBACK_URL}?{params}" + + authenticator = AUTHENTICATOR + state = STATE + email_id = EMAIL_ID + + msg = "" + authenticator.parse_and_validate_redirect_url(callback_url, state) + msg = 'successfully validated login attempt' + + resp = authenticator.verify_signed_email_idenfier(email_id) + + # if authenticator.is_email_verified(email_id): + # msg = msg + 'email is verified' + # else: + # msg = msg + 'email is not verified' + + # try: + # msg = "" + # authenticator.parse_and_validate_redirect_url(callback_url, state) + # msg = 'successfully validated login attempt' + # if authenticator.is_email_verified(email_id): + # msg = msg + 'email is verified' + # else: + # msg = msg + 'email is not verified' + # except ValueError: + # msg = 'failed to validate login attempt' + + return resp + + +if __name__=='__main__': + app.run(debug=True) \ No newline at end of file diff --git a/services/server/3bot/utlis/cors.py b/services/server/3bot/utlis/cors.py new file mode 100644 index 0000000..f294358 --- /dev/null +++ b/services/server/3bot/utlis/cors.py @@ -0,0 +1,10 @@ +from flask import make_response + +def enable_cors(func): + def decorator(*args, **kwargs): + response = make_response() + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, OPTIONS, DELETE" + response.headers["Access-Control-Allow-Headers"] = "Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token" + return func(*args, **kwargs) + return decorator \ No newline at end of file diff --git a/services/server/app.py b/services/server/app.py index e739afc..edc56b0 100644 --- a/services/server/app.py +++ b/services/server/app.py @@ -4,7 +4,7 @@ import os from api import api -from auth import github_blueprint, auth_blueprint, JWTManager +from auth import github_blueprint, auth_blueprint, tribot_bp, JWTManager from model import DATABASE_URL @@ -17,7 +17,7 @@ SECRET_KEY = os.getenv("SECRET_KEY") -app.config['SECRET_KEY'] = SECRET_KEY +app.config['SECRET_KEY'] = "SECRET_KEY" app.config["MONGO_URI"] = DATABASE_URL @@ -31,3 +31,4 @@ app.register_blueprint(github_blueprint, url_prefix='/login') app.register_blueprint(auth_blueprint, url_prefix='/auth') +app.register_blueprint(tribot_bp, url_prefix='/3bot') diff --git a/services/server/auth.py b/services/server/auth.py index 1188d0e..f3403b1 100644 --- a/services/server/auth.py +++ b/services/server/auth.py @@ -1,7 +1,9 @@ -from flask import Flask, request, jsonify, make_response, redirect, url_for, Blueprint +from flask import Flask, request, jsonify, make_response, redirect, url_for, Blueprint, json, session from flask_dance.contrib.github import make_github_blueprint, github from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, JWTManager, unset_jwt_cookies, set_access_cookies - +from urllib.parse import urlencode +from uuid import uuid4 +import requests import os from model import users @@ -11,12 +13,91 @@ auth_blueprint = Blueprint('auth_blueprint', __name__) +tribot_bp = Blueprint('threebot_blueprint', __name__) + +github_blueprint = make_github_blueprint( + client_id=GITHUB_ID, client_secret=GITHUB_SECRET) + +######################################### +## ========== 3 bot Auth ========== ## +######################################### + +# APIs URL +PROXY_OAUTH_SERVER_URL = "http://127.0.0.1:9000" +HOST_URL = 'https://login.threefold.me' + +# App info +APP_ID = '127.0.0.1:5000' +REDIRECT_ROUTE = "/3bot/callback" + + +def generate_login_url(): + + state = str(uuid4()).replace("-", "") + session["state"] = state + + response = requests.get(f"{PROXY_OAUTH_SERVER_URL}/pubkey") + # will raise an HTTPError if the HTTP request returned an unsuccessful status code + response.raise_for_status() + data = response.json() + pubkey = data["publickey"].encode() + + params = { + "state": state, + "appid": APP_ID, + "scope": json.dumps({"user": True, "email": True}), + "redirecturl": REDIRECT_ROUTE, + "publickey": pubkey, + } + + url_params = urlencode(params) + return f"{HOST_URL}?{url_params}" + + +@tribot_bp.route("/login") +def tribot_login(): + return redirect(generate_login_url()) + + +@tribot_bp.route("/callback") +def callback(): + state = session["state"] + signed_attempt_val = request.args.get("signedAttempt") + + data = { + "signedAttempt": signed_attempt_val, + "state": state + } + + response = requests.post(f"{PROXY_OAUTH_SERVER_URL}/verify", data=data) + response.raise_for_status() + + account_info = response.json() + + username = account_info['username'] + # fetch user from db + user = users.find_one({'username': username}) -github_blueprint = make_github_blueprint(client_id=GITHUB_ID, client_secret=GITHUB_SECRET ) + # or create new one + if user is None: + user = {'username': username, + 'password': "", + 'admin': False} + + users.insert(user) + + # create and respond with token + access_token = create_access_token(identity=username) + + response = make_response(redirect("http://127.0.0.1:8080/")) + response.set_cookie("access_token_cookie", access_token) + return response ######################################### -## ============= Auth ============= ## +## ========= github Auth ========== ## ######################################### + + @auth_blueprint.route('/github') def github_login(): @@ -37,10 +118,10 @@ def github_login(): 'admin': False} users.insert(user) - + # create and respond with token access_token = create_access_token(identity=username) - + # default set cookie response = jsonify(access_token=access_token) set_access_cookies(response, access_token) @@ -50,6 +131,10 @@ def github_login(): # response.set_cookie('access_token_cookie', access_token) return response +######################################### +## ========== basic Auth ========== ## +######################################### + @auth_blueprint.route('/register', methods=['POST']) def register(): @@ -71,7 +156,7 @@ def register(): # create and respond with token access_token = create_access_token(identity=username) - + # default set cookie response = jsonify(access_token=access_token) # set_access_cookies(response, access_token) # make the set from front end @@ -80,7 +165,8 @@ def register(): # response = make_response(redirect(url_for("index"))) response.set_cookie('access_token_cookie', access_token) return response - + + @auth_blueprint.route('/login', methods=['POST']) def login(): # getting criedentials from BODY @@ -99,7 +185,7 @@ def login(): return "No such user." if password != user['password']: return "Wrong password." - + # create token access_token = create_access_token(identity=username) diff --git a/services/server/start_proxy_server.sh b/services/server/start_proxy_server.sh new file mode 100755 index 0000000..03c35f9 --- /dev/null +++ b/services/server/start_proxy_server.sh @@ -0,0 +1,3 @@ +#! /usr/bin/sh + +python3 ./3bot/proxy_server.py \ No newline at end of file diff --git a/services/server/wsgi.py b/services/server/wsgi.py old mode 100644 new mode 100755 index 5751813..65317ac --- a/services/server/wsgi.py +++ b/services/server/wsgi.py @@ -1,4 +1,6 @@ +#! /usr/bin/python3 + from app import app if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file + app.run(debug=True, ssl_context=('/home/omar/localhost.pem', '/home/omar/localhost-key.pem')) \ No newline at end of file