diff --git a/.template.env b/.template.env index 410b43c..31b147b 100644 --- a/.template.env +++ b/.template.env @@ -33,6 +33,12 @@ USERS_DB_PATH=./users.sqlite SECRET_KEY= # à générer : python -c "import secrets; print(secrets.token_hex(32))" APP_BASE_URL=http://localhost:8050 +# Connexion LinkedIn (OpenID Connect) — créer une app sur le LinkedIn Developer Portal, +# activer "Sign In with LinkedIn using OpenID Connect", déclarer le redirect URI +# {APP_BASE_URL}/auth/linkedin/callback +LINKEDIN_CLIENT_ID= +LINKEDIN_CLIENT_SECRET= + # Brevo — envoi des emails transactionnels (vérification email, reset mot de passe) BREVO_API_KEY= # clé API transactionnelle Brevo BREVO_TEMPLATE_VERIFY_ID= # ID numérique du template "vérification d'adresse" diff --git a/pyproject.toml b/pyproject.toml index b407ad8..6fdb1a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "brevo-python==5.0.0rc1", "flask-wtf", "email-validator", + "authlib", "flask-cors>=6.0.2", "flask-smorest>=0.46.0", "marshmallow>=3.20.0", diff --git a/src/auth/oauth.py b/src/auth/oauth.py new file mode 100644 index 0000000..1e73b28 --- /dev/null +++ b/src/auth/oauth.py @@ -0,0 +1,21 @@ +import os + +from authlib.integrations.flask_client import OAuth +from flask import Flask + +LINKEDIN_DISCOVERY_URL = ( + "https://www.linkedin.com/oauth/.well-known/openid-configuration" +) + +oauth = OAuth() + + +def init_oauth(app: Flask) -> None: + oauth.init_app(app) + oauth.register( + name="linkedin", + client_id=os.getenv("LINKEDIN_CLIENT_ID"), + client_secret=os.getenv("LINKEDIN_CLIENT_SECRET"), + server_metadata_url=LINKEDIN_DISCOVERY_URL, + client_kwargs={"scope": "openid profile email"}, + ) diff --git a/src/auth/setup.py b/src/auth/setup.py index fc9e01f..8703877 100644 --- a/src/auth/setup.py +++ b/src/auth/setup.py @@ -6,6 +6,7 @@ from flask_wtf.csrf import CSRFProtect from src.auth import db, mailer from src.auth.models import load_user +from src.auth.oauth import init_oauth from src.utils import DEVELOPMENT, logger _csrf: CSRFProtect | None = None @@ -47,6 +48,8 @@ def init_auth(app: Flask) -> None: app.register_blueprint(auth_bp) + init_oauth(app) + _csrf = CSRFProtect(app) if not os.getenv("BREVO_API_KEY"): @@ -54,3 +57,9 @@ def init_auth(app: Flask) -> None: "BREVO_API_KEY non défini : les emails d'auth échoueront. " "Définissez les variables BREVO_* dans .env pour envoyer des emails." ) + + if not os.getenv("LINKEDIN_CLIENT_ID"): + logger.warning( + "LINKEDIN_CLIENT_ID non défini : la connexion LinkedIn échouera. " + "Définissez LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET dans .env." + ) diff --git a/tests/auth/conftest.py b/tests/auth/conftest.py index ea6e2d1..02134a0 100644 --- a/tests/auth/conftest.py +++ b/tests/auth/conftest.py @@ -19,6 +19,9 @@ def app(users_db_path, monkeypatch): from src.auth.setup import init_auth monkeypatch.setenv("SECRET_KEY", "test-secret-key") + monkeypatch.setenv("LINKEDIN_CLIENT_ID", "test-client-id") + monkeypatch.setenv("LINKEDIN_CLIENT_SECRET", "test-client-secret") + monkeypatch.setenv("APP_BASE_URL", "http://localhost:8050") app = Flask(__name__) app.config["WTF_CSRF_ENABLED"] = False init_auth(app)