diff --git a/funding/orm.py b/funding/orm.py index 4cae7c5..e20cbdc 100644 --- a/funding/orm.py +++ b/funding/orm.py @@ -7,6 +7,7 @@ from sqlalchemy.orm import relationship, backref import sqlalchemy as sa from sqlalchemy.orm import scoped_session, sessionmaker, relationship from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.types import Float from sqlalchemy_json import MutableJson @@ -26,11 +27,14 @@ class User(db.Model): admin = db.Column(db.Boolean, default=False) proposals = relationship('Proposal', back_populates="user") comments = relationship("Comment", back_populates="user") + uuid = db.Column(UUID(as_uuid=True), unique=True) - def __init__(self, username, password, email): + def __init__(self, username, password, email, uuid=None): from funding.factory import bcrypt self.username = username - self.password = bcrypt.generate_password_hash(password).decode('utf8') + if password: + self.password = bcrypt.generate_password_hash(password).decode('utf8') + self.uuid = uuid self.email = email self.registered_on = datetime.utcnow() @@ -57,16 +61,17 @@ class User(db.Model): return self.username @classmethod - def add(cls, username, password, email): + def add(cls, username, password=None, email=None, uuid=None): from funding.factory import db from funding.validation import val_username, val_email try: # validate incoming username/email val_username(username) - val_email(email) + if email: + val_email(email) - user = User(username, password, email) + user = User(username=username, password=password, email=email, uuid=uuid) db.session.add(user) db.session.commit() db.session.flush() diff --git a/funding/routes.py b/funding/routes.py index 4030897..a7b5681 100644 --- a/funding/routes.py +++ b/funding/routes.py @@ -1,7 +1,8 @@ +import uuid from datetime import datetime import requests -from flask import request, redirect, render_template, url_for, flash, make_response, send_from_directory, jsonify +from flask import request, redirect, render_template, url_for, flash, make_response, send_from_directory, jsonify, session from flask_login import login_user , logout_user , current_user from dateutil.parser import parse as dateutil_parse from flask_yoloapi import endpoint, parameter @@ -301,12 +302,93 @@ def register(): return make_response(render_template('register.html')) +if settings.OPENID_ENABLED: + @app.route("/wow-auth/") + def wow_auth(): + assert "state" in request.args + assert "session_state" in request.args + assert "code" in request.args + + # verify state + if not session.get('auth_state'): + return "session error", 500 + if request.args['state'] != session['auth_state']: + return "attack detected :)", 500 + + # with this authorization code we can fetch an access token + url = f"{settings.OPENID_URL}/token" + data = { + "grant_type": "authorization_code", + "code": request.args["code"], + "redirect_uri": settings.OPENID_REDIRECT_URI, + "client_id": settings.OPENID_CLIENT_ID, + "client_secret": settings.OPENID_CLIENT_SECRET, + "state": request.args['state'] + } + try: + resp = requests.post(url, data=data) + resp.raise_for_status() + except: + return "something went wrong :( #1", 500 + + data = resp.json() + assert "access_token" in data + assert data.get("token_type") == "bearer" + access_token = data['access_token'] + + # fetch user information with the access token + url = f"{settings.OPENID_URL}/userinfo" + + try: + resp = requests.post(url, headers={"Authorization": f"Bearer {access_token}"}) + resp.raise_for_status() + user_profile = resp.json() + except: + return "something went wrong :( #2", 500 + + username = user_profile.get("preferred_username") + sub = user_profile.get("sub") + if not username: + return "something went wrong :( #3", 500 + + sub_uuid = uuid.UUID(sub) + user = User.query.filter_by(username=username).first() + if user: + if not user.uuid: + user.uuid = sub_uuid + db.session.commit() + db.session.flush() + else: + user = User.add(username=username, + password=None, email=None, uuid=sub_uuid) + login_user(user) + response = redirect(request.args.get('next') or url_for('index')) + response.headers['X-Set-Cookie'] = True + return response + + @app.route('/login', methods=['GET', 'POST']) @endpoint.api( - parameter('username', type=str, location='form'), - parameter('password', type=str, location='form') + parameter('username', type=str, location='form', required=False), + parameter('password', type=str, location='form', required=False) ) def login(username, password): + if settings.OPENID_ENABLED: + state = uuid.uuid4().hex + session['auth_state'] = state + + url = f"{settings.OPENID_URL}/auth?" \ + f"client_id={settings.OPENID_CLIENT_ID}&" \ + f"redirect_uri={settings.OPENID_REDIRECT_URI}&" \ + f"response_type=code&" \ + f"state={state}" + + return redirect(url) + + if not username or not password: + flash('Enter username/password pl0x') + return make_response(render_template('login.html')) + if request.method == 'GET': return make_response(render_template('login.html')) @@ -327,7 +409,6 @@ def logout(): logout_user() response = redirect(request.args.get('next') or url_for('login')) response.headers['X-Set-Cookie'] = True - flash('Logout successfully') return response diff --git a/settings.py_example b/settings.py_example index a984164..97cec9f 100644 --- a/settings.py_example +++ b/settings.py_example @@ -14,6 +14,13 @@ PSQL_DB = '' PSQL_USER = 'postgres' PSQL_PASS = '' +OPENID_ENABLED = False +OPENID_REALM = "master" +OPENID_URL = f"https://login.wownero.com/auth/realms/{OPENID_REALM}/protocol/openid-connect" +OPENID_CLIENT_ID = "" +OPENID_CLIENT_SECRET = "" +OPENID_REDIRECT_URI = "http://0.0.0.0:5004/wow-auth/" + SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql://{user}:{pw}@localhost/{db}').format(user=PSQL_USER, pw=PSQL_PASS, db=PSQL_DB) SESSION_COOKIE_NAME = os.environ.get('{coincode}_SESSION_COOKIE_NAME', '{coincode}_id').format(coincode=COINCODE.upper())