From 0984271a113eccc208fd9440ab4a9684cf638871 Mon Sep 17 00:00:00 2001 From: JustIceO7 Date: Tue, 11 Feb 2025 02:22:27 +0000 Subject: [PATCH] FEAT: Added Google OAuth login feature to sign up as well as login --- frontend/public/images/icons/google-icon.png | Bin 0 -> 1209 bytes frontend/src/components/Auth/LoginForm.tsx | 2 + frontend/src/components/Auth/OAuth.tsx | 23 ++++ web_server/blueprints/__init__.py | 6 + web_server/blueprints/authentication.py | 11 +- web_server/blueprints/middleware.py | 3 + web_server/blueprints/oauth.py | 124 +++++++++++++------ web_server/database/app.db | Bin 159744 -> 159744 bytes web_server/database/users.sql | 6 +- web_server/requirements.txt | 3 +- web_server/utils/user_utils.py | 6 +- 11 files changed, 128 insertions(+), 56 deletions(-) create mode 100644 frontend/public/images/icons/google-icon.png diff --git a/frontend/public/images/icons/google-icon.png b/frontend/public/images/icons/google-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3085a7ca527de0357872287be578fdf7dc93e65b GIT binary patch literal 1209 zcmV;q1V;ObP)2~~U`roFM3lDCfR}jAY(i|A&Y3i7TQ!+=@lqjTK_MtUv}(QJY|=~e z;zRKQ3Td0!T~ zW!WA0edp}=oq+{`Kp+qZ1OkCTAV@Wq4*9ee-9c4pM^MEt2Z?lm68t`U-O8@72MK;6 zM4aUoKpx}@ClM-SVh=OHqm)P!Ox}tcf=8)}4^x7+@M2`IxQZK@bcjn`ZP-1|@|}@Y zg%~1Eg{snDTwtXVB+~Jrn#%Rru-#jNL|TX?cp~OUvx)gOo_`@v}gv2FoF(5>drj z%ie#MrHc|g7DZkMyD_5T+o_6Y5*=q$MUV2s5X4}3%iY)ULlQp=Ect#D*pVQSK2A`+ z^=Ef^(X#Gci*I4yJwXZH3wC6>r?@&)f7kah#rZfS&Yf_TShDN@yD}Yc^n{N&7DspA z`VEtvy>b!oYmkfl>xZ>rzhjOS!Qz>CXZ4S?29I2_`d>$muQ|p{pzs+kzBE%uOGoI| zik~ia?D?RA%OF=_tKSuok0jh*IQ!4evVowAdm)zz`wM>M!qE~wa>FB#Af##RA>x^M zHQ8*|%|=KEmLFDy1CIHq@&U)f#AmP?5`=inHsTq#l8xdq|I>9VKlQ8sDcl@$_5F@> zz({uicsA`je-osd3;v>VE*drdf>W6)fYLl!miYn%3QlLL0J1)Q?=}4sQ1!_TwrGNr zU%;B&>;aK6_*<-9Fw|eVE$vf4H!q}R3UJsecSqU&v%cPS0qoU$kRaQ2TDq}i;>LJU z^s(yLV{rT|q>E{Pe#7Nkzi{}4?v1Cg*JUBYHCNfGt%nsv(Z_ek+y%OMC}R&p!^cWa zKhU{tn9DE8=deP!b*qSY%)yuq&|@~F`*GOEim-U1Z|#>=9d)x2@ko%Ny|OZ^Z|;e+ z`m^eR$u*Ep#`=q^n|mt0c+3akM0ahl*KkO`P%FBgp&!?nn+d(j8a z6)sy|H=iaRb0+Q-XfiiJCU6?=+mNA6bJ6(G&Y*T}D_D>h|GxG2u>1O*ZH?b5O>`>G z_8)*ug1q%Fa=`?{C&VAvaJ|h0>)7i`sl9Fk>U29LZ+$)ADcI0XF8#dJ{`lXUTh9C$ z?Fq1^dGt=mG)$%~x^(j4yX^jh9Y0*52)n=z$cGz>`PgHzQklvR_Z(N6&d*&u_;dqU zd0-dT9ei+Aq$#E_M|S)o@BjTHel`HRA%7)1u;JQ=3~fGD1>pU|?Q<7^ox$yHnK!!? z77yLm;o1(_0-yWH``GiW5JSm`Bd==|8=7A3b6*eH2>)5|`OhS}n9z7);@T7w?_+7G z^0`YOXRsg-d);NILwky47c#W{$g91}gj?D54gNlEcUADBi9Zzz1OkCTAP@)yf>iQ9 XN = ({ onSubmit }) => { /> + diff --git a/frontend/src/components/Auth/OAuth.tsx b/frontend/src/components/Auth/OAuth.tsx index e69de29..a3c5446 100644 --- a/frontend/src/components/Auth/OAuth.tsx +++ b/frontend/src/components/Auth/OAuth.tsx @@ -0,0 +1,23 @@ +import { useEffect } from "react"; + +export default function GoogleLogin() { + const handleLoginClick = (e: React.MouseEvent) => { + e.preventDefault(); + window.location.href = "/api/login/google"; + }; + + return ( +
+ +
+ ); +} diff --git a/web_server/blueprints/__init__.py b/web_server/blueprints/__init__.py index ec462fa..a6361fe 100644 --- a/web_server/blueprints/__init__.py +++ b/web_server/blueprints/__init__.py @@ -9,6 +9,7 @@ from blueprints.stripe import stripe_bp from blueprints.user import user_bp from blueprints.streams import stream_bp from blueprints.chat import chat_bp +from blueprints.oauth import oauth_bp, init_oauth from blueprints.socket import socketio from celery import Celery from celery_tasks import celery_init_app @@ -24,10 +25,13 @@ def create_app(): And setup web sockets to be used throughout the project. """ app = Flask(__name__) + app.config["SERVER_NAME"] = "127.0.0.1:8080" app.config["SECRET_KEY"] = getenv("FLASK_SECRET_KEY") app.config["SESSION_PERMANENT"] = False app.config["SESSION_TYPE"] = "filesystem" app.config["PROPAGATE_EXCEPTIONS"] = True + app.config['GOOGLE_CLIENT_ID'] = getenv("GOOGLE_CLIENT_ID") + app.config['GOOGLE_CLIENT_SECRET'] = getenv("GOOGLE_CLIENT_SECRET") app.config.from_mapping( CELERY=dict( @@ -47,6 +51,7 @@ def create_app(): Session(app) app.before_request(logged_in_user) + init_oauth(app) # adds in error handlers register_error_handlers(app) @@ -63,6 +68,7 @@ def create_app(): app.register_blueprint(user_bp) app.register_blueprint(stream_bp) app.register_blueprint(chat_bp) + app.register_blueprint(oauth_bp) socketio.init_app(app) diff --git a/web_server/blueprints/authentication.py b/web_server/blueprints/authentication.py index d99c25d..21bd2af 100644 --- a/web_server/blueprints/authentication.py +++ b/web_server/blueprints/authentication.py @@ -81,18 +81,13 @@ def signup(): # Create new user once input is validated db.execute( """INSERT INTO users - (username, password, email, num_followers, stream_key, is_partnered, bio, current_stream_title, current_selected_category_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (username, password, email, stream_key) + VALUES (?, ?, ?, ?)""", ( username, generate_password_hash(password), email, - 0, - token_hex(32), - 0, - "This user does not have a Bio.", - "My Stream", - None + token_hex(32) ) ) diff --git a/web_server/blueprints/middleware.py b/web_server/blueprints/middleware.py index 2d43571..b8b581d 100644 --- a/web_server/blueprints/middleware.py +++ b/web_server/blueprints/middleware.py @@ -32,6 +32,9 @@ def admin_required(view): return wrapped_view def register_error_handlers(app): + """ + Default reponses to status codes + """ error_responses = { 400: "Bad Request", 403: "Forbidden", diff --git a/web_server/blueprints/oauth.py b/web_server/blueprints/oauth.py index f45e49b..11821dd 100644 --- a/web_server/blueprints/oauth.py +++ b/web_server/blueprints/oauth.py @@ -1,60 +1,102 @@ from authlib.integrations.flask_client import OAuth, OAuthError -from flask import Blueprint, url_for, jsonify, session +from flask import Blueprint, jsonify, session, redirect from blueprints.user import get_session_info_email +from database.database import Database +from secrets import token_hex, token_urlsafe +from random import randint oauth_bp = Blueprint("oauth", __name__) +google = None + def init_oauth(app): oauth = OAuth(app) - + global google google = oauth.register( 'google', client_id=app.config['GOOGLE_CLIENT_ID'], client_secret=app.config['GOOGLE_CLIENT_SECRET'], authorize_url='https://accounts.google.com/o/oauth2/auth', - authorize_params=None, - access_token_url='https://accounts.google.com/o/oauth2/token', - access_token_params=None, - refresh_token_url=None, - redirect_uri=url_for('google.google_auth', _external=True), - scope='openid profile email', + access_token_url='https://oauth2.googleapis.com/token', + client_kwargs={'scope': 'openid profile email'}, + api_base_url='https://www.googleapis.com/oauth2/v1/', + userinfo_endpoint='https://openidconnect.googleapis.com/v1/userinfo', + server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', + redirect_uri="http://127.0.0.1:8080/api/google_auth" ) +@oauth_bp.route('/login/google') +def login_google(): + """ + Redirects to Google's OAuth authorization page + """ + # Creates nonce to be sent + session["nonce"] = token_urlsafe(16) + return google.authorize_redirect( + 'http://127.0.0.1:8080/api/google_auth', + nonce=session['nonce'] + ) - @oauth_bp.route('/login/google') - def login_google(): - """ - Redirects to Google's OAuth authorization page - """ - return google.authorize_redirect(url_for('google.google_auth', _external=True)) +@oauth_bp.route('/google_auth') +def google_auth(): + """ + Receives token from Google OAuth and authenticates it to validate login + """ + try: + token = google.authorize_access_token() - @oauth_bp.route('/google_auth') - def google_auth(): - try: - token = google.authorize_access_token() - user = google.parse_id_token(token) - - # check if email exists else create a database entry - user_email = user.get("email") + # Verifies token as well as nonce + nonce = session.pop('nonce', None) + if not nonce: + return jsonify({'error': 'Missing nonce in session'}), 400 + + user = google.parse_id_token(token, nonce=nonce) + print(user, flush=True) + + # Check if email exists to login else create a database entry + user_email = user.get("email") + + user_data = get_session_info_email(user_email) + + if not user_data: + with Database() as db: + # Generates a new username for the user + for _ in range(1000000): + username = user.get("given_name") + str(randint(1, 1000000)) + taken = db.fetchone(""" + SELECT * FROM users + WHERE username = ? + """, (username,)) + + if not taken: + break + + db.execute( + """INSERT INTO users + (username, email, stream_key) + VALUES (?, ?, ?)""", + ( + username, + user_email, + token_hex(32), + ) + ) user_data = get_session_info_email(user_email) - session.clear() - session["username"] = user_data["username"] - session["user_id"] = user_data["user_id"] + session.clear() + session["username"] = user_data["username"] + session["user_id"] = user_data["user_id"] - return jsonify({ - 'message': 'User authenticated successfully', - }) - - except OAuthError as e: - # Handle OAuth errors like failed authentication or invalid token - return jsonify({ - 'message': 'Authentication failed', - 'error': str(e) - }), 400 + # TODO: redirect back to original page user started on, or other pages based on success failure of login + return redirect("http://127.0.0.1:8080/") - except Exception as e: - # Handle other unexpected errors - return jsonify({ - 'message': 'An unexpected error occurred', - 'error': str(e) - }), 500 \ No newline at end of file + except OAuthError as e: + return jsonify({ + 'message': 'Authentication failed', + 'error': str(e) + }), 400 + + except Exception as e: + return jsonify({ + 'message': 'An unexpected error occurred', + 'error': str(e) + }), 500 diff --git a/web_server/database/app.db b/web_server/database/app.db index 89703241f94b779345011c7aca83fbfbd282f802..dee097635e4fc6412060706de859cd27fc359469 100644 GIT binary patch delta 262 zcmV+h0r~!b;0b`>36L8RVgLXDCjbBd7XSbN0ssI5eUU6P0b;RWsXq(l58DsX562Jn z54y7vAgK?Ny-zs-2DA514ZsEr0~`Pku9plD0UEa?5CO>u0t_v;?-&6-0|XAz0x`ES z90A@#0Rp!XPyz4{wr~Lf5Ge}!01yRM01uoGG7hv3K@HIjWeS1{5($k75D1zGC&^lh6qS zlN=3;DIi2eMnP3fR3Il*XlZjGb#rBMAY^Z4b0BVSbRcM9c4Z)8AVO(xE+>36L8RRsaA1CjbBd7XSbN0ssI5cabbJ0ameKsXq(#59JTr577_D z54y7vAgK?Ny-zs-0kii{4ZsKu0~`Pkt_&@g3J?Jrw-;Dtw;=N8X?f?J) diff --git a/web_server/database/users.sql b/web_server/database/users.sql index b72733a..5023caf 100644 --- a/web_server/database/users.sql +++ b/web_server/database/users.sql @@ -3,13 +3,13 @@ CREATE TABLE users ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR(50) NOT NULL, - password VARCHAR(256) NOT NULL, + password VARCHAR(256), email VARCHAR(128) NOT NULL, - num_followers INTEGER NOT NULL, + num_followers INTEGER NOT NULL DEFAULT 0, stream_key VARCHAR(60) NOT NULL, is_partnered BOOLEAN NOT NULL DEFAULT 0, is_live BOOLEAN NOT NULL DEFAULT 0, - bio VARCHAR(1024), + bio VARCHAR(1024) DEFAULT 'This user does not have a Bio.', current_stream_title VARCHAR(100), current_selected_category_id INTEGER diff --git a/web_server/requirements.txt b/web_server/requirements.txt index 349268f..1cca4ad 100644 --- a/web_server/requirements.txt +++ b/web_server/requirements.txt @@ -27,4 +27,5 @@ gevent-websocket flask-oauthlib==0.9.6 celery==5.2.3 redis==5.2.1 -python-dateutil \ No newline at end of file +python-dateutil +Authlib==1.4.1 \ No newline at end of file diff --git a/web_server/utils/user_utils.py b/web_server/utils/user_utils.py index 99afc95..1fd796f 100644 --- a/web_server/utils/user_utils.py +++ b/web_server/utils/user_utils.py @@ -22,7 +22,7 @@ def get_username(user_id: str) -> Optional[str]: with Database() as db: data = db.fetchone(""" SELECT username - FROM user + FROM users WHERE user_id = ? """, (user_id,)) return data['username'] if data else None @@ -31,10 +31,10 @@ def get_session_info_email(email: str) -> dict: """ Returns username and user_id given email """ - with Database as db: + with Database() as db: session_info = db.fetchone(""" SELECT user_id, username - FROM user + FROM users WHERE email = ? """, (email,)) return session_info