diff --git a/frontend/public/images/icons/google-icon.png b/frontend/public/images/icons/google-icon.png new file mode 100644 index 0000000..3085a7c Binary files /dev/null and b/frontend/public/images/icons/google-icon.png differ diff --git a/frontend/src/components/Auth/LoginForm.tsx b/frontend/src/components/Auth/LoginForm.tsx index 0a2628d..eb04d24 100644 --- a/frontend/src/components/Auth/LoginForm.tsx +++ b/frontend/src/components/Auth/LoginForm.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import Input from "../Layout/Input"; import Button from "../Layout/Button"; import { useAuth } from "../../context/AuthContext"; +import GoogleLogin from "./OAuth"; interface LoginFormData { username: string; @@ -133,6 +134,7 @@ const LoginForm: React.FC = ({ 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 8970324..dee0976 100644 Binary files a/web_server/database/app.db and b/web_server/database/app.db differ 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