diff --git a/imaginate_api/app.py b/imaginate_api/app.py
index 404bacf..32b08c9 100644
--- a/imaginate_api/app.py
+++ b/imaginate_api/app.py
@@ -1,14 +1,23 @@
 from flask import Flask, json, render_template
+from flask_cors import CORS
 from werkzeug.exceptions import HTTPException
 from imaginate_api.date.routes import bp as date_routes
 from imaginate_api.image.routes import bp as image_routes
+from imaginate_api.user.routes import bp as user_routes
 from imaginate_api.config import Config
+from imaginate_api.extensions import login_manager
+import os
+
 
 def create_app():
   app = Flask(__name__)
+  CORS(app)
   app.config.from_object(Config)
+  login_manager.init_app(app)
+  app.secret_key = os.getenv("FLASK_SECRET_KEY")
   app.register_blueprint(date_routes, url_prefix="/date")
   app.register_blueprint(image_routes, url_prefix="/image")
+  app.register_blueprint(user_routes, url_prefix="/user")
   return app
 
 
@@ -39,8 +48,9 @@ def handle_exception(exc: HTTPException):
 
 # Run app on invocation
 if __name__ == "__main__":
-  if Config.DB_ENV == 'prod':
+  if app.config["DB_ENV"] == "prod":
     from waitress import serve
+
     serve(app, host="0.0.0.0", port=8080)
   else:
     app.run()
diff --git a/imaginate_api/config.py b/imaginate_api/config.py
index b6ae442..3ae6c6e 100644
--- a/imaginate_api/config.py
+++ b/imaginate_api/config.py
@@ -21,4 +21,20 @@ class Config:
   MONGO_TOKEN = os.getenv("MONGO_TOKEN")
   PEXELS_TOKEN = os.getenv("PEXELS_TOKEN")
   DB_ENV = get_db_env()
+  AUTH_PROVIDERS = {
+    "google": {
+      "client_id": os.getenv("GOOGLE_CLIENT_ID"),
+      "client_secret": os.getenv("GOOGLE_CLIENT_SECRET"),
+      "authorize_url": "https://accounts.google.com/o/oauth2/auth",
+      "token_url": "https://accounts.google.com/o/oauth2/token",
+      "user_info": {
+        "url": "https://www.googleapis.com/oauth2/v3/userinfo",
+        "data": lambda json: {"email": json["email"], "id": json["sub"]},
+      },
+      "scopes": ["https://www.googleapis.com/auth/userinfo.email"],
+    }
+  }
+  BASE_URL = (
+    "https://playimaginate.com" if DB_ENV == "prod" else "http://localhost:5173"
+  )
   TESTING = False
diff --git a/imaginate_api/extensions.py b/imaginate_api/extensions.py
index 6f379be..cb24697 100644
--- a/imaginate_api/extensions.py
+++ b/imaginate_api/extensions.py
@@ -2,19 +2,21 @@
 import gridfs
 from imaginate_api.config import Config
 import sys
+from flask_login import LoginManager
 
 
 def connect_mongodb(conn_uri: str, db_name: str):
-    client = MongoClient(conn_uri)
+  client = MongoClient(conn_uri)
 
-    # If the connection was not established properly, an exception will be raised by this if statement
-    if db_name not in client.list_database_names():
-        print(f"Database \"{db_name}\" does not exist", file=sys.stderr)
-        sys.exit(1)
-    
-    return client[db_name], gridfs.GridFS(client[db_name])
+  # If the connection was not established properly, an exception will be raised by this if statement
+  if db_name not in client.list_database_names():
+    print(f'Database "{db_name}" does not exist', file=sys.stderr)
+    sys.exit(1)
+
+  return client[db_name], gridfs.GridFS(client[db_name])
 
 
 # Setup
