Compare commits

...

17 Commits

Author SHA1 Message Date
Jonathan Perret 3401da37ff v1.0.9 release 2025-10-03 13:19:29 +02:00
Jonathan Perret 1124893498 Merge pull request #34 from proconnect-gouv/add-siret-mapping 2025-10-03 13:16:09 +02:00
Jonathan Perret 1557f7ba22 🔧(siret) add initial SIRET map to production environment
This mapping was provided by RENATER on 2025-09-19.
2025-10-03 13:10:40 +02:00
Jonathan Perret 6c344fae05 (siret) introduce SIRET mapping from EntityID
The proxy now uses a SIRET_MAP environment variable that must contain
a JSON mapping of SAML entity IDs to SIRET numbers.
These SIRET numbers will be returned to OIDC clients that request the
`siret` scope, in the `siret` claim.
2025-10-02 19:50:40 +02:00
Jonathan Perret cb8352787d Merge pull request #32 from proconnect-gouv/fix-acr-claim 2025-09-17 17:31:38 +02:00
Jonathan Perret 9a33149e9e (e2e) adapt to ProConnect error page wording change
The generic message was replaced by the error code
and description from the IdP.
2025-09-17 16:06:37 +02:00
Jonathan Perret a56ce17b34 ⬆️(oidc-test-client) upgrade oic to 1.7.0
This mainly avoids a conflict with the oidc2fer requirements
when installing both in a local virtualenv for IDE support.
2025-09-17 16:06:36 +02:00
Jonathan Perret a8bf8f61b4 🐛(oidc) stop forcing id_token 'acr' claim
There is a regression in pyop 3.4.2 which causes the configured
extra_id_token_claims to be ignored. Fortunately, ProConnect Core
now requests the required `acr` claim explicitly, so it is no longer
necessary to force it.

The OIDC test client was updated to request the `acr` claim in the
same way that ProConnect does it.
2025-09-17 16:06:35 +02:00
Jonathan Perret 7b1571b0ed (e2e) fix e2e test to more reliably select test IdP
The list of IdPs on the RENATER test federation seems to have
grown enough that the test IdP we want is no longer displayed
by default, so we have to search for it.
2025-09-15 12:15:05 +02:00
Jonathan Perret 3ecbf1a5a0 Fix docker registry for production 2025-09-10 18:23:52 +02:00
Jonathan Perret 445485ecc5 Deploy release 1.0.8 to production 2025-09-10 18:19:49 +02:00
Jonathan Perret 1bceed1373 v1.0.8 release 2025-09-10 18:08:39 +02:00
Jonathan Perret 850636970c 🚀(staging) add missing ghcr.io prefix to image name
Without a registry name, Docker Hub is still the default.
2025-09-10 18:06:02 +02:00
Jonathan Perret 9a1803b3e6 Merge pull request #31 from proconnect-gouv/move-to-proconnect 2025-09-10 17:56:47 +02:00
Jonathan Perret c51c95e007 💚(ci) fix CI jobs post org move
- use the GitHub app on numerique-gouv only to access the secrets repo
- inline helmfile-lint action (it only works on repos in numerique-gouv
  currently)
