Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migration vers ProConnect #4620

Draft
wants to merge 17 commits into
base: staging
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .env.docker
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ALLOWED_HOSTS=localhost
ALLOWED_HOSTS=localhost,127.0.0.1
SECRET=my local secret
DEBUG=True
DEBUG_FRONT=True
Expand Down Expand Up @@ -32,9 +32,9 @@ REDIS_PREPEND_KEY=LOCAL
OVERRIDE_TEST_SEED=
SIRET_API_KEY=
SIRET_API_SECRET=
MONCOMPTEPRO_CLIENT_ID=
MONCOMPTEPRO_SECRET=
MONCOMPTEPRO_CONFIG=
PROCONNECT_CLIENT_ID=
PROCONNECT_SECRET=
PROCONNECT_CONFIG=
MAX_DAYS_HISTORICAL_RECORDS=
CSV_PURCHASE_CHUNK_LINES=
ENABLE_XP_RESERVATION=True
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ repos:
args: [--line-length=119]
exclude: ^migrations/
- repo: https://github.com/pycqa/flake8
rev: 3.8.3
rev: 7.1.0
hooks:
- id: flake8
args: [--config=.flake8]
Expand Down
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: remote-debug Django",
"type": "python",
"request": "attach",
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app"
}
],
"port": 3000,
"host": "127.0.0.1",
"justMyCode": false
},
{
"name": "Python: Django",
"type": "debugpy",
Expand Down
1 change: 1 addition & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ services:
- "/app/node_modules"
ports:
- 8000:8000
- 3000:3000
env_file:
- ".env.docker"
- path: ".env"
Expand Down
6 changes: 3 additions & 3 deletions docs/ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ REDIS_PREPEND_KEY= Optionnel - Ajout ce string au début de chaque clé Redis. U
OVERRIDE_TEST_SEED= Optionnel - `seed` utilisé par les tests pour les éléments aléatoires. Utile lors qu'un test échoue et qu'on veut reproduire exactement ce qu'il s'est passé.
SIRET_API_KEY= Optionnel - pour utiliser l'API INSEE Sirene. Pour générer : accèder votre compte https://api.insee.fr/catalogue/ > My applications > Add application
SIRET_API_SECRET= Optionnel - pour utiliser l'API INSEE Sirene
MONCOMPTEPRO_CLIENT_ID= Optionnel - Client ID utilisé pour l'authentification via [MonComptePro](https://github.com/betagouv/moncomptepro).
MONCOMPTEPRO_SECRET= Optionnel - Secret utilisé pour l'authentification via [MonComptePro](https://github.com/betagouv/moncomptepro).
MONCOMPTEPRO_CONFIG= Optionnel - Url de configuration utilisé pour l'authentification via [MonComptePro](https://github.com/betagouv/moncomptepro). Par exemple : `https://app-test.moncomptepro.beta.gouv.fr/.well-known/openid-configuration`
PROCONNECT_CLIENT_ID= Optionnel - Client ID utilisé pour l'authentification via [ProConnect](https://github.com/numerique-gouv/proconnect-documentation).
PROCONNECT_SECRET= Optionnel - Secret utilisé pour l'authentification via [ProConnect](https://github.com/numerique-gouv/proconnect-documentation).
PROCONNECT_CONFIG= Optionnel - Url de configuration utilisé pour l'authentification via [ProConnect](https://github.com/numerique-gouv/proconnect-documentation/blob/main/resources/valeur_ac_domain.md). Par exemple : `https://[PROCONNECT_DOMAIN]/api/v2/.well-known/openid-configuration`
MAX_DAYS_HISTORICAL_RECORDS= Optionnel - Flag pour indiquer le nombre de jours pendant lequels on garde l'historique des modèles. Utilisé lors d'une tâche Celery.
CSV_PURCHASE_CHUNK_LINES= Optionnel - Définit le nombre de lignes dans chaque chunk pour l'import des achats. Le choix par défaut est de 81920 ce qui représente en moyenne des chunks de 3Mb.
```
Expand Down
12 changes: 6 additions & 6 deletions macantine/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,14 +563,14 @@
REDIS_PREPEND_KEY = os.getenv("REDIS_PREPEND_KEY", "")

AUTHLIB_OAUTH_CLIENTS = {
"moncomptepro": {
"client_id": os.getenv("MONCOMPTEPRO_CLIENT_ID"),
"client_secret": os.getenv("MONCOMPTEPRO_SECRET"),
"proconnect": {
"client_id": os.getenv("PROCONNECT_CLIENT_ID"),
"client_secret": os.getenv("PROCONNECT_SECRET"),
}
}
MONCOMPTEPRO_CONFIG = os.getenv("MONCOMPTEPRO_CONFIG")
USES_MONCOMPTEPRO = (
os.getenv("MONCOMPTEPRO_CLIENT_ID") and os.getenv("MONCOMPTEPRO_SECRET") and os.getenv("MONCOMPTEPRO_CONFIG")
PROCONNECT_CONFIG = os.getenv("PROCONNECT_CONFIG")
USES_PROCONNECT = (
os.getenv("PROCONNECT_CLIENT_ID") and os.getenv("PROCONNECT_SECRET") and os.getenv("PROCONNECT_CONFIG")
)
MAX_DAYS_HISTORICAL_RECORDS = (
int(os.getenv("MAX_DAYS_HISTORICAL_RECORDS")) if os.getenv("MAX_DAYS_HISTORICAL_RECORDS", None) else None
Expand Down
10 changes: 10 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ def main():
"""Run administrative tasks."""
dotenv.load_dotenv()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "macantine.settings")

from django.conf import settings

if settings.DEBUG:
if os.environ.get("RUN_MAIN") or os.environ.get("WERKZEUG_RUN_MAIN"):
import debugpy

debugpy.listen(("0.0.0.0", 3000))
print("Attached!")

try:
from django.core.management import execute_from_command_line
except ImportError as exc:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ cramjam==2.8.3
cryptography==43.0.1
cssbeautifier==1.15.1
cssselect2==0.7.0
debugpy==1.8.7
defusedxml==0.7.1
Deprecated==1.2.14
dill==0.3.9
Expand Down
27 changes: 27 additions & 0 deletions web/static/css/auth.css

Large diffs are not rendered by default.

16 changes: 0 additions & 16 deletions web/static/images/button-moncomptepro.svg

This file was deleted.

21 changes: 15 additions & 6 deletions web/templates/auth/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{% block title %}S'identifier{% endblock %}

{% load static %}
{% load moncomptepro %}
{% load proconnect %}

{% block content %}

Expand Down Expand Up @@ -60,11 +60,20 @@
</form>
<a href="{% url 'register' %}">Créer mon compte</a>
<hr aria-hidden="true" role="presentation" style="margin-top: 22px;" />
{% uses_moncomptepro as show_mcp_button %}
{% if show_mcp_button %}
<a href="{% url 'oidc-login' %}">
<img src="{% static 'images/button-moncomptepro.svg' %}" alt="S'identifier avec MonComptePro" />
</a>
{% uses_proconnect as show_proconnect_button %}
{% if show_proconnect_button %}
<div>
<form method="get" action="{% url 'oidc-login' %}">
<button class="proconnect-button">
<span class="proconnect-sr-only">S'identifier avec ProConnect</span>
</button>
</form>
<p>
<a href="https://www.proconnect.gouv.fr/" target="_blank" rel="noopener noreferrer" title="Qu’est-ce que ProConnect ? - nouvelle fenêtre">
Qu’est-ce que ProConnect ?
</a>
</p>
</div>
{% endif %}
<p style="font-size: 0.85em; margin-top: 20px;"><a href="{% url 'password_reset' %}">J'ai perdu mon mot de passe</a></p>
<p style="font-size: 0.85em; margin-top: 20px;"><a href="{% url 'magicauth-login' %}">Recevoir un lien de connexion par email</a></p>
Expand Down
23 changes: 15 additions & 8 deletions web/templates/auth/register.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends 'base.html' %}
{% block title %}Créer mon compte{% endblock %}
{% load moncomptepro %}
{% load proconnect %}
{% load static %}

{% block content %}
Expand Down Expand Up @@ -189,14 +189,21 @@

</form>
<div>
<a href="{% url 'login' %}">J'ai déjà un compte</a>
<p><a href="{% url 'login' %}">J'ai déjà un compte</a></p>
</div>
{% uses_moncomptepro as show_mcp_button %}
{% if show_mcp_button %}
<div style="margin-top: 16px;">
<a href="{% url 'oidc-login' %}">
<img src="{% static 'images/button-moncomptepro.svg' %}" alt="S'identifier avec MonComptePro" />
</a>
{% uses_proconnect as show_proconnect_button %}
{% if show_proconnect_button %}
<div>
<form method="get" action="{% url 'oidc-login' %}">
<button class="proconnect-button">
<span class="proconnect-sr-only">S'identifier avec ProConnect</span>
</button>
</form>
<p>
<a href="https://www.proconnect.gouv.fr/" target="_blank" rel="noopener noreferrer" title="Qu’est-ce que ProConnect ? - nouvelle fenêtre">
Qu’est-ce que ProConnect ?
</a>
</p>
</div>
{% endif %}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@


@register.simple_tag
def uses_moncomptepro():
return getattr(settings, "USES_MONCOMPTEPRO", "")
def uses_proconnect():
return getattr(settings, "USES_PROCONNECT", "")
2 changes: 1 addition & 1 deletion web/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
),
]

if settings.USES_MONCOMPTEPRO:
if settings.USES_PROCONNECT:
urlpatterns.append(
path(
"oidc-login",
Expand Down
83 changes: 55 additions & 28 deletions web/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@

logger = logging.getLogger(__name__)

if settings.USES_MONCOMPTEPRO:
if settings.USES_PROCONNECT:
oauth = OAuth()
oauth.register(
name="moncomptepro",
server_metadata_url=settings.MONCOMPTEPRO_CONFIG,
client_kwargs={"scope": "openid email profile organizations"},
name="proconnect",
server_metadata_url=settings.PROCONNECT_CONFIG,
client_kwargs={"scope": "openid email given_name usual_name siret"},
)


Expand Down Expand Up @@ -205,58 +205,85 @@ def _login_and_send_activation_email(username, request):
class OIDCLoginView(View):
def get(self, request, *args, **kwargs):
redirect_uri = request.build_absolute_uri(reverse_lazy("oidc-authorize"))
return oauth.moncomptepro.authorize_redirect(request, redirect_uri)
return oauth.proconnect.authorize_redirect(request, redirect_uri)


ID_TOKEN_KEY = "id_token"


class OIDCAuthorizeView(View):
def get(self, request, *args, **kwargs):
try:
token = oauth.moncomptepro.authorize_access_token(request)
mcp_data = oauth.moncomptepro.userinfo(token=token)
user = OIDCAuthorizeView.get_or_create_user(mcp_data)
token = oauth.proconnect.authorize_access_token(request)
user_data = OIDCAuthorizeView.userinfo(token)
user = OIDCAuthorizeView.get_or_create_user(user_data)
login(request, user)
return redirect(reverse_lazy("app"))
except Exception as e:
logger.exception("Error authenticating with MonComptePro")
logger.exception("Error authenticating with ProConnect")
logger.exception(e)
return redirect("app")

@staticmethod
def get_or_create_user(mcp_data):
mcp_id = mcp_data.get("sub")
mcp_email = mcp_data.get("email")
def get_or_create_user(user_data):
user_id = user_data.get("sub")
email = user_data.get("email")
siret = user_data.get("siret")
organizations = [{"siret": siret, "id": siret}] # recreate old MonComptePro structure

# Attempt with mcp_id
# Attempt with id provided by Identity Provider
try:
user = get_user_model().objects.get(mcp_id=mcp_id)
user.mcp_organizations = mcp_data.get("organizations")
user = get_user_model().objects.get(mcp_id=user_id)
user.mcp_organizations = organizations
user.save()
logger.info(f"MonComptePro user {mcp_id} (ID Ma Cantine: {user.id}) was found.")
logger.info(f"ProConnect user {user_id} (ID Ma Cantine: {user.id}) was found.")
return user
except get_user_model().DoesNotExist:
pass

# Attempt with email
try:
user = get_user_model().objects.get(email=mcp_email)
user.mcp_id = mcp_data.get("sub")
user.mcp_organizations = mcp_data.get("organizations")
user = get_user_model().objects.get(email=email)
user.mcp_id = user_id
user.mcp_organizations = organizations
user.save()
logger.info(f"MonComptePro user {mcp_id} was already registered in MaCantine with email {mcp_email}.")
logger.info(f"ProConnect user {user_id} was already registered in MaCantine with email {email}.")
return user
except get_user_model().DoesNotExist:
pass

# Create user
logger.info(f"Creating new user from MonComptePro user {mcp_id} with email {mcp_email}.")
last_name = user_data.get("usual_name")
logger.info(f"Creating new user from ProConnect user {user_id} with email {email}.")
user = get_user_model().objects.create(
first_name=mcp_data.get("given_name"),
last_name=mcp_data.get("family_name"),
email=mcp_email,
mcp_id=mcp_id,
phone_number=mcp_data.get("phone_number"),
username=f"{mcp_data.get('family_name')}-mcp-{mcp_id}",
mcp_organizations=mcp_data.get("organizations"),
first_name=user_data.get("given_name"),
last_name=last_name,
email=email,
mcp_id=user_id,
# phone_number=mcp_data.get("phone"),
username=f"{last_name}-proconnect-{user_id}",
mcp_organizations=organizations,
created_with_mcp=True,
)
return user

@staticmethod
def userinfo(token):
"""
Authlib's method (callable as oauth.proconnect.userinfo(token=token))
does not currently function with ProConnect tokens.
There are issues with a non-configurable leeway and the structure of the token
received by ProConnect.
This method takes their function as of v1.3.2 and rewrites it to fix the
issues manually. Inspired by:
https://github.com/datagouv/udata-front/blob/f227ce5a8bba9822717ebd5986f5319f45e1622f/udata_front/views/proconnect.py#L29
"""
metadata = oauth.proconnect.load_server_metadata()
resp = oauth.proconnect.get(metadata["userinfo_endpoint"], token=token)
resp.raise_for_status()
# Create a new token that `client.parse_id_token` expects. Replace the initial
# `id_token` with the jwt we received from the `userinfo_endpoint`.
userinfo_token = token.copy()
userinfo_token[ID_TOKEN_KEY] = resp.content
user_data = oauth.proconnect.parse_id_token(userinfo_token, nonce=None)
return user_data
Loading