-print(f"Running in \"{Config.DB_ENV}\" environment")
-db, fs = connect_mongodb(Config.MONGO_TOKEN, f"imaginate_{Config.DB_ENV}")
\ No newline at end of file
+print(f'Running in "{Config.DB_ENV}" environment')
+db, fs = connect_mongodb(Config.MONGO_TOKEN, f"imaginate_{Config.DB_ENV}")
+login_manager = LoginManager()
diff --git a/imaginate_api/schemas/user_info.py b/imaginate_api/schemas/user_info.py
new file mode 100644
index 0000000..f4bb2f1
--- /dev/null
+++ b/imaginate_api/schemas/user_info.py
@@ -0,0 +1,81 @@
+from bson.objectid import ObjectId
+from flask_login import UserMixin
+from imaginate_api.extensions import login_manager
+from imaginate_api.extensions import db
+
+# Specification: https://flask-login.readthedocs.io/en/latest/#
+COLLECTION_NAME = "users"
+COLLECTION = db[COLLECTION_NAME]
+
+
+class User(UserMixin):
+  def __init__(self, user_data=None):
+    self.user_data = user_data or {}
+
+  @property
+  def is_authenticated(self):
+    return self.user_data.get("authenticated", False)
+
+  @property
+  def is_active(self):
+    return self.user_data.get("active", False)
+
+  @property
+  def is_anonymous(self):
+    return False  # Always return False based on specification
+
+  def get_id(self):
+    return str(self.user_data["_id"])
+
+  def get_clientside_data(self):
+    return {
+      "email": self.user_data.get("email"),
+    }
+
+  def authenticate_user(self):
+    COLLECTION.update_one(
+      {"_id": self.user_data["_id"]}, {"$set": {"authenticated": True}}
+    )
+    self.user_data["authenticated"] = True
+
+  def deactivate_user(self):
+    COLLECTION.update_one({"_id": self.user_data["_id"]}, {"$set": {"active": False}})
+    self.user_data["active"] = False
+
+  @classmethod
+  def find_or_create_user(cls, data, provider=None):
+    # Primary identifier: Try to find the existing user by using the unique ID from the provider
+    if provider:
+      existing_user = COLLECTION.find_one({f"{provider}_id": data["id"]})
+      if existing_user:
+        return User(user_data=existing_user)
+
+    # Secondary identifier: Try to find the existing user by using the email from the provider
+    existing_user = COLLECTION.find_one({"email": data["email"]})
+    if existing_user:
+      if provider:
+        COLLECTION.update_one(
+          {"_id": existing_user["_id"]}, {"$set": {f"{provider}_id": data["id"]}}
+        )
+      return User(user_data=existing_user)
+
+    # If no user is found, create a new one
+    data["authenticated"] = False
+    data["active"] = True
+    data[f"{provider}_id"] = data.pop("id")
+    new_user = COLLECTION.insert_one(data)
+    return User.get(new_user.inserted_id)
+
+  # Get user by ID
+  @classmethod
+  def get(cls, user_id):
+    user = COLLECTION.find_one({"_id": ObjectId(user_id)})
+    if not user:
+      return None
+    return cls(user_data=user)
+
+
+# Callback function for Flask login library to load user from session user_id
+@login_manager.user_loader
+def load_user(user_id):
+  return User.get(user_id)
diff --git a/imaginate_api/user/routes.py b/imaginate_api/user/routes.py
new file mode 100644
index 0000000..6fff3da
--- /dev/null
+++ b/imaginate_api/user/routes.py
@@ -0,0 +1,98 @@
+from flask import Blueprint, abort, request, redirect, url_for, session, current_app
+from flask_login import current_user, login_user
+from imaginate_api.schemas.user_info import User
+from http import HTTPStatus
+from urllib.parse import urlencode
+import secrets
+import requests
+
+bp = Blueprint("user", __name__)
+
+
+# Initiates the authorization process with the specified provider
+@bp.route("/authorize/<provider>")
+def user_authorize(provider):
+  if not current_user.is_anonymous:
+    return redirect(f'{current_app.config["BASE_URL"]}/?{urlencode(current_user.get_clientside_data())}')
+
+  provider_data = current_app.config["AUTH_PROVIDERS"].get(provider)
+  if not provider_data:
+    abort(
+      HTTPStatus.NOT_FOUND,
+      description=f"Invalid provider, supports: {list(current_app.config["AUTH_PROVIDERS"].keys())}",
+    )
+
+  session["oauth_state"] = secrets.token_urlsafe(32)
+  query = urlencode(
+    {
+      "client_id": provider_data["client_id"],
+      "redirect_uri": url_for("user.user_callback", provider=provider, _external=True),
+      "response_type": "code",  # This tells the OAuth provider that we expect an authorization code to be returned
+      "scope": " ".join(provider_data["scopes"]),
+      "state": session["oauth_state"],
+    }
+  )
+
+  return redirect(f"{provider_data["authorize_url"]}?{query}")
+
+
+# Handles the callback (i.e. redirection response) process with the specified provider
+@bp.route("/callback/<provider>")
+def user_callback(provider):
+  if not current_user.is_anonymous:
+    return redirect(current_app.config["BASE_URL"])
+
+  provider_data = current_app.config["AUTH_PROVIDERS"].get(provider)
+  if not provider_data:
+    abort(
+      HTTPStatus.NOT_FOUND,
+      description=f"Invalid provider, supports: {list(current_app.config["AUTH_PROVIDERS"].keys())}",
+    )
+
+  # Unable to authenticate with the specified provider
+  if "error" in request.args:
+    for k, v in request.args.items():
+      if k.startswith("error"):
+        print(f"{k}: {v}")  # Debug any errors by printing them
+    abort(HTTPStatus.BAD_REQUEST, description="Authentication error")
+
+  # Authorization does not match the specification we have set
+  if request.args["state"] != session.get("oauth_state") or "code" not in request.args:
+    abort(HTTPStatus.BAD_REQUEST, description="Authorization error")
+
+  # Get an access token from the authorization code
+  response = requests.post(
+    provider_data["token_url"],
+    data={
+      "client_id": provider_data["client_id"],
+      "client_secret": provider_data["client_secret"],
+      "code": request.args["code"],
+      "grant_type": "authorization_code",
+      "redirect_uri": url_for("user.user_callback", provider=provider, _external=True),
+    },
+    headers={"Accept": "application/json"},
+  )
+  if not response.ok:
+    abort(response.status_code, description="Authorization error")
+  response_data = response.json()
+  token = response_data.get("access_token")
+  if not token:
+    abort(HTTPStatus.UNAUTHORIZED, description="Authorization error")
+
+  # Get the requested data
+  response = requests.get(
+    provider_data["user_info"]["url"],
+    headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
+  )
+  if not response.ok:
+    abort(response.status_code, description="Authorization error")
+
+  # Login user and map requested data
+  user_data = provider_data["user_info"]["data"](response.json())
+  user = User.find_or_create_user(user_data, provider)
+  success = login_user(user)
+  if success:
+    user.authenticate_user()
+    return redirect(f'{current_app.config["BASE_URL"]}/?{urlencode(user.get_clientside_data())}')
+
+  return redirect(current_app.config["BASE_URL"])
diff --git a/poetry.lock b/poetry.lock
index 1312b67..824fcc2 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,14 +1,14 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
 
 [[package]]
 name = "blinker"
