From 9ee1b9c4b01dc32c9f471ec7a24d96b97ac5ab20 Mon Sep 17 00:00:00 2001 From: VJ Date: Sun, 11 Jun 2023 14:18:46 +0530 Subject: [PATCH 1/3] add google auth, remove JWT and bcrypt Documentation is updated, and requirements are more streamlined --- .gitignore | 8 +++ README.md | 54 +++++++++++---- docker-compose.yml | 16 +++++ main.py | 3 +- requirements.txt | 62 +++++++++--------- scripts/generate_keys.py | 13 ++++ src/app.py | 25 ++----- src/config.py | 5 +- src/models.py | 19 +----- src/routes/{auth.py => auth/discord.py} | 42 +++++++----- src/routes/auth/google.py | 61 +++++++++++++++++ src/utils.py | 87 ++++++++++++++++++++++--- 12 files changed, 283 insertions(+), 112 deletions(-) create mode 100644 docker-compose.yml create mode 100755 scripts/generate_keys.py rename src/routes/{auth.py => auth/discord.py} (60%) create mode 100644 src/routes/auth/google.py diff --git a/.gitignore b/.gitignore index 68bc17f..0d0b2cf 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,11 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +# poetry stuff, which isn't really what's being used in this project +poetry.lock +pyproject.toml + +# the right way to store secrets +/config.py \ No newline at end of file diff --git a/README.md b/README.md index aeb2bff..6165431 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Backend Infrastructure for the IITM BSc Discord +# Backend Infrastructure for the IITM B.Sc. Discord ## Setup @@ -6,12 +6,15 @@ 1. Navigate to the [discord developer portal](https://discord.com/developers/applications) and create a new app 2. On the sidebar navigate to OAuth2 > General 3. Add `http://localhost:8081/discord/auth/login/callback` as the redirect URL -4. In the Default Authorization Link section, select custom URL from the drop down and add the same URL as above -5. It should look like this -![image](https://github.com/IITM-BS-Codebase/iitm-backend/assets/42805453/b735a18d-e9d0-4cbd-9352-4eca3f5ddc6e) -6. On the sidebar navigate to OAuth2 > URL generator -7. Pick appropriate scopes, in this case `identify` and `guilds` and choose the previously added URL as the redirect -8. Copy the generated Redirect URL + +### 2. Google OAuth Setup +1. Navigate to the [Google developer console](https://console.developers.google.com) +2. Create a new project by going to Select a project > NEW PROJECT on the top left +3. To generate credentials, on the new page that appears, navigate to Credentials and + click on `+ CREATE CREDENTIALS` on the top and select OAuth Client ID +4. Follow the prompts and answer the questions. +5. Add `http://localhost:8081` to authorized JavaScript origins and + `http://localhost:8081/google/auth/login/callback` as an authorized redirect URI ### 2. Configuration Variables 1. Create a new file named `.env` and add the following @@ -19,8 +22,10 @@ #DISCORD OAUTH DETAILS DISCORD_CLIENT_ID=PASTE CLIENT ID DISCORD_CLIENT_SECRET="PASTE CLIENT SECRET" -DISCORD_OAUTH_REDIRECT="http://localhost:8081/discord/auth/login/callback" -DISCORD_OAUTH_URL="PASTE THE LONG GENERATED REDIRECT URL" + +#GOOGLE OAUTH DETAILS +GOOGLE_CLIENT_ID=PASTE CLIENT ID +GOOGLE_CLIENT_SECRET=PASTE CLIENT SECRET FRONTEND_URL="http://localhost:8080/" @@ -28,11 +33,38 @@ FRONTEND_URL="http://localhost:8080/" DB_CONNECTION_STRING="postgresql+psycopg2://user:password@hostname/database_name" #SECURITY -SECURITY_KEY="something-secret" -JWT_SECRET_KEY="something-secret-but-for-jwt" +PASETO_PRIVATE_KEY="hex-of-private-key-bytes" # just run `scripts/generate_keys.py` to get this for first time setup. ``` +### Database setup + +We use [PostgreSQL](https://www.postgresql.org), so you can install it locally on your +own computer, or opt for a hosted option, whichever is convenient. The format of the +connection string remains the same. + +If you use Docker, you can use provided `docker-compose.yml` to spin up a server +quickly. + +> :warning: If you don't have Postgres or the Postgres client libraries installed on +> your machine, `psycopg2` will fail to install. To work around this, either install the +> required libraries for your system, or replace that package with `psycopg2-binary` of +> the same version. + + ## Running the backend + +At least Python 3.10 is required. + +### Virtual Environments + +It is recommended to use a virtual environment to run this app, or any of your python +projects. + +1. Create a virtual environment: `python3 -m venv .venv` +2. Activate the environment + - Unix: `source ./.venv/bin/activate` + - Windows: `.\.venv\Scripts\activate` + #### Make sure you set `debug=False` in `main.py` when running in prod - Install the requirements by running `pip install -r requirements.txt` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bfb51f5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3' +services: + postgres: + image: postgres:15 + environment: + POSTGRES_USER: postgres + POSTGRES_HOST_AUTH_METHOD: trust # for demo purposes only, don't use in production unless your network is secured. + POSTGRES_DB: backend + ports: + - 5432:5432 + volumes: + - postgres-data:/var/lib/postgresql/data + +volumes: + postgres-data: + driver: local diff --git a/main.py b/main.py index a4f7d5f..3e8491a 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,9 @@ import logging -from src import create_app, setup_auth, setup_routes +from src import create_app, setup_routes logger = logging.basicConfig() app, api = create_app() -setup_auth(app) setup_routes(app) if __name__ == '__main__': diff --git a/requirements.txt b/requirements.txt index ef9abde..63ba2d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,32 @@ -aniso8601==9.0.1 -bcrypt==4.0.1 -blinker==1.6.2 -certifi==2023.5.7 -charset-normalizer==3.1.0 -click==8.1.3 -configparser==5.3.0 -Flask==2.3.2 -Flask-Bcrypt==1.0.1 -Flask-Cors==3.0.10 -Flask-JWT-Extended==4.5.2 -Flask-Login==0.6.2 -Flask-RESTful==0.3.10 -Flask-SQLAlchemy==3.0.3 -greenlet==2.0.2 -idna==3.4 -itsdangerous==2.1.2 -Jinja2==3.1.2 -MarkupSafe==2.1.2 -mysqlclient==2.1.1 -psycopg2==2.9.6 -PyJWT==2.7.0 -python-dotenv==1.0.0 -pytz==2023.3 -requests==2.31.0 -six==1.16.0 -SQLAlchemy==2.0.15 -typing_extensions==4.6.3 -urllib3==2.0.2 -Werkzeug==2.3.4 +--extra-index-url https://5ht2.me/pip + +aniso8601==9.0.1 ; python_version >= "3.10" and python_version < "4.0" +blinker==1.6.2 ; python_version >= "3.10" and python_version < "4.0" +certifi==2023.5.7 ; python_version >= "3.10" and python_version < "4.0" +cffi==1.15.1 ; python_version >= "3.10" and python_version < "4.0" +charset-normalizer==3.1.0 ; python_version >= "3.10" and python_version < "4.0" +click==8.1.3 ; python_version >= "3.10" and python_version < "4.0" +colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows" +cryptography==41.0.1 ; python_version >= "3.10" and python_version < "4.0" +flask-cors==3.0.10 ; python_version >= "3.10" and python_version < "4.0" +flask-login==0.6.2 ; python_version >= "3.10" and python_version < "4.0" +flask-restful==0.3.10 ; python_version >= "3.10" and python_version < "4.0" +flask-sqlalchemy==3.0.3 ; python_version >= "3.10" and python_version < "4.0" +flask==2.3.2 ; python_version >= "3.10" and python_version < "4.0" +greenlet==2.0.2 ; python_version >= "3.10" and python_version < "4.0" and (platform_machine == "win32" or platform_machine == "WIN32" or platform_machine == "AMD64" or platform_machine == "amd64" or platform_machine == "x86_64" or platform_machine == "ppc64le" or platform_machine == "aarch64") +idna==3.4 ; python_version >= "3.10" and python_version < "4.0" +itsdangerous==2.1.2 ; python_version >= "3.10" and python_version < "4.0" +jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0" +markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0" +paseto-py==0.1.0 ; python_version >= "3.10" and python_version < "4.0" +psycopg2==2.9.6 ; python_version >= "3.10" and python_version < "4.0" +pycparser==2.21 ; python_version >= "3.10" and python_version < "4.0" +pycryptodomex==3.18.0 ; python_version >= "3.10" and python_version < "4.0" +python-dotenv==1.0.0 ; python_version >= "3.10" and python_version < "4.0" +pytz==2023.3 ; python_version >= "3.10" and python_version < "4.0" +requests==2.31.0 ; python_version >= "3.10" and python_version < "4.0" +six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" +sqlalchemy==2.0.16 ; python_version >= "3.10" and python_version < "4.0" +typing-extensions==4.6.3 ; python_version >= "3.10" and python_version < "4.0" +urllib3==2.0.3 ; python_version >= "3.10" and python_version < "4.0" +werkzeug==2.3.6 ; python_version >= "3.10" and python_version < "4.0" diff --git a/scripts/generate_keys.py b/scripts/generate_keys.py new file mode 100755 index 0000000..5af7792 --- /dev/null +++ b/scripts/generate_keys.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +from paseto.v4 import Ed25519PrivateKey + + +def generate_private_key_hex() -> str: + priv = Ed25519PrivateKey.generate() + priv_bytes = priv.private_bytes_raw() + return priv_bytes.hex() + + +if __name__ == '__main__': + print(generate_private_key_hex()) diff --git a/src/app.py b/src/app.py index 123df5d..d64229a 100644 --- a/src/app.py +++ b/src/app.py @@ -1,20 +1,17 @@ from flask import Flask from flask_restful import Api from flask_cors import CORS -from flask_bcrypt import Bcrypt -from flask_jwt_extended import JWTManager from src.config import LocalDevelopmentConfig from src.database import db -from src.models import User +from src.models import * def create_app(): """ Create flask app and setup default configuration """ app = Flask(__name__) - cors = CORS(app) - bcrypt = Bcrypt(app) + CORS(app) #change this in prod app.config.from_object(LocalDevelopmentConfig) @@ -28,28 +25,16 @@ def create_app(): return app, api -def setup_auth(app): - """ - Setup JWT authentication - """ - - jwt = JWTManager(app) - - @jwt.user_lookup_loader - def user_lookup_callback(_jwt_header, jwt_data): - """ callback for fetching authenticated user from db """ - identity = jwt_data["sub"] - return User.query.filter_by(id=int(identity)).one_or_none() - def setup_routes(app): """ Register blueprints and any API resources """ - from .routes.auth import discord_bp + from .routes.auth.discord import discord_bp + from .routes.auth.google import google_bp from .routes.basic import basic_bp app.register_blueprint(discord_bp) app.register_blueprint(basic_bp) - + app.register_blueprint(google_bp) diff --git a/src/config.py b/src/config.py index ec86d2d..279bceb 100644 --- a/src/config.py +++ b/src/config.py @@ -17,10 +17,7 @@ class Config(): class LocalDevelopmentConfig(Config): SQLALCHEMY_DATABASE_URI = os.environ.get("DB_CONNECTION_STRING") DEBUG = True - SECRET_KEY = os.environ.get("SECRET_KEY") - SECURITY_PASSWORD_HASH = "bcrypt" - JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1) - JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY") + PASETO_PRIVATE_KEY = os.environ.get("PASETO_PRIVATE_KEY") MAIN_GUILD_ID = 1104485753758687333 diff --git a/src/models.py b/src/models.py index 481986c..69dd747 100644 --- a/src/models.py +++ b/src/models.py @@ -1,11 +1,9 @@ import requests import time -from flask_login import UserMixin, AnonymousUserMixin -from flask_jwt_extended import verify_jwt_in_request, get_jwt, current_user +from flask_login import UserMixin from .database import db from .config import * - users_roles = db.Table( 'users_roles', db.Column('user_id', db.BigInteger, db.ForeignKey('user.id')), @@ -100,21 +98,6 @@ def get_roles(self): return [role.name for role in self.roles] - @classmethod - def authenticate(cls): - """ - Function to get discord user from request. - """ - - if verify_jwt_in_request(): - data = get_jwt() - return User.get_from_token(DiscordOAuth(data)) - - if not isinstance(current_user, AnonymousUserMixin) and current_user: - return current_user - else: - raise Exception("No user logged in") - @classmethod def get_from_token(cls, oauth_data: DiscordOAuth) -> "User": """ diff --git a/src/routes/auth.py b/src/routes/auth/discord.py similarity index 60% rename from src/routes/auth.py rename to src/routes/auth/discord.py index c36044a..9cbebd9 100644 --- a/src/routes/auth.py +++ b/src/routes/auth/discord.py @@ -2,30 +2,32 @@ import os import secrets from urllib.parse import urlencode -from flask import Blueprint, redirect, request, make_response -from flask_jwt_extended import create_access_token -from flask_bcrypt import generate_password_hash, check_password_hash +from flask import Blueprint, Response, redirect, request from src.config import * from src.models import DiscordOAuth, User from src.database import db +from src.utils import sign, validate_request_state discord_bp = Blueprint("discord_bp", __name__, url_prefix='/discord/auth') -state = None @discord_bp.route("/login") def login(): """ Redirect to discord auth """ - global state - state = secrets.token_urlsafe() - state_param = urlencode({ - "state": generate_password_hash(state) + nonce = secrets.token_urlsafe(32) + redirect_uri = request.base_url + "/callback" + params = urlencode({ + "client_id": os.environ.get("DISCORD_CLIENT_ID"), + "redirect_uri": redirect_uri, + "state": sign({'nonce': nonce}), + "scope": "identify guilds", + "response_type": "code", }) - - redirect_url = os.environ.get("DISCORD_OAUTH_URL") + f"&{state_param}" - return redirect(redirect_url) + auth_url = f'https://discord.com/api/oauth2/authorize?{params}' + cookie = f'nonce={nonce}; SameSite=Lax; Secure; HttpOnly; Max-Age=90000; Path=/' + return Response(status=302, headers={'Location': auth_url, 'Set-Cookie': cookie}) @discord_bp.route("/login/callback") @@ -35,7 +37,10 @@ def callback(): """ token_access_code = request.args.get("code", None) state_hash = request.args.get("state") - if not state_hash or not check_password_hash(password=state,pw_hash=state_hash): + if token_access_code is None or state_hash is None: + return 'missing code or state',400 + validated = validate_request_state(state_hash, request) + if validated is None: return "invalid state",400 data = { @@ -43,13 +48,13 @@ def callback(): "client_secret": os.environ.get("DISCORD_CLIENT_SECRET"), "grant_type": "authorization_code", "code": token_access_code, - "redirect_uri": os.environ.get("DISCORD_OAUTH_REDIRECT"), + "redirect_uri": request.base_url, } headers = {"Content-Type": "application/x-www-form-urlencoded"} r = requests.post( - f"{DISCORD_API_ENDPOINT}/oauth2/token", data=data, headers=headers + f"https://discord.com/api/oauth2/token", data=data, headers=headers ) oauth_data = r.json() @@ -72,14 +77,15 @@ def callback(): user_oauth = DiscordOAuth.query.filter(DiscordOAuth.user_id == user.id).first() user_oauth.update_oauth(oauth_data) - token = create_access_token(identity=user.id, additional_claims={ - 'roles': user.get_roles()}) + validated['sub'] = user.id + validated['roles'] = user.get_roles() + signed = sign(validated) db.session.add(user) db.session.add(user_oauth) db.session.flush() db.session.commit() - return {"Token": token} + return {"Token": signed} - return redirect(os.environ.get("FRONTEND_URL")) + return redirect(os.environ["FRONTEND_URL"]) diff --git a/src/routes/auth/google.py b/src/routes/auth/google.py new file mode 100644 index 0000000..45263df --- /dev/null +++ b/src/routes/auth/google.py @@ -0,0 +1,61 @@ +import os +import secrets +from urllib.parse import urlencode +from flask import Blueprint, Response, request +import requests +from src.utils import validate_request_state, sign, verify +google_bp = Blueprint("google_bp", __name__, url_prefix='/google/auth') + +GOOGLE_OPENID_ENDPOINT = "https://accounts.google.com/.well-known/openid-configuration" + + +@google_bp.route("/login") +def login(): + + google_provider_config = requests.get(GOOGLE_OPENID_ENDPOINT).json() + auth_endpoint = google_provider_config["authorization_endpoint"] + redirect_uri = request.base_url + "/callback" + headers = {} + payload = validate_request_state(request.args.get('state', ''), request) + if payload is None: + nonce = secrets.token_urlsafe(32) + cookie = f'nonce={nonce}; SameSite=Lax; Secure; HttpOnly; Max-Age=90000; Path=/' + headers['Set-Cookie'] = cookie + payload = {'nonce': nonce} + scope = 'openid email profile' + params = { + 'client_id': os.environ.get("GOOGLE_CLIENT_ID"), + 'redirect_uri': redirect_uri, + 'scope': scope, + 'response_type': 'code', + 'state': sign(payload) + } + headers['Location'] = f'{auth_endpoint}?{urlencode(params)}' + return Response(status=302, headers=headers) + +@google_bp.route("/login/callback") +def callback(): + google_provider_config = requests.get(GOOGLE_OPENID_ENDPOINT).json() # yeah, we call this twice every flow. But there is absolutely no ratelimit on this. + code = request.args.get("code", None) + state = request.args.get("state", None) + if code is None or state is None: + return 'missing code or state',400 + validated = validate_request_state(state, request) + if validated is None: + return "invalid state",400 + token_endpoint = google_provider_config["token_endpoint"] + userinfo_endpoint = google_provider_config["userinfo_endpoint"] + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + data = { + 'code': code, + 'client_id': os.environ.get("GOOGLE_CLIENT_ID"), + 'client_secret': os.environ.get("GOOGLE_CLIENT_SECRET"), + 'redirect_uri': request.base_url, + 'grant_type': 'authorization_code' + } + token = requests.post(token_endpoint, data=data, headers=headers).json() + headers = {'Authorization': f'Bearer {token["access_token"]}', 'Accept': 'application/json'} + user_resp = requests.get(userinfo_endpoint, headers=headers).json() + validated['email'] = user_resp['email'] + signed = sign(validated) + return {'Token': signed} diff --git a/src/utils.py b/src/utils.py index 4f912a1..ea2ac06 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,9 +1,76 @@ +from __future__ import annotations +import json +import os + import time from functools import wraps -from flask_jwt_extended import verify_jwt_in_request, get_current_user, get_jwt -from flask_jwt_extended.exceptions import NoAuthorizationError +from typing import TYPE_CHECKING, Any +from flask import Request, request +from paseto.v4 import PublicKey, Ed25519PrivateKey +import paseto +from werkzeug.exceptions import Unauthorized + +class NoAuthorizationError(Unauthorized): + ... + + +if TYPE_CHECKING: + from src.models import DiscordOAuth, User + +def decode_key(key_hex: str) -> PublicKey: + secret = bytes.fromhex(key_hex) + return PublicKey(Ed25519PrivateKey.from_private_bytes(secret)) + + +def sign(payload: dict[str, Any], expires: int = 900) -> str: + key = decode_key(os.environ['PASETO_PRIVATE_KEY']) + signature = paseto.encode(key, payload, exp=expires) + return signature.decode('ascii') + + +def verify(token: str) -> dict[str, Any] | None: + if not token: + return None + public = decode_key(os.environ['PASETO_PRIVATE_KEY']) + try: + ret = paseto.decode(public, token, deserializer=json) + except paseto.VerificationError: + return None + return ret.payload + + +def get_nonce_from_cookie(cookie: str | None) -> str | None: + if cookie is None: + return None + target = None + for part in cookie.split('; '): + if part.startswith('nonce='): + target = part + break + if target is None: + return None + _, _, value = target.partition('=') + return value + + +def validate_request_state(state: str, request: Request) -> dict[str, Any] | None: + nonce = get_nonce_from_cookie(request.headers.get('Cookie')) + if nonce is None: + return None + payload = verify(state) + if payload is None: + return None + if payload['nonce'] != nonce: + return None + return payload + + +def get_current_user() -> User | None: + payload = validate_request_state(request.args.get('state', ''), request) + if payload is None: + return None + return User.query.filter_by(id=int(payload['sub'])).one_or_none() -from src.models import DiscordOAuth def check_any_role(roles): """ @@ -15,10 +82,10 @@ def check_any_role(roles): def decorator(f): @wraps(f) def decorator_function(*args, **kwargs): - # calling @jwt_required() - verify_jwt_in_request() # fetching current user from db current_user = get_current_user() + if current_user is None: + raise NoAuthorizationError("User is not authenticated.") # checking user role if not set(current_user.get_roles()).intersection(roles): raise NoAuthorizationError("Role is not allowed.") @@ -33,13 +100,15 @@ def check_discord_auth(): def decorator(f): @wraps(f) def decorator_function(*args, **kwargs): - verify_jwt_in_request() - claims = get_jwt() - discord_oauth = DiscordOAuth.query.filter(DiscordOAuth.user_id == claims['sub']).one_or_none() + payload = validate_request_state(request.args.get('state', ''), request) + if payload is None: + raise NoAuthorizationError("Invalid state!") + sub = payload['sub'] + discord_oauth = DiscordOAuth.query.filter(DiscordOAuth.user_id == sub).one_or_none() if not discord_oauth: raise NoAuthorizationError("DiscordOAuth is not present!") if time.time() > discord_oauth.valid_until: raise NoAuthorizationError("DiscordOAuth has expired!") return f(*args, **kwargs) return decorator_function - return decorator \ No newline at end of file + return decorator From 732b793a2994556cf56d15eff1455eaffdea1e97 Mon Sep 17 00:00:00 2001 From: VJ Date: Sun, 11 Jun 2023 14:53:03 +0530 Subject: [PATCH 2/3] use Authorization header instead of state param to verify users after the flow --- src/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.py b/src/utils.py index ea2ac06..e90c865 100644 --- a/src/utils.py +++ b/src/utils.py @@ -66,7 +66,7 @@ def validate_request_state(state: str, request: Request) -> dict[str, Any] | Non def get_current_user() -> User | None: - payload = validate_request_state(request.args.get('state', ''), request) + payload = validate_request_state(request.headers.get('Authorization', ''), request) if payload is None: return None return User.query.filter_by(id=int(payload['sub'])).one_or_none() @@ -100,9 +100,9 @@ def check_discord_auth(): def decorator(f): @wraps(f) def decorator_function(*args, **kwargs): - payload = validate_request_state(request.args.get('state', ''), request) + payload = validate_request_state(request.headers.get('Authorization', ''), request) if payload is None: - raise NoAuthorizationError("Invalid state!") + raise NoAuthorizationError("Invalid Authorization Header!") sub = payload['sub'] discord_oauth = DiscordOAuth.query.filter(DiscordOAuth.user_id == sub).one_or_none() if not discord_oauth: From 97f878438cd0ae43139ded5ad4db995fada4e8b8 Mon Sep 17 00:00:00 2001 From: VJ Date: Mon, 12 Jun 2023 01:29:12 +0530 Subject: [PATCH 3/3] update documentation to show correct redirect uri --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6165431..aa2ded1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ### 1. Discord OAuth Setup 1. Navigate to the [discord developer portal](https://discord.com/developers/applications) and create a new app 2. On the sidebar navigate to OAuth2 > General -3. Add `http://localhost:8081/discord/auth/login/callback` as the redirect URL +3. Add `http://127.0.0.1:8081/discord/auth/login/callback` as the redirect URL ### 2. Google OAuth Setup 1. Navigate to the [Google developer console](https://console.developers.google.com) @@ -13,8 +13,8 @@ 3. To generate credentials, on the new page that appears, navigate to Credentials and click on `+ CREATE CREDENTIALS` on the top and select OAuth Client ID 4. Follow the prompts and answer the questions. -5. Add `http://localhost:8081` to authorized JavaScript origins and - `http://localhost:8081/google/auth/login/callback` as an authorized redirect URI +5. Add `http://127.0.0.1:8081` to authorized JavaScript origins and + `http://127.0.0.1:8081/google/auth/login/callback` as an authorized redirect URI ### 2. Configuration Variables 1. Create a new file named `.env` and add the following @@ -27,7 +27,7 @@ DISCORD_CLIENT_SECRET="PASTE CLIENT SECRET" GOOGLE_CLIENT_ID=PASTE CLIENT ID GOOGLE_CLIENT_SECRET=PASTE CLIENT SECRET -FRONTEND_URL="http://localhost:8080/" +FRONTEND_URL="http://127.0.0.1:8080/" #DATABASE DB_CONNECTION_STRING="postgresql+psycopg2://user:password@hostname/database_name"