- replace deprecated `git whatchanged` with `git log`
- publish to ghcr.io instead of Docker Hub
- rename workflows to match other proconnect-gouv repos
2025-09-10 17:40:32 +02:00
Jonathan Perret 2687868465 📝(repo) update remaining references to numerique-gouv
Also updating CHANGELOG.
2025-09-10 17:40:30 +02:00
Jonathan Perret 38707412b3 🔧(secrets) change secrets submodule path
Since the oidcfer repo moved to a different organization the relative
path to the secrets submodule needs to change.
2025-09-10 15:24:56 +02:00
22 changed files with 266 additions and 80 deletions
+2 -2
View File
@@ -9,9 +9,9 @@ We primarily use GitHub as an issue tracker. If however you're encountering an i
---
Please make sure you have read our [main Readme](https://github.com/numerique-gouv/oidc2fer).
Please make sure you have read our [main Readme](https://github.com/proconnect-gouv/oidc2fer).
Also make sure it was not already answered in [an open or close issue](https://github.com/numerique-gouv/oidc2fer/issues).
Also make sure it was not already answered in [an open or close issue](https://github.com/proconnect-gouv/oidc2fer/issues).
If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌
@@ -1,7 +1,6 @@
name: Docker Hub Workflow
name: 🐳 Build docker images
on:
workflow_dispatch:
push:
branches:
- 'main'
@@ -14,42 +13,37 @@ on:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
-
uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: "oidc2fer,secrets"
-
name: Checkout repository
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
-
name: Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Docker meta
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: lasuite/oidc2fer
-
name: Load sops secrets
uses: rouja/actions-sops@main
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=ref,event=pr
type=sha,prefix=sha-
# set latest tag for default branch
type=raw,value=latest,enable={{is_default_branch}}
- name: Login to Github Container Registry
uses: docker/login-action@v3
with:
secret-file: secrets/numerique-gouv/oidc2fer/secrets.enc.env
age-key: ${{ secrets.SOPS_PRIVATE }}
-
name: Login to DockerHub
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
-
name: Build and push
uses: docker/build-push-action@v5
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
target: production
@@ -1,4 +1,4 @@
name: OIDC2FER Workflow
name: 💚 CI Tests
on:
push:
@@ -23,7 +23,7 @@ jobs:
run: git log
- name: Enforce absence of print statements in code
run: |
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/oidc2fer.yml' | grep "print("
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude).github/**' | grep "print("
- name: Check absence of fixup commits
run: |
! git log | grep 'fixup!'
@@ -43,7 +43,7 @@ jobs:
with:
fetch-depth: 0
- name: Check that the CHANGELOG has been modified in the current branch
run: git whatchanged --name-only --pretty="" origin/${{ github.event.pull_request.base.ref }}..HEAD | grep CHANGELOG
run: git log --name-only --pretty="" origin/${{ github.event.pull_request.base.ref }}..HEAD | grep CHANGELOG
lint-changelog:
runs-on: ubuntu-latest
@@ -101,3 +101,37 @@ jobs:
pip install --user .[dev]
- name: Run tests
run: ~/.local/bin/pytest
helmfile-lint:
runs-on: ubuntu-latest
container:
image: ghcr.io/helmfile/helmfile:v0.171.0
steps:
-
uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: numerique-gouv
repositories: secrets
-
name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
-
name: Helmfile lint
shell: bash
run: |
mkdir -p ~/.config/sops/age/
echo ${{ secrets.SOPS_PRIVATE }} > ~/.config/sops/age/keys.txt
set -e
HELMFILE=src/helm/helmfile.yaml
environments=$(awk '/environments:/ {flag=1; next} flag && NF {print} !NF {flag=0}' "$HELMFILE" | grep -E '^[[:space:]]{2}[a-zA-Z]+' | sed 's/^[[:space:]]*//;s/:.*//')
for env in $environments; do
echo "################### $env lint ###################"
helmfile -e $env -f src/helm/helmfile.yaml lint || exit 1
echo -e "\n"
done
-22
View File
@@ -1,22 +0,0 @@
name: Helmfile lint
run-name: Helmfile lint
on:
pull_request:
branches:
- 'main'
jobs:
helmfile-lint:
runs-on: ubuntu-latest
container:
image: ghcr.io/helmfile/helmfile:v0.171.0
steps:
-
uses: numerique-gouv/action-helmfile-lint@main
with:
app-id: ${{ secrets.APP_ID }}
age-key: ${{ secrets.SOPS_PRIVATE }}
private-key: ${{ secrets.PRIVATE_KEY }}
helmfile-src: "src/helm"
repositories: "oidc2fer,secrets"
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "secrets"]
path = secrets
url = ../secrets
url = ../../numerique-gouv/secrets
+9
View File
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.9] - 2025-10-03
- map SAML entity IDs to SIRET (#34)
- include ACR claim in ID token only if requested (#32)
## [1.0.8] - 2025-09-10
- fix secrets submodule path following move to `proconnect-gouv` organization
- update other references to `numerique-gouv` organization
- publish Docker images at ghcr.io/proconnect-gouv/oidc2fer
## [1.0.7] - 2025-05-07
- remove unused APT dependencies
- upgrade to gunicorn 23.0.0
+1 -1
View File
@@ -57,7 +57,7 @@ number and date.
## Deploying a release to production (DINUM instance)
1. Make sure the release you want to deploy has been built and appears on
https://hub.docker.com/r/lasuite/oidc2fer/tags .
https://github.com/proconnect-gouv/oidc2fer/pkgs/container/oidc2fer
2. Edit `image/tag` at the top of
`src/helm/env.d/outscale-production/values.oidc2fer.yaml.gotmpl`.
3. Commit and push to `main`.
+1 -1
View File
@@ -66,7 +66,7 @@ services:
#OIDC_PROVIDER: https://oidc2fer-staging.beta.numerique.gouv.fr
OIDC_CLIENT_ID: oidc-test-client
OIDC_CLIENT_SECRET: oidc-test-secret
OIDC_SCOPES: openid,uid,given_name,usual_name,email
OIDC_SCOPES: openid,uid,given_name,usual_name,email,siret
extra_hosts:
- "oidc2fer.127.0.0.1.nip.io:host-gateway"
+2 -4
View File
@@ -2,13 +2,10 @@ from flask import Flask, jsonify, request, session
from oic.oic import Client
from oic.utils.authn.client import CLIENT_AUTHN_METHOD
from oic import rndstr
from oic.oic.message import RegistrationResponse
from oic.oic.message import Claims, ClaimsRequest, RegistrationResponse
from oic.utils.http_util import Redirect
from oic.oic.message import AuthorizationResponse
import secrets
import webbrowser
import threading
import time
import logging
import os
@@ -52,6 +49,7 @@ def index():
"nonce": session["nonce"],
"redirect_uri": client.registration_response["redirect_uris"][0],
"state": session["state"],
"claims": ClaimsRequest(id_token=Claims(acr=None, amr=None)),
}
)
login_url = auth_req.request(client.authorization_endpoint)
+1 -1
View File
@@ -1,2 +1,2 @@
Flask==3.0.3
oic==1.6.1
oic==1.7.0
@@ -36,6 +36,10 @@ satosa:
"token_endpoint_auth_method": "client_secret_post"
}
}
SIRET_MAP: |-
{
"https://test-idp.federation.renater.fr/idp/shibboleth": "12345678200010"
}
ingress:
enabled: true
@@ -1,7 +1,7 @@
image:
repository: lasuite/oidc2fer
repository: ghcr.io/proconnect-gouv/oidc2fer
pullPolicy: Always
tag: "v1.0.7"
tag: "v1.0.8"
satosa:
replicas: 2
@@ -24,6 +24,94 @@ satosa:
OIDC_DB_URI: { secretKeyRef: { name: redis.redis.libre.sh, key: url } }
# As provided by RENATER on 2025-09-19
SIRET_MAP: |-
{
"https://shibboleth.grenoble-inp.fr/idp/shibboleth": "19381912500017",
"https://identites.ensea.fr/idp/shibboleth": "19951376300011",
"https://idp.ent.dauphine.fr/idp/shibboleth": "19754692200018",
"https://idp.univ-lyon2.fr/idp/shibboleth": "19691775100014",
"https://shibboleth.insa-rouen.fr/idp/shibboleth": "19760165100023",
"https://idp1.univ-fcomte.fr/idp/shibboleth": "19251215000363",
"https://idp.univ-tours.fr/idp/shibboleth": "19370800500478",
"https://apps.univ-lr.fr/idp/shibboleth": "19170032700015",
"https://shib.mines-albi.fr/idp/shibboleth": "19811200500022",
"https://federation.upf.pf/idp/shibboleth": "19987001500013",
"https://federation.utbm.fr/idp/shibboleth": "19900356700013",
"urn:mace:cru.fr:federation:univ-paris1.fr": "19751717000019",
"https://idp.sciencespobordeaux.fr/idp/shibboleth": "19330192600039",
"https://vip.espci.fr/saml2/idp/metadata.php": "20000068500012",
"urn:mace:cru.fr:federation:univ-nantes.fr": "13002974700016",
"https://shibboleth.univ-corse.fr/idp/shibboleth": "19202664900264",
"https://srv-fii.insa-toulouse.fr/idp/shibboleth": "19310152400018",
"urn:mace:cru.fr:federation:univ-rouen.fr": "19761904200017",
"https://ident-shib.ensc-rennes.fr/idp/shibboleth": "19350077400016",
"urn:mace:cru.fr:federation:unilim.fr": "19870669900321",
"https://idp.inha.fr/idp/shibboleth": "19754688000018",
"https://federation.unimes.fr/idp/shibboleth": "13000375900029",
"urn:mace:cru.fr:federation:univ-rennes1.fr": "13003051300019",
"urn:mace:cru.fr:federation:univ-ubs.fr": "19561718800600",
"https://idp.univ-orleans.fr/idp/shibboleth": "19450855200016",
"https://shibboleth.univ-savoie.fr/idp/shibboleth": "19730858800015",
"https://idp.cirad.fr/idp/shibboleth": "33159627000016",
"https://sso.ird.fr/idp/shibboleth": "18000602500159",
"https://idp.ensma.fr/idp/shibboleth": "19860073600021",
"https://idp.renater.fr/idp/shibboleth": "18008947600055",
"https://idp.inp-toulouse.fr/idp/shibboleth": "19311381800127",
"https://idp.unistra.fr/idp/shibboleth": "13000545700010",
"https://idp1.agroparistech.fr/idp/shibboleth": "13000285000134",
"https://janus.cnrs.fr/idp": "18008901303720",
"https://federation.umontpellier.fr/idp/shibboleth": "13002979600013",
"https://idp.sciencespo-lyon.fr/idp/shibboleth": "19690173000024",
"https://shibboleth.univ-grenoble-alpes.fr/idp/shibboleth": "13002608100013",
"https://idp.bnu.fr/idp/shibboleth": "18004406700015",
"https://ruhnu.univ-tlse2.fr/idp/shibboleth": "19311383400017",
"https://idp2.amue.fr/idp/shibboleth": "18004312700083",
"https://idpv3.univ-amu.fr/idp/shibboleth": "13001533200013",
"https://idp-ng.univ-st-etienne.fr/idp/shibboleth": "93850168100010",
"https://identities.univ-jfc.fr/idp/prod": "19811201300018",
"https://idp3.univ-lorraine.fr/idp/shibboleth": "13001550600012",
"https://idp.univ-lemans.fr/idp/shibboleth": "19720916600010",
"https://idp.univ-lille.fr/idp/shibboleth": "13002975400012",
"https://idp.univ-tln.fr/idp/shibboleth": "19830766200017",
"https://idp.uca.fr/idp/shibboleth": "13002806100013",
"https://shibboleth3.utt.fr/idp/shibboleth": "19101060200032",
"https://idp3.ut-capitole.fr/idp/shibboleth": "19311382600013",
"https://idp.imt-atlantique.fr/idp/shibboleth": "18009202500121",
"https://shib.univ-lehavre.fr/idp/shibboleth": "19762762300097",
"https://idp2.univ-paris8.fr/idp/shibboleth": "19931827000014",
"https://shib2.unc.nc/idp/shibboleth": "13000322100012",
"https://upnidp2.parisnanterre.fr/idp/shibboleth": "19921204400010",
"https://idp.ensfea.fr/idp/shibboleth": "19310143300012",
"https://idp.univ-eiffel.fr/idp/shibboleth": "13002612300013",
"https://idp.insa-rennes.fr/idp/shibboleth": "19350097200016",
"https://idp.univ-tlse3.fr/idp/shibboleth": "93827139200012",
"https://idp.universite-paris-saclay.fr/idp": "13002602400054",
"https://shibboleth.ens2m.fr/idp/shibboleth": "19250082500026",
"https://sso-ciation-saclay.centralesupelec.fr/idp/shibboleth": "13002076100016",
"https://federation.ens.psl.eu/idp/shibboleth": "19753459700012",
"https://authentification.inrae.fr/saml/metadata/idp": "18007003901803",
"https://idp.ensam.eu/idp/shibboleth": "19753472000010",
"https://idp1.ens-paris-saclay.fr/idp/shibboleth": "19940607500036",
"https://idp2.emse.fr/idp/shibboleth": "18009202500105",
"https://idp-septa.onisep.fr/idp/shibboleth": "18004302800653",
"https://papangue.vetagro-sup.fr/idp/shibboleth": "13000858400018",
"https://multipass.imt-nord-europe.fr/idp/shibboleth": "18009202500139",
"https://idp4.unicaen.fr/idp/shibboleth": "19141408500016",
"https://sso.bordeaux-inp.fr/idp/shibboleth": "13000635600013",
"https://auth.uttop.fr/saml/metadata": "19650048200019",
"https://idp.univ-cotedazur.fr/idp/shibboleth": "13002566100013",
"https://idp2.brgm.fr/idp/shibboleth": "58205614900120",
"https://idp.crous-reims.fr/idp/shibboleth": "18510200100012",
"https://idp.univ-avignon.fr/idp/shibboleth": "19840685200204",
"https://idp-genci.renater.fr/idp/shibboleth": "49468697500041",
"https://auth.sso.vet-alfort.fr/saml/metadata": "19940608300014",
"https://idp.univ-perp.fr/idp/shibboleth": "19660437500010",
"https://idp.institut-agro.fr/idp": "13000279300011",
"https://prod-idp.inria.fr": "18008904700013",
"https://idp.ec-nantes.fr/idp/shibboleth": "19440100600011"
}
ingress:
enabled: true
host: renater.agentconnect.gouv.fr
@@ -1,7 +1,7 @@
image:
repository: lasuite/oidc2fer
repository: ghcr.io/proconnect-gouv/oidc2fer
pullPolicy: Always
tag: "main"
tag: "latest"
satosa:
replicas: 2
@@ -24,6 +24,11 @@ satosa:
OIDC_DB_URI: { secretKeyRef: { name: redis.redis.libre.sh, key: url } }
SIRET_MAP: |-
{
"https://test-idp.federation.renater.fr/idp/shibboleth": "12345678200010"
}
ingress:
enabled: true
host: oidc2fer-staging.beta.numerique.gouv.fr
+3
View File
@@ -31,3 +31,6 @@ attributes:
uid:
openid:
- uid
siret:
openid:
- siret
@@ -0,0 +1 @@
from .entity_id_to_siret_mapper import EntityIdToSiretMapper
@@ -0,0 +1,23 @@
import json
import logging
from satosa.micro_services.base import ResponseMicroService
logger = logging.getLogger(__name__)
class EntityIdToSiretMapper(ResponseMicroService):
def __init__(self, config, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attribute = config.get("attribute", "siret")
self.mapping = json.loads(config.get("mapping_json", "{}"))
def process(self, context, data):
entity_id = data.auth_info.issuer
if entity_id in self.mapping:
siret = self.mapping[entity_id]
logger.info("Mapping entity ID %s to SIRET %s", entity_id, siret)
data.attributes[self.attribute] = siret
else:
logger.warning("No SIRET mapping found for entity ID %s", entity_id)
return super().process(context, data)
@@ -43,6 +43,7 @@ config:
- uid
- given_name
- usual_name
- siret
# Set code/token lifetimes as short as possible
authorization_code_lifetime: 60
@@ -56,8 +57,5 @@ config:
- given_name
usual_name:
- usual_name
extra_id_token_claims:
oidc-test-client:
- acr
agent-connect:
- acr
siret:
- siret
@@ -0,0 +1,5 @@
module: oidc2fer.attribute_generators.EntityIdToSiretMapper
name: EntityIdToSiretMapper
config:
attribute: siret
mapping_json: !ENV SIRET_MAP
+1
View File
@@ -17,6 +17,7 @@ MICRO_SERVICES:
- plugins/microservices/primary_identifier.yaml
- plugins/microservices/static_attributes.yaml
- plugins/microservices/attribute_processor.yaml
- plugins/microservices/siret_mapping.yaml
LOGGING:
# All the logging configuration is done in the Gunicorn config, this is just
# here to avoid overwriting it with the default SATOSA config
+4 -4
View File
@@ -31,10 +31,10 @@ dependencies = [
]
[project.urls]
"Bug Tracker" = "https://github.com/numerique-gouv/oidc2fer/issues/new"
"Changelog" = "https://github.com/numerique-gouv/oidc2fer/blob/main/CHANGELOG.md"
"Homepage" = "https://github.com/numerique-gouv/oidc2fer"
"Repository" = "https://github.com/numerique-gouv/oidc2fer"
"Bug Tracker" = "https://github.com/proconnect-gouv/oidc2fer/issues/new"
"Changelog" = "https://github.com/proconnect-gouv/oidc2fer/blob/main/CHANGELOG.md"
"Homepage" = "https://github.com/proconnect-gouv/oidc2fer"
"Repository" = "https://github.com/proconnect-gouv/oidc2fer"
[project.optional-dependencies]
dev = [
@@ -0,0 +1,42 @@
import json
from satosa.context import Context
from satosa.internal import AuthenticationInformation, InternalData
from oidc2fer.attribute_generators import EntityIdToSiretMapper
class TestEntityIdToSiretMapper:
def create_mapper(self):
mapper = EntityIdToSiretMapper(
config={
"attribute": "siret",
"mapping_json": json.dumps(
{
"https://idp.example.fr": "12345678200010",
}
),
},
name="siret_mapper",
base_url="https://satosa.example.com",
)
mapper.next = lambda ctx, data: data
return mapper
def test_sets_siret_for_known_entityid(self):
mapper = self.create_mapper()
resp = InternalData(
auth_info=AuthenticationInformation(issuer="https://idp.example.fr")
)
ctx = Context()
mapper.process(ctx, resp)
assert resp.attributes["siret"] == "12345678200010"
def test_does_not_set_siret_for_unknown_entityid(self):
mapper = self.create_mapper()
resp = InternalData(
auth_info=AuthenticationInformation(issuer="https://unknown.example.fr")
)
ctx = Context()
mapper.process(ctx, resp)
assert "siret" not in resp.attributes
+4 -1
View File
@@ -20,6 +20,7 @@ def renater_test_idp(page, login):
def renater_wayf(page):
page.get_by_text("Veuillez sélectionner").click()
page.get_by_role("searchbox").fill("GIP RENATER - IdP de test")
page.get_by_role("option", name="GIP RENATER - IdP de test", exact=True).click()
page.get_by_role("button", name="Sélection").click()
@@ -48,6 +49,7 @@ def oidc_to_renater(
"email": expected_email,
"given_name": expected_given_name,
"usual_name": expected_usual_name,
"siret": "12345678200010",
}.items() <= userinfo.items()
return id_token
@@ -101,6 +103,7 @@ def pro_connect_to_renater(
"email": expected_email,
"given_name": expected_given_name,
"usual_name": expected_usual_name,
"siret": "12345678200010",
}.items() <= result.items()
return result
@@ -125,7 +128,7 @@ def test_pro_connect_to_renater_student_not_allowed(page: Page):
renater_wayf(page)
renater_test_idp(page, login="etudiant1")
expect(page.locator("body")).to_contain_text("Une erreur technique est survenue.")
expect(page.locator("body")).to_contain_text("access_denied")
@pytest.mark.skipif(