-version = "1.8.2"
+version = "1.9.0"
 description = "Fast, simple object-to-object and broadcast signaling"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"},
-    {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"},
+    {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"},
+    {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"},
 ]
 
 [[package]]
@@ -241,15 +241,44 @@ Werkzeug = ">=2.3.7"
 async = ["asgiref (>=3.2)"]
 dotenv = ["python-dotenv"]
 
+[[package]]
+name = "flask-cors"
+version = "5.0.0"
+description = "A Flask extension adding a decorator for CORS support"
+optional = false
+python-versions = "*"
+files = [
+    {file = "Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc"},
+    {file = "flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef"},
+]
+
+[package.dependencies]
+Flask = ">=0.9"
+
+[[package]]
+name = "flask-login"
+version = "0.6.3"
+description = "User authentication and session management for Flask."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333"},
+    {file = "Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d"},
+]
+
+[package.dependencies]
+Flask = ">=1.0.4"
+Werkzeug = ">=1.0.1"
+
 [[package]]
 name = "identify"
-version = "2.6.1"
+version = "2.6.2"
 description = "File identification library for Python"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"},
-    {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"},
+    {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"},
+    {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"},
 ]
 
 [package.extras]
@@ -401,13 +430,13 @@ files = [
 
 [[package]]
 name = "marshmallow"
-version = "3.23.0"
+version = "3.23.1"
 description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "marshmallow-3.23.0-py3-none-any.whl", hash = "sha256:82f20a2397834fe6d9611b241f2f7e7b680ed89c49f84728a1ad937be6b4bdf4"},
-    {file = "marshmallow-3.23.0.tar.gz", hash = "sha256:98d8827a9f10c03d44ead298d2e99c6aea8197df18ccfad360dae7f89a50da2e"},
+    {file = "marshmallow-3.23.1-py3-none-any.whl", hash = "sha256:fece2eb2c941180ea1b7fcbd4a83c51bfdd50093fdd3ad2585ee5e1df2508491"},
+    {file = "marshmallow-3.23.1.tar.gz", hash = "sha256:3a8dfda6edd8dcdbf216c0ede1d1e78d230a6dc9c5a088f58c4083b974a0d468"},
 ]
 
 [package.dependencies]
@@ -415,18 +444,18 @@ packaging = ">=17.0"
 
 [package.extras]
 dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"]
-docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)", "sphinx-version-warning (==1.1.2)"]
+docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.14)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)", "sphinx-version-warning (==1.1.2)"]
 tests = ["pytest", "simplejson"]
 
 [[package]]
 name = "mongomock"
-version = "4.2.0.post1"
+version = "4.3.0"
 description = "Fake pymongo stub for testing simple MongoDB-dependent code"
 optional = false
 python-versions = "*"
 files = [
-    {file = "mongomock-4.2.0.post1-py2.py3-none-any.whl", hash = "sha256:ff78f1944bf0cdcfc291ece198357db805c2f0db39e814bcef8a43c9f53e8a81"},
-    {file = "mongomock-4.2.0.post1.tar.gz", hash = "sha256:9241d2cec7274b9736dbe8edacb19528ff66af3b3779b324d79ecc4201227f31"},
+    {file = "mongomock-4.3.0-py2.py3-none-any.whl", hash = "sha256:5ef86bd12fc8806c6e7af32f21266c61b6c4ba96096f85129852d1c4fec1327e"},
+    {file = "mongomock-4.3.0.tar.gz", hash = "sha256:32667b79066fabc12d4f17f16a8fd7361b5f4435208b3ba32c226e52212a8c30"},
 ]
 
 [package.dependencies]
@@ -451,13 +480,13 @@ files = [
 
 [[package]]
 name = "packaging"
-version = "24.1"
+version = "24.2"
 description = "Core utilities for Python packages"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
-    {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
+    {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+    {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
 ]
 
 [[package]]
@@ -748,23 +777,23 @@ files = [
 
 [[package]]
 name = "setuptools"
-version = "75.2.0"
+version = "75.5.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "setuptools-75.2.0-py3-none-any.whl", hash = "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8"},
-    {file = "setuptools-75.2.0.tar.gz", hash = "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec"},
+    {file = "setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829"},
+    {file = "setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef"},
 ]
 
 [package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"]
-core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"]
+core = ["importlib-metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
 cover = ["pytest-cov"]
 doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
 enabler = ["pytest-enabler (>=2.2)"]
-test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
-type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
+type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"]
 
 [[package]]
 name = "urllib3"
@@ -785,13 +814,13 @@ zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "virtualenv"
-version = "20.27.0"
+version = "20.27.1"
 description = "Virtual Python Environment builder"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"},
-    {file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"},
+    {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"},
+    {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"},
 ]
 
 [package.dependencies]
@@ -805,28 +834,28 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
 
 [[package]]
 name = "waitress"
-version = "3.0.0"
+version = "3.0.2"
 description = "Waitress WSGI server"
 optional = false
-python-versions = ">=3.8.0"
+python-versions = ">=3.9.0"
 files = [
-    {file = "waitress-3.0.0-py3-none-any.whl", hash = "sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669"},
-    {file = "waitress-3.0.0.tar.gz", hash = "sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1"},
+    {file = "waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e"},
+    {file = "waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f"},
 ]
 
 [package.extras]
 docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"]
-testing = ["coverage (>=5.0)", "pytest", "pytest-cov"]
+testing = ["coverage (>=7.6.0)", "pytest", "pytest-cov"]
 
 [[package]]
 name = "werkzeug"
-version = "3.0.6"
+version = "3.1.3"
 description = "The comprehensive WSGI web application library."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17"},
-    {file = "werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d"},
+    {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"},
+    {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"},
 ]
 
 [package.dependencies]
@@ -838,4 +867,4 @@ watchdog = ["watchdog (>=2.3)"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.12"
-content-hash = "25f15be63699528889e3b0acb237c7f3f309fd701803396c1c3a872994cfefdc"
+content-hash = "9dd3ea08dbf35ff0a59d9db55dbf7feb5bc9030429cca6dc32cab866c164f6f0"
diff --git a/pyproject.toml b/pyproject.toml
index 9fdfa99..09a5bf2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,6 +17,8 @@ image-handler-client = {git = "https://github.com/imaginate-ai/image-handler-cli
 requests = "^2.32.3"
 waitress = "^3.0.0"
 setuptools = "^75.2.0"
+flask-login = "^0.6.3"
+flask-cors = "^5.0.0"
 
 [tool.poetry.group.dev.dependencies]
 pre-commit = "^3.7.1"