Compare commits

...

67 Commits

Author SHA1 Message Date
Jonathan Perret 877f2f896b 🔧(config) add healthcheck to docker-compose
This makes docker-compose up --wait work.
2025-11-21 11:48:41 +01:00
Jonathan Perret 5884e28292 🔧(cnb) temporarily hardcode email
Our test account currently does not have an email
address in the IdP.
2025-11-21 11:46:50 +01:00
Jonathan Perret b62fae53ec 🔧(cnb) request signed assertions or response
At least in the "recette" environment responses are not signed, but
assertions are.
2025-11-21 11:46:49 +01:00
Jonathan Perret 4a4a180ef1 🔧(cnb) change supported name_id_format
To comply with the CNB IdP we need to support
urn:oasis:names:tc:SAML:2.0:nameid-format:transient.
2025-11-21 11:46:48 +01:00
Jonathan Perret 138bc431fd 📌(pysaml2) pin pysaml2 to avoid double-signing of AuthNRequests
The current version of pysaml2 (7.5.2) has an issue where AuthNRequests
are both signed in the XML and with an extra `Signature` queryparam.
This was reported initially in 2021:
    https://github.com/IdentityPython/pysaml2/issues/819
And it was fixed by a changed in SATOSA:
    https://github.com/IdentityPython/SATOSA/pull/380
But it reappeared apparently and the original reporter has a PR open
against pysaml2 that is supposed to fix it:
    https://github.com/IdentityPython/pysaml2/pull/973
They report that the regression was introduced in pysaml2 by
   https://github.com/IdentityPython/pysaml2/pull/834
We try here to pin pysaml2 to the last version before this PR was
merged. Unfortunately this is quite an old version, but from basic
testing it seems to still be compatible with the current SATOSA
version.
This in turn forces us to also pin xmlschema to avoid
    https://github.com/IdentityPython/pysaml2/issues/947

Hopefully this can be temporary.
2025-11-21 11:46:47 +01:00
Jonathan Perret fbb190b455 🧑‍💻(oidc-test-client) configure OIDC client on each request
Downloading the discovery document from the OIDC provider on
each request rather than on startup lets the oidc-test-client be
independent of the availability of the OIDC provider, which is useful
in development scenarios.
2025-11-21 11:46:46 +01:00
Jonathan Perret 2cb64a42a8 🚀(cnb) create cnb-dev environment
For local validation of Helm chart using Tilt.
2025-11-21 11:46:44 +01:00
Jonathan Perret 917313bb5a 🚀(cnb) create cnb-staging environment
Test deployment for CNB.
2025-11-21 11:45:33 +01:00
Jonathan Perret 769342494f 🔧(config) create config for CNB
First attempt at a configuration that may work for interoperation
with the CNB IdP.
2025-11-21 11:43:42 +01:00
Jonathan Perret 8ee0bacee0 🔧(config) create instance-specific configuration directories
The Docker image will now expect the working directory to be set,
for example to /app/config/fer for the current (RENATER) configuration.
2025-11-21 11:42:25 +01:00
Jonathan Perret dda5bb918d 🔧(config) create instance-specific configuration directories
The Docker image will now expect the working directory to be set,
for example to /app/config/fer for the current (RENATER) configuration.
2025-11-21 11:42:23 +01:00
Jonathan Perret 46c7a7eea7 🔨(dockerfile) fix development image to work with a different working dir
Because the `pip install --editable` command was run from an empty
directory, the `oidc2fer` directory that was later copied from the sources
did not show up as an importable module. This worked before changing
the working directory because the import falled back on looking in the
current directory.
2025-11-21 11:40:21 +01:00
Jonathan Perret 5a2d3bb35b 🔧(secrets) update secrets to current main
No change for oidc2fer secrets.
2025-11-21 11:40:20 +01:00
Jonathan Perret 6dc94af598 📌(helm) move to bitnamilegacy image for local Redis
Bitnami moved their images…
2025-11-21 11:40:19 +01:00
Jonathan Perret 44b9879a3b Deploy release v1.0.10 to production 2025-11-21 11:10:07 +01:00
Jonathan Perret 7c3a0fccc9 v1.0.10 release 2025-11-21 11:07:03 +01:00
Jonathan Perret c224e31a66 Merge pull request #35 from rouja/helm-refacto
⬆️(helmfile) rename helmfile to be compatible with new helmfile plugin
2025-11-20 16:09:14 +01:00
Jacques ROUSSEL 4892192b85 ⬆️(helmfile) rename helmfile.yaml to helmfile.yaml.gotmpl
For compatibility with helmfile v1.x.
2025-11-20 16:02:50 +01:00
Jonathan Perret 79aee346a2 🔧(siret) update SIRET map to replace closed entities
These new SIRET numbers were found using
https://annuaire-entreprises.data.gouv.fr/
2025-10-23 10:21:57 +02:00
Jonathan Perret 4e84ec0f2b Deploy release v1.0.9 to production 2025-10-03 13:22:03 +02:00
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
Jonathan Perret 8cd89621b2 Deploy release v1.0.7 to production. 2025-05-07 17:08:42 +02:00
Jonathan Perret 425365d22a v1.0.7 release 2025-05-07 17:07:48 +02:00
Jonathan Perret 63806bde22 Merge pull request #30 from numerique-gouv/fix-cve
Remove unused APT dependencies that trigger a CVE warning
2025-05-07 13:46:13 +02:00
Jonathan Perret 96299e1386 ⬆️ (deps) Bump gunicorn to 23.0.0 2025-05-07 13:43:45 +02:00
Jonathan Perret bf015c9238 🔒️(docker) remove unused APT dependencies that trigger a CVE warning
Specifically CVE-2023-45853 was flagged in our Docker image, but
it is in libfreetype6 which we don't use, and is only pulled in
because of a recommended depdency of `gcc` — which we don't need
in any case.
2025-05-07 13:43:45 +02:00
Jacques ROUSSEL 589d447f19 🐛(ci) fix helmfile linter
Latest helmfile version breaks lots of thing.
2025-03-28 14:46:38 +01:00
Jacques ROUSSEL ddc09cca50 🐛(ci) use github action for argocd webhook notification
In order to refactor this notification between alls projetcs, we
chooseto use a custom github action
2025-03-28 14:46:38 +01:00
Jonathan Perret ae2d4a573b Deploy release v1.0.6 to production. 2025-03-11 16:34:02 +01:00
Jonathan Perret 5be05fe43e v1.0.6 release 2025-03-11 16:31:50 +01:00
Jonathan Perret f90df5c2c6 Merge pull request #27 from numerique-gouv/allow-more-affiliations
Allow more affiliations
2025-03-11 16:02:10 +01:00
Jonathan Perret 6bbcd95069 🔧(config) allow more affiliations
We agreed with RENATER to allow people having one of the following
affiliations: faculty, staff, employee, researcher, teacher.
2025-03-11 15:42:27 +01:00
Jonathan Perret b839c32b50 Merge pull request #26 from numerique-gouv/satosa-8.5.1
⬆️ (deps) upgrade to SATOSA 8.5.1
2025-03-11 15:38:15 +01:00
Jonathan Perret 9b062aa27d ⬆️ (deps) upgrade to SATOSA 8.5.1 2025-03-11 15:23:36 +01:00
Jonathan Perret 4a2ece217b Deploy release v1.0.5 to production. 2025-03-06 19:48:00 +01:00
Jonathan Perret a7577ff85e v1.0.5 release 2025-03-06 19:38:28 +01:00
Jonathan Perret 4e81e1e750 Merge pull request #25 from numerique-gouv/custom-log-levels
🚀(logging) allow customizing log levels per logger
2025-03-06 19:36:05 +01:00
Jonathan Perret c27ebfe6ae 🚀(logging) allow customizing log levels per logger
This lets one enable increased logging at a finer level. For example,
one could enable debug logging for a specific logger, while keeping the
rest of the application at info level.
2025-03-06 19:27:01 +01:00
Jonathan Perret d84f2b32d5 Merge pull request #24 from numerique-gouv/filter-paths
🔒️(nginx) add basic path allowlist to block random attacks
2025-03-05 18:32:09 +01:00
Jonathan Perret 9cb67bfb87 🔒️(nginx) add basic path allowlist
This should cut down on opportunistic attack attempts, which are more
a nuisance than anything right now but it can't hurt to stop them
earlier.
2025-03-05 18:26:45 +01:00
Jonathan Perret ba1774bdbf Merge pull request #23 from numerique-gouv/fix-e2e-test
(e2e) fix e2e test with PC staging
2025-03-05 18:18:45 +01:00
Jonathan Perret cb8ff5d878 (e2e) fix e2e test with PC staging
Adapt to updated mock service.
2025-03-05 17:52:03 +01:00
rouja a6e01c071e Merge pull request #21 from numerique-gouv/add-pdbs
🔧(helm) add pdbs to deployements
2025-02-10 17:24:10 +01:00
Jacques ROUSSEL 5a0f2da202 🔧(helm) add pdbs to deployements
In order to avoid a service interruption during a Kubernetes
(k8s)upgrade, we add a Pod Disruption Budget (PDB) to deployments.
2025-02-10 17:19:08 +01:00
rouja 3b2d50d657 Merge pull request #20 from numerique-gouv/fix-issuer
fix letsencrypt issuer for new preprod
2025-02-07 11:53:59 +01:00
Jacques ROUSSEL 52d7de305c 🔧(helm) change letsencrypt issuer
The name of the cluster issuer on the new preprod is 'letsencrypt,' so I
adjusted it.
2025-02-07 10:49:36 +01:00
Jonathan Perret 64a0d4dd3d Merge pull request #19 from numerique-gouv/improve-helm 2024-10-02 10:46:04 +02:00
Jacques ROUSSEL ffbd161e29 (ci) add helmfile linter
Add a github job to run helmfile linter on PR
2024-09-24 17:11:13 +02:00
Jonathan Perret ba1c14291d Merge pull request #17 from numerique-gouv/add-architecture-doc
📝(architecture) add first architecture doc
2024-09-21 12:21:01 +02:00
Jonathan Perret 4c4ccc88ba 📝(architecture) add first architecture doc
This gives an overview and the detail of the workings of OIDC2FER.
2024-09-18 19:31:33 +02:00
Jonathan Perret c1b33453c3 Deploy release v1.0.4 to production. 2024-09-11 12:43:17 +02:00
57 changed files with 975 additions and 204 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! 👌
+67
View File
@@ -0,0 +1,67 @@
name: 🐳 Build docker images
on:
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
branches:
- 'main'
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
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:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
target: production
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
notify-argocd:
runs-on: ubuntu-latest
if: |
github.event_name != 'pull_request'
needs:
- build-and-push
steps:
- uses: numerique-gouv/action-argocd-webhook-notification@main
id: notify
with:
deployment_repo_path: "${{ github.repository }}"
argocd_webhook_secret: "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}"
argocd_url: "${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}"
@@ -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
# skip this job on forks since they won't have access to the required secrets
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request'
container:
image: ghcr.io/helmfile/helmfile:v1.2.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
env:
SOPS_AGE_KEY: ${{ secrets.SOPS_PRIVATE }}
run: |
HELMFILE=src/helm/helmfile.yaml.gotmpl
environments=$(< $HELMFILE awk -F'[ :]+' '/^ [^ ]/ {print $2} /^---/ { exit }')
for env in $environments; do
echo "################### $env lint ###################"
helmfile -e $env -f "$HELMFILE" lint || exit 1
echo -e "\n"
done
-93
View File
@@ -1,93 +0,0 @@
name: Docker Hub Workflow
on:
workflow_dispatch:
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
branches:
- 'main'
jobs:
build-and-push:
runs-on: ubuntu-latest
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
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: lasuite/oidc2fer
-
name: Load sops secrets
uses: rouja/actions-sops@main
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
with:
context: .
target: production
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
notify-argocd:
runs-on: ubuntu-latest
if: |
github.event_name != 'pull_request'
needs:
- build-and-push
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
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
-
name: Load sops secrets
uses: rouja/actions-sops@main
with:
secret-file: secrets/numerique-gouv/oidc2fer/secrets.enc.env
age-key: ${{ secrets.SOPS_PRIVATE }}
-
name: Call argocd github webhook
run: |
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}'
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${ARGOCD_WEBHOOK_SECRET}'' | awk '{print "X-Hub-Signature: sha1="$2}')
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" $ARGOCD_WEBHOOK_URL
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "secrets"]
path = secrets
url = ../secrets
url = ../../numerique-gouv/secrets
+27
View File
@@ -6,6 +6,33 @@ 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).
## Unreleased
- add alternate configuration for connection to CNB (#33)
## [1.0.10] - 2025-11-21
- rename helmfile.yaml for compatibility with helmfile 1.x
## [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.0.6] - 2025-03-11
- upgrade to SATOSA 8.5.1
- allow faculty, staff, employee, researcher, teacher affiliations
## [1.0.5] - 2025-03-06
- add allowlist for URL paths to nginx ingress
- allow customizing log levels per logger
## [1.0.4] - 2024-09-11
- serve static files
+16 -14
View File
@@ -1,17 +1,11 @@
# ---- base image to inherit from ----
FROM python:3.11-slim-bookworm as common
FROM python:3.11.12-slim-bookworm AS common
# Install xmlsec1 dependencies required for xmlsec (for SAML)
# Needs to be kept before the `pip install`
RUN apt-get update && \
apt-get -y upgrade && \
apt-get install -y \
pkg-config \
gcc \
xmlsec1 \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl && \
apt-get install -qy --no-install-recommends xmlsec1 && \
rm -rf /var/lib/apt/lists/*
# We want the most up-to-date stable pip release
@@ -19,7 +13,7 @@ RUN pip install --upgrade pip
ENV PYTHONUNBUFFERED=1
# Give the "root" group the same permissions as the "root" user on /etc/passwd
# Give the "root" group the same permissions AS the "root" user on /etc/passwd
# to allow a user belonging to the root group to add new users; typically the
# docker user (see entrypoint).
RUN chmod g=u /etc/passwd
@@ -41,7 +35,10 @@ COPY docker/files/usr/local/etc/gunicorn/satosa.py /usr/local/etc/gunicorn/satos
CMD ["gunicorn", "-c", "/usr/local/etc/gunicorn/satosa.py"]
# ---- Development image ----
FROM common as development
FROM common AS development
# Install curl (for healthchecks)
RUN apt-get update && apt-get install -qy curl
# Playwright browsers
ENV PLAYWRIGHT_BROWSERS_PATH=/pw-browsers
@@ -57,17 +54,22 @@ WORKDIR /app
# Copy project file to list dependencies
COPY ./src/satosa/pyproject.toml /app/
# Install oidc2fer in editable mode along with development dependencies
RUN pip install -e .[dev]
# Create empty module directory to please pip install --editable
# (without this, the oidc2fer module will not be importable because
# pip --editable scans the directory to create its "links")
RUN mkdir -p /app/oidc2fer
# Copy oidc2fer application (see .dockerignore)
# Install oidc2fer in editable mode along with development dependencies
RUN pip install --editable .[dev]
# Copy oidc2fer sources (see .dockerignore)
COPY ./src/satosa /app/
# Switch to unprivileged user
USER ${DOCKER_USER}
# ---- Production image (keep last so it is the default target) ----
FROM common as production
FROM common AS production
# Copy oidc2fer application (see .dockerignore)
COPY ./src/satosa /app/
+8 -1
View File
@@ -57,12 +57,19 @@ 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`.
4. Update the `production` tag and push it.
## Environment variables
| variable | usage |
| --- | --- |
| `LOG_LEVEL` | Sets the log level for the root logger, i.e. the default. Defaults to `INFO`. |
| `LOG_LEVELS` | A JSON object that can be used to set log levels for specific loggers, e.g. `{"satosa.backends.saml2": "DEBUG"}`. Defaults to `{}`. |
## Contributing
This project is intended to be community-driven, so please, do not hesitate to
+1 -1
View File
@@ -15,4 +15,4 @@ docker_build(
watch_file('src/helm')
k8s_yaml(local('cd src/helm && helmfile -n oidc2fer -e dev template .'))
k8s_yaml(local('cd src/helm && helmfile -n oidc2fer -e %s template .' % os.getenv('TILT_ENV', 'dev')))
+10 -7
View File
@@ -13,7 +13,7 @@ services:
BASE_URL: https://satosa.traefik.me
GUNICORN_CMD_ARGS: --workers=2
TEST_E2E: 1
#TEST_E2E_AC: 1
#TEST_E2E_PC: 1
OIDC_DB_URI: redis://redis/0
CLIENT_DB_JSON: |
{
@@ -38,24 +38,27 @@ services:
volumes:
- ./src/satosa:/app
- ./docker/files/usr/local/etc/gunicorn/satosa.py:/usr/local/etc/gunicorn/satosa.py
working_dir: /app/config/fer
depends_on:
- redis
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8000/ping"]
interval: 60s
timeout: 5s
retries: 3
start_period: 30s
start_interval: 1s
oidc-test-client:
build: docker/oidc-test-client
depends_on:
- nginx # nginx must be up to override satosa.traefik.me resolution
- app-dev
stop_signal: SIGKILL
volumes:
- ./docker/oidc-test-client:/app
- ./env.d/development/certs/mkcert-root-ca.pem:/usr/local/share/ca-certificates/mkcert-root-ca.crt
# - run update-ca-certificates to make mkcert certificates valid
# - add startup delay because IDP must be available on boot
entrypoint: |
/bin/bash -c '
cat /usr/local/share/ca-certificates/mkcert-root-ca.crt >> $(python -m certifi)
sleep 1
cd /app
exec python app.py
'
@@ -66,7 +69,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"
+1
View File
@@ -38,6 +38,7 @@ printenv SAML2_BACKEND_CERT > /tmp/backend.crt
printenv SAML2_BACKEND_KEY > /tmp/backend.key
printenv OIDC_FRONTEND_KEY > /tmp/frontend.key
printenv CLIENT_DB_JSON > /tmp/client_db.json
printenv SAML2_IDP_METADATA > /tmp/idp_metadata.xml
# Redis database number must be specified
if [[ ! $OIDC_DB_URI =~ /[0-9]+$ ]]; then
+36 -1
View File
@@ -1,4 +1,5 @@
import datetime
import json
import logging
import os
import sys
@@ -29,6 +30,8 @@ class RequestJSONFormatter(json_log_formatter.JSONFormatter):
url += f"?{record.args['q']}"
return {
"logger": record.name,
"level": record.levelname,
"remote_ip": record.args["h"],
"method": record.args["m"],
"path": url,
@@ -37,7 +40,7 @@ class RequestJSONFormatter(json_log_formatter.JSONFormatter):
"user_agent": record.args["a"],
"referer": record.args["f"],
"duration_in_ms": record.args["M"],
"poppid": record.process,
"pid": record.process,
}
@@ -55,11 +58,23 @@ class DefaultJSONFormatter(json_log_formatter.JSONFormatter):
payload: dict[str, str | int | float] = super().json_record(
message, extra, record
)
payload["logger"] = record.name
payload["level"] = record.levelname
payload["pid"] = record.process
return payload
class NoPingFilter(logging.Filter):
"""
Filters out /ping requests from the access log.
"""
def filter(self, record: logging.LogRecord) -> bool:
return not( record.args.get("m", "") == "GET"
and record.args.get("U", "") == "/ping"
and str(record.args.get("s", "")) == "200" )
bind = ["0.0.0.0:8000"]
name = "satosa"
python_path = "/app"
@@ -76,6 +91,9 @@ accesslog = "-"
errorlog = "-"
loglevel = os.environ.get("LOG_LEVEL", "INFO")
loglevels_json = os.environ.get("LOG_LEVELS", "{}")
loglevels = json.loads(loglevels_json)
logconfig_dict = {
"version": 1,
"disable_existing_loggers": True,
@@ -95,7 +113,24 @@ logconfig_dict = {
"handlers": ["json_request"],
"propagate": False,
"qualname": "gunicorn.access",
"filters": [NoPingFilter()],
},
"satosa.base": {
"level": "INFO",
},
"satosa.state": {
"level": "INFO",
},
"satosa.proxy_server": {
"level": "INFO",
},
"satosa.routing": {
"level": "INFO",
},
"satosa.frontends.ping": {
"level": "INFO",
},
**{k: {"level": v.upper()} for k, v in loglevels.items()},
},
"formatters": {
"json_request": {
+36 -17
View File
@@ -2,13 +2,11 @@ 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
from werkzeug.exceptions import InternalServerError
import secrets
import webbrowser
import threading
import time
import logging
import os
@@ -26,21 +24,27 @@ app.config.update(
}
)
client = Client(client_authn_method=CLIENT_AUTHN_METHOD)
provider_info = client.provider_config(os.environ["OIDC_PROVIDER"])
def create_client() -> Client:
client = Client(client_authn_method=CLIENT_AUTHN_METHOD)
info = {
"client_id": os.environ["OIDC_CLIENT_ID"],
"client_secret": os.environ["OIDC_CLIENT_SECRET"],
"redirect_uris": [f"{os.environ['OIDC_ROOT_URL']}/redirect_uri"],
}
client_reg = RegistrationResponse(**info)
client.store_registration_info(client_reg)
client.provider_config(os.environ["OIDC_PROVIDER"])
info = {
"client_id": os.environ["OIDC_CLIENT_ID"],
"client_secret": os.environ["OIDC_CLIENT_SECRET"],
}
client.store_registration_info(RegistrationResponse(**info))
client.redirect_uris = [f"{os.environ['OIDC_ROOT_URL']}/redirect_uri"]
return client
@app.route("/")
def index():
client = create_client()
session["state"] = rndstr()
session["nonce"] = rndstr()
@@ -50,8 +54,9 @@ def index():
"response_type": "code",
"scope": os.environ["OIDC_SCOPES"].split(","),
"nonce": session["nonce"],
"redirect_uri": client.registration_response["redirect_uris"][0],
"redirect_uri": client.redirect_uris[0],
"state": session["state"],
"claims": ClaimsRequest(id_token=Claims(acr=None, amr=None)),
}
)
login_url = auth_req.request(client.authorization_endpoint)
@@ -61,6 +66,8 @@ def index():
@app.route("/redirect_uri")
def oidc_callback():
client = create_client()
response = request.query_string.decode("utf-8")
aresp = client.parse_response(
AuthorizationResponse, info=response, sformat="urlencoded"
@@ -73,7 +80,6 @@ def oidc_callback():
error_response=aresp.to_dict(),
)
code = aresp["code"]
logging.info("got auth code=%s", code)
@@ -92,7 +98,20 @@ def oidc_callback():
logging.info("got userinfo=%s", userinfo)
return jsonify(
access_token_response=access_token_response.to_dict(), userinfo=userinfo.to_dict()
access_token_response=access_token_response.to_dict(),
userinfo=userinfo.to_dict(),
)
@app.errorhandler(InternalServerError)
def handle_server_exception(e):
exc = e.original_exception
import traceback
return (
f"""<h1>Internal Server Error</h1>
<pre>{"".join(traceback.format_exception(exc))}</pre>""",
500,
)
@@ -100,6 +119,6 @@ def oidc_callback():
def health():
return "OK"
if __name__ == "__main__":
app.run(host="0.0.0.0")
+1 -1
View File
@@ -1,2 +1,2 @@
Flask==3.0.3
oic==1.6.1
oic==1.7.0
+93
View File
@@ -0,0 +1,93 @@
# Principe de fonctionnement de la passerelle ProConnect vers RENATER (`OIDC2FER`)
## Vue d'ensemble
Ce premier schéma donne un aperçu des échanges entre :
* un Fournisseur de Service ProConnecté (ex. https://pad.numerique.gouv.fr) ;
* le service ProConnect (hébergé sur `https://auth.agentconnect.gouv.fr`) ;
* la passerelle `OIDC2FER` (hébergé sur `https://renater.agentconnect.gouv.fr`) ;
* le service de _discovery_ ou _Where Are You From (WAYF)_ RENATER qui permet à l'utilisateur de choisir son établissement de rattachement, hébergé sur https://discovery.renater.fr/agentconnect/ ;
* le serveur d'identité SAML de l'établissement sélectionné (par exemple, https://cas.inria.fr).
``` mermaid
sequenceDiagram
participant FS as Fournisseur<br>de Service
participant ProConnect
box LightYellow
participant OIDC2FER
end
participant WAYF as WAYF<br>RENATER
participant IdP as IdP SAML
FS->>ProConnect: OIDC auth request
ProConnect->>OIDC2FER: OIDC auth request
OIDC2FER->>WAYF: discovery request
WAYF->>OIDC2FER: IdP entityID
OIDC2FER->>IdP: SAML AuthnRequest
IdP->>OIDC2FER: SAML Assertion
OIDC2FER->>ProConnect: OIDC tokens+userinfo
ProConnect->>FS: OIDC tokens+userinfo
```
## Détail des échanges
Noter que le schéma précédent ne reflète pas le détail des échanges entre l'utilisateur, son navigateur (qu'il est utile de distinguer pour illustrer que plusieurs échanges se font sans intervention humaine), et les différents services mentionnés. En voici une version exhaustive :
``` mermaid
sequenceDiagram
actor Utilisateur
participant Navigateur
participant FS as Fournisseur<br>de Service
participant ProConnect
box LightYellow
participant OIDC2FER
end
participant WAYF as WAYF<br>RENATER
participant IdP SAML
Utilisateur->>Navigateur: Saisie/clic URL FS
Navigateur->>FS: Requête d'une page du FS
opt si pas de session FS ouverte
FS->>Utilisateur: Présentation page d'accueil avec bouton ProConnect
Utilisateur->>FS: Clic bouton ProConnect
FS->>Navigateur: Redirection OIDC vers ProConnect avec state, nonce
Navigateur->>ProConnect: Requête GET avec state, nonce
opt si pas de session ProConnect ouverte
ProConnect->>Utilisateur: Présentation mire ProConnect
Utilisateur->>ProConnect: Clic bouton RENATER, ou saisie "robert@univ-exemple.fr"
ProConnect->>Navigateur: Redirection OIDC vers OIDC2FER avec state, nonce
Navigateur->>OIDC2FER: Requête GET avec state, nonce
OIDC2FER->>Navigateur: Redirection vers WAYF
Navigateur->>WAYF: Requête GET
opt si pas de préselection enregistrée dans le WAYF
WAYF->>Utilisateur: Présentation liste d'établissements autorisés
Utilisateur->>WAYF: Choix d'un établissement
end
WAYF->>Navigateur: Redirection vers OIDC2FER avec entityID IdP
Navigateur->>OIDC2FER: Requête GET avec entityID IdP
OIDC2FER->>Navigateur: Redirection vers IdP avec AuthnRequest SAML dans l'URL
Navigateur->>IdP SAML: Requête GET avec AuthnRequest SAML dans l'URL
opt si pas de session IdP ouverte
IdP SAML->>Utilisateur: Présentation mire de connexion IdP
Utilisateur->>IdP SAML: Saisie identifiants de connexion
end
IdP SAML->>Navigateur: Page avec formulaire contenant assertion SAML
Navigateur->>OIDC2FER: Requête POST avec assertion SAML
Note over OIDC2FER: Validation<br>eduPersonAffiliation
Note over OIDC2FER: Conversion attributs<br>SAML vers OIDC
OIDC2FER->>Navigateur: Redirection vers callback OIDC ProConnect avec authorization_code
Navigateur->>ProConnect: Requête GET avec authorization_code
ProConnect->>OIDC2FER: Demande tokens avec authorization_code
OIDC2FER->>ProConnect: Tokens OIDC
ProConnect->>OIDC2FER: Demande userinfo avec accessToken
OIDC2FER->>ProConnect: userinfo
end
ProConnect->>Navigateur: Redirection vers callback OIDC FS avec authorization_code
Navigateur->>FS: Requête GET avec authorization_code
FS->>ProConnect: Demande tokens avec authorization_code
ProConnect->>FS: Tokens OIDC
FS->>ProConnect: Demande userinfo avec accessToken
ProConnect->>FS: userinfo
end
FS->>Utilisateur: Contenu du service
```
+2 -1
View File
@@ -1,2 +1,3 @@
# Python
PYTHONPATH=/app
LOG_LEVEL=DEBUG
LOG_LEVELS='{ "satosa.backends.saml2": "DEBUG" }'
+1 -1
Submodule secrets updated: 647b88689c...4e6030feee
File diff suppressed because one or more lines are too long
@@ -0,0 +1,44 @@
image:
repository: localhost:5001/oidc2fer
pullPolicy: Always
tag: "latest"
satosa:
replicas: 1
workingDir: /app/config/cnb
envVars:
BASE_URL: https://oidc2fer.127.0.0.1.nip.io
GUNICORN_CMD_ARGS: --workers=3
LOG_LEVEL: debug
SAML2_ENTITY_ID: https://oidc2fer.127.0.0.1.nip.io/Saml2/proxy_saml2_backend.xml
SAML2_IDP_METADATA: { secretKeyRef: { name: oidc2fer, key: SAML2_IDP_METADATA } }
STATE_ENCRYPTION_KEY: { secretKeyRef: { name: oidc2fer, key: STATE_ENCRYPTION_KEY } }
SAML2_BACKEND_CERT: { secretKeyRef: { name: oidc2fer, key: SAML2_BACKEND_CERT } }
SAML2_BACKEND_KEY: { secretKeyRef: { name: oidc2fer, key: SAML2_BACKEND_KEY } }
OIDC_FRONTEND_KEY: { secretKeyRef: { name: oidc2fer, key: OIDC_FRONTEND_KEY } }
OIDC_DB_URI: "redis://:pass@redis-master:6379"
CLIENT_DB_JSON: |-
{
"oidc-test-client": {
"response_types": [
"id_token",
"code"
],
"redirect_uris": [
"https://oidc-test-client.traefik.me/redirect_uri",
"https://oidc-test-client.traefik.me:8443/redirect_uri"
],
"client_secret": "oidc-test-secret",
"token_endpoint_auth_method": "client_secret_post"
}
}
ingress:
enabled: true
host: oidc2fer.127.0.0.1.nip.io
annotations:
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
+1
View File
@@ -0,0 +1 @@
../../../../secrets/numerique-gouv/oidc2fer/env/cnb-staging/secrets.enc.yaml
@@ -0,0 +1,34 @@
image:
repository: ghcr.io/proconnect-gouv/oidc2fer
pullPolicy: Always
tag: "main"
satosa:
replicas: 2
workingDir: /app/config/cnb
envVars:
BASE_URL: https://oidc2cnb-staging.beta.numerique.gouv.fr
GUNICORN_CMD_ARGS: --workers=3
LOG_LEVEL: DEBUG
LOG_LEVELS: '{ "satosa.backends.saml2": "DEBUG" }'
SAML2_IDP_METADATA: { secretKeyRef: { name: oidc2fer, key: SAML2_IDP_METADATA } }
SAML2_ENTITY_ID: https://oidc2cnb-staging.beta.numerique.gouv.fr/Saml2/proxy_saml2_backend.xml
STATE_ENCRYPTION_KEY: { secretKeyRef: { name: oidc2fer, key: STATE_ENCRYPTION_KEY } }
SAML2_BACKEND_CERT: { secretKeyRef: { name: oidc2fer, key: SAML2_BACKEND_CERT } }
SAML2_BACKEND_KEY: { secretKeyRef: { name: oidc2fer, key: SAML2_BACKEND_KEY } }
OIDC_FRONTEND_KEY: { secretKeyRef: { name: oidc2fer, key: OIDC_FRONTEND_KEY } }
CLIENT_DB_JSON: { secretKeyRef: { name: oidc2fer, key: CLIENT_DB_JSON } }
OIDC_DB_URI: { secretKeyRef: { name: redis.redis.libre.sh, key: url } }
ingress:
enabled: true
host: oidc2cnb-staging.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
@@ -5,6 +5,7 @@ image:
satosa:
replicas: 1
workingDir: /app/config/fer
envVars:
BASE_URL: https://oidc2fer.127.0.0.1.nip.io
GUNICORN_CMD_ARGS: --workers=3
@@ -36,6 +37,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.3"
tag: "v1.0.10"
satosa:
replicas: 2
@@ -9,7 +9,8 @@ satosa:
BASE_URL: https://renater.agentconnect.gouv.fr
GUNICORN_CMD_ARGS: --workers=3
LOG_LEVEL: info
LOG_LEVEL: INFO
LOG_LEVELS: '{ "satosa.backends.saml2": "DEBUG" }'
SAML2_DISCOVERY_URL: https://discovery.renater.fr/agentconnect/
SAML2_METADATA_URL: https://pub.federation.renater.fr/metadata/renater/main/main-idps-renater-metadata.xml
@@ -23,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": "93810656400017",
"https://idp.univ-tours.fr/idp/shibboleth": "19370800500478",
"https://apps.univ-lr.fr/idp/shibboleth": "19170032700015",
"https://shib.mines-albi.fr/idp/shibboleth": "18009202500097",
"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": "93249157400012",
"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": "18004312700091",
"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": "13003061200019",
"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": "18510200100327",
"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": "13002622200013",
"https://prod-idp.inria.fr": "18008904700013",
"https://idp.ec-nantes.fr/idp/shibboleth": "19440100600011"
}
ingress:
enabled: true
host: renater.agentconnect.gouv.fr
@@ -1,15 +1,17 @@
image:
repository: lasuite/oidc2fer
repository: ghcr.io/proconnect-gouv/oidc2fer
pullPolicy: Always
tag: "main"
tag: "latest"
satosa:
replicas: 2
workingDir: /app/config/fer
envVars:
BASE_URL: https://oidc2fer-staging.beta.numerique.gouv.fr
GUNICORN_CMD_ARGS: --workers=3
LOG_LEVEL: debug
LOG_LEVEL: DEBUG
LOG_LEVELS: '{ "satosa.backends.saml2": "DEBUG" }'
SAML2_DISCOVERY_URL: https://discovery.renater.fr/test/
SAML2_METADATA_URL: https://pub.federation.renater.fr/metadata/test/preview/preview-idps-test-metadata.xml
@@ -23,10 +25,15 @@ 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
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
cert-manager.io/cluster-issuer: letsencrypt
nginx.ingress.kubernetes.io/proxy-buffer-size: 128k
@@ -1,3 +1,30 @@
environments:
dev:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
cnb-dev:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
staging:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
cnb-staging:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
outscale-production:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
---
repositories:
- name: bitnami
url: registry-1.docker.io/bitnamicharts
@@ -5,7 +32,7 @@ repositories:
releases:
- name: redis
installed: {{ eq .Environment.Name "dev" | toYaml }}
installed: {{ or (eq .Environment.Name "dev") (eq .Environment.Name "cnb-dev") | toYaml }}
namespace: {{ .Namespace }}
chart: bitnami/redis
version: 18.19.2
@@ -13,9 +40,11 @@ releases:
- auth:
password: pass
architecture: standalone
image:
repository: bitnamilegacy/redis
- name: extra
installed: {{ ne .Environment.Name "dev" | toYaml }}
installed: {{ and (ne .Environment.Name "dev") (ne .Environment.Name "cnb-dev") | toYaml }}
namespace: {{ .Namespace }}
chart: ./extra
secrets:
@@ -29,21 +58,3 @@ releases:
- env.d/{{ .Environment.Name }}/values.oidc2fer.yaml.gotmpl
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
environments:
dev:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
staging:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
outscale-production:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
+7 -4
View File
@@ -1,5 +1,6 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "oidc2fer.fullname" . -}}
{{- $port := .Values.satosa.service.port -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -33,13 +34,15 @@ spec:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
{{- range .Values.ingress.paths }}
- path: {{ .path | quote }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "oidc2fer.satosa.fullname" . }}
name: {{ $fullName }}-satosa
port:
number: {{ .Values.satosa.service.port }}
number: {{ $port }}
{{- end }}
{{- with .Values.ingress.customBackends }}
{{- toYaml . | nindent 10 }}
{{- end }}
@@ -38,6 +38,9 @@ spec:
command:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.satosa.workingDir }}
workingDir: {{ . | quote }}
{{- end }}
{{- with .Values.satosa.args }}
args:
{{- toYaml . | nindent 12 }}
@@ -82,3 +85,16 @@ spec:
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
---
{{ if .Values.satosa.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace | quote }}
spec:
maxUnavailable: 1
selector:
matchLabels:
{{- include "oidc2fer.common.selectorLabels" (list . $component) | nindent 6 }}
{{ end }}
+1
View File
@@ -5,6 +5,7 @@ metadata:
namespace: {{ .Release.Namespace | quote }}
stringData:
STATE_ENCRYPTION_KEY: {{ .Values.STATE_ENCRYPTION_KEY | quote }}
SAML2_IDP_METADATA: {{ .Values.SAML2_IDP_METADATA | toYaml | indent 8 }}
SAML2_BACKEND_CERT: {{ .Values.SAML2_BACKEND_CERT | toYaml | indent 8 }}
SAML2_BACKEND_KEY: {{ .Values.SAML2_BACKEND_KEY | toYaml | indent 8 }}
OIDC_FRONTEND_KEY: {{ .Values.OIDC_FRONTEND_KEY | toYaml | indent 8 }}
+10 -1
View File
@@ -33,7 +33,9 @@ ingress:
enabled: false
className: null
host: oidc2fer.example.com
path: /
paths:
- path: '/$|^/\.well-known/openid-configuration$|^/images/|^/Saml2/|^/OIDC/|^/ping$'
pathType: ImplementationSpecific # enables regex matching
## @param ingress.hosts Additional host to configure for the Ingress
hosts: []
# - chart-example.local
@@ -48,10 +50,17 @@ ingress:
## @param ingress.customBackends Add custom backends to ingress
customBackends: []
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
## @section satosa
satosa:
## @param satosa.pdb.enabled Enable pdb on backend
pdb:
enabled: true
## @param satosa.command Override the satosa container command
command: []
@@ -0,0 +1,24 @@
attributes:
givenname:
openid:
- given_name
saml:
- cnb_prenom
email:
openid:
- email
saml:
- cnb_email
usual_name:
openid:
- usual_name
saml:
- cnb_nom
acr:
openid:
- acr
uid:
openid:
- uid
saml:
- cnb_id
@@ -0,0 +1,49 @@
module: satosa.backends.saml2.SAMLBackend
name: Saml2
config:
entityid_endpoint: true
mirror_force_authn: false
memorize_idp: false
use_memorized_idp_when_force_authn: false
send_requester_id: false
enable_metadata_reload: false
acs_selection_strategy: prefer_matching_host
sp_config:
name: Passerelle ProConnect vers CNB
description: Passerelle ProConnect vers CNB
key_file: /tmp/backend.key
cert_file: /tmp/backend.crt
organization:
name: DINUM
display_name: DINUM
url: https://beta.gouv.fr/incubateurs/dinum_produits_interministeriels.html
metadata:
local:
- /tmp/idp_metadata.xml
entityid: !ENV SAML2_ENTITY_ID
accepted_time_diff: 60
allow_unknown_attributes: true
service:
sp:
ui_info:
display_name:
- lang: fr
text: Passerelle ProConnect vers CNB
logo:
text: https://beta.gouv.fr/img/incubators/logo_dinum.png
width: '200'
height: '200'
authn_requests_signed: true
want_response_signed: false
want_assertions_signed: false
want_assertions_or_response_signed: true
signing_algorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
allow_unsolicited: true
name_id_format:
- urn:oasis:names:tc:SAML:2.0:nameid-format:transient
endpoints:
assertion_consumer_service:
- - <base_url>/<name>/acs/post
- 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
name_id_format_allow_create: true
@@ -56,8 +56,3 @@ config:
- given_name
usual_name:
- usual_name
extra_id_token_claims:
oidc-test-client:
- acr
agent-connect:
- acr
@@ -0,0 +1,12 @@
module: satosa.micro_services.attribute_processor.AttributeProcessor
name: AttributeProcessor
config:
process:
# Internally SATOSA represents most attributes as lists of strings.
# The SATOSA OIDC frontend automatically flattens standard OIDC attributes
# (like 'given_name' or 'email') to single strings when possible, but
# doesn't do that for custom attributes (like 'usual_name').
- attribute: usual_name
processors:
- module: oidc2fer.attribute_processors.flattening_processor
name: FlatteningProcessor
@@ -0,0 +1,22 @@
module: satosa.micro_services.primary_identifier.PrimaryIdentifier
name: PrimaryIdentifier
config:
# The ordered identifier candidates are searched in order
# to find a candidate primary identifier. The search ends
# when the first candidate is found. The identifier or attribute
# names are the internal SATOSA names for the attributes as
# defined in internal_attributes.yaml.
ordered_identifier_candidates:
- attribute_names: [uid]
# The internal SATOSA attribute into which to place the primary
# identifier value once found from the above configured ordered
# candidates.
primary_identifier: uid
# Whether or not to clear the input attributes after setting the
# primary identifier value.
clear_input_attributes: no
# Whether to replace subject_id with the constructed primary identifier
replace_subject_id: yes
@@ -0,0 +1,7 @@
module: satosa.micro_services.attribute_modifications.AddStaticAttributes
name: AddAttributes
config:
static_attributes:
acr: eidas1
# FIXME temporary hardcoded email for testing
email: ["test@cnb.avocat.fr"] # SATOSA wants this to be an array
+22
View File
@@ -0,0 +1,22 @@
BASE: !ENV BASE_URL
COOKIE_STATE_NAME: SATOSA_STATE
CONTEXT_STATE_DELETE: 'yes'
STATE_ENCRYPTION_KEY: !ENV STATE_ENCRYPTION_KEY
cookies_samesite_compat:
- - SATOSA_STATE
- SATOSA_STATE_LEGACY
INTERNAL_ATTRIBUTES: internal_attributes.yaml
BACKEND_MODULES:
- plugins/backends/saml2_backend.yaml
FRONTEND_MODULES:
- plugins/frontends/openid_connect_frontend.yaml
- plugins/frontends/ping_frontend.yaml
MICRO_SERVICES:
- plugins/microservices/primary_identifier.yaml
- plugins/microservices/static_attributes.yaml
- plugins/microservices/attribute_processor.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
version: 1
incremental: true

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+9
View File
@@ -0,0 +1,9 @@
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="1; url=https://proconnect.gouv.fr/">
</head>
<body>
<h1>Passerelle ProConnect vers CNB. Redirection en cours…</h1>
</body>
</html>
@@ -31,3 +31,6 @@ attributes:
uid:
openid:
- uid
siret:
openid:
- siret
@@ -3,11 +3,11 @@ name: Saml2
config:
disco_srv: !ENV SAML2_DISCOVERY_URL
entityid_endpoint: true
mirror_force_authn: 'no'
memorize_idp: 'no'
use_memorized_idp_when_force_authn: 'no'
send_requester_id: 'no'
enable_metadata_reload: 'no'
mirror_force_authn: false
memorize_idp: false
use_memorized_idp_when_force_authn: false
send_requester_id: false
enable_metadata_reload: false
acs_selection_strategy: prefer_matching_host
sp_config:
name: Passerelle OIDC vers FER
@@ -0,0 +1,61 @@
module: oidc2fer.frontends.jwt_userinfo_openid_connect.JWTUserInfoOpenIDConnectFrontend
name: OIDC
config:
signing_key_path: /tmp/frontend.key
signing_key_id: frontend.key1
# Defines the database connection URI for the databases:
# - authz_code_db
# - access_token_db
# - refresh_token_db
# - sub_db
# - user_db
#
# supported storage backends:
# - In-memory dictionary
# - MongoDB (e.g. mongodb://db.example.com)
# - Redis (e.g. redis://example/0)
# - Stateless (eg. stateless://user:encryptionkey?alg=aes256)
#
# This configuration is optional.
# By default, the in-memory storage is used.
db_uri: !ENV OIDC_DB_URI
# Where to store clients.
#
# If client_db_uri is set, the database connection is used.
# Otherwise, if client_db_path is set, the JSON file is used.
# By default, an in-memory dictionary is used.
# client_db_uri: mongodb://db.example.com
client_db_path: /tmp/client_db.json
# if not specified, it is randomly generated on every startup
sub_hash_salt: randomSALTvalue
sub_mirror_public: yes
provider:
client_registration_supported: no
response_types_supported: ["code", "id_token token"]
subject_types_supported: ["public"]
scopes_supported:
- openid
- email
- uid
- given_name
- usual_name
- siret
# Set code/token lifetimes as short as possible
authorization_code_lifetime: 60
access_token_lifetime: 60
id_token_lifetime: 60
extra_scopes:
uid:
- uid
given_name:
- given_name
usual_name:
- usual_name
siret:
- siret
@@ -0,0 +1,3 @@
module: satosa.frontends.ping.PingFrontend
name: ping
config: null
@@ -13,4 +13,8 @@ config:
# later in the PrimaryIdentifier processor.
- ".+"
eduPersonAffiliation:
- "^faculty$"
- "^staff$"
- "^employee$"
- "^researcher$"
- "^teacher$"
@@ -0,0 +1,5 @@
module: oidc2fer.attribute_generators.EntityIdToSiretMapper
name: EntityIdToSiretMapper
config:
attribute: siret
mapping_json: !ENV SIRET_MAP
@@ -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
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

@@ -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)
+13 -6
View File
@@ -23,18 +23,25 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"SATOSA==8.4.0",
"gunicorn==22.0.0",
"SATOSA==8.5.1",
"gunicorn==23.0.0",
"redis==5.0.4",
"JSON-log-formatter==1.0",
"WhiteNoise==6.7.0",
# Use the most recent pysaml2 that doesn't have the recurrence of
# https://github.com/IdentityPython/pysaml2/issues/819
# (AuthNRequests signed twice)
"pysaml2==7.1.0",
# Pin xmlschema like pysaml2 did in release 7.5.1, see
# https://github.com/IdentityPython/pysaml2/issues/947#issuecomment-1916767026
"xmlschema==2.5.1",
]
[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
+16 -13
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
@@ -75,57 +77,58 @@ def test_oidc_to_renater_student_not_allowed(page: Page):
expect(page.locator("pre")).to_contain_text('"error":"access_denied"')
def agent_connect_login(page: Page, email):
def pro_connect_login(page: Page, email):
page.goto("https://fsa1v2.integ01.dev-agentconnect.fr/")
page.get_by_label("Connexion à AgentConnect").click()
page.get_by_role("button", name="Sidentifier avec ProConnect").click()
page.get_by_label("Email professionnel").fill(email)
page.get_by_test_id("interaction-connection-button").click()
def agent_connect_to_renater(
def pro_connect_to_renater(
page: Page,
login="enseignant1",
expected_email="georges.grospieds@formation.renater.fr",
expected_given_name="Georges",
expected_usual_name="Grospieds",
):
agent_connect_login(page, email=expected_email)
pro_connect_login(page, email=expected_email)
renater_wayf(page)
renater_test_idp(page, login=login)
expect(page.locator("body")).to_contain_text(expected_email)
text = page.inner_text("#json")
text = page.inner_text("#userinfo")
result = json.loads(text)
assert {
"uid": f"{login}@test-renater.fr",
"email": expected_email,
"given_name": expected_given_name,
"usual_name": expected_usual_name,
"siret": "12345678200010",
}.items() <= result.items()
return result
@pytest.mark.skipif(
"TEST_E2E_AC" not in os.environ, reason="Depends on staging deployment"
"TEST_E2E_PC" not in os.environ, reason="Depends on staging deployment"
)
def test_agent_connect_to_renater_keeps_sub(browser: Browser):
def test_pro_connect_to_renater_keeps_sub(browser: Browser):
with browser.new_context().new_page() as page:
result1 = agent_connect_to_renater(page)
result1 = pro_connect_to_renater(page)
with browser.new_context().new_page() as page:
result2 = agent_connect_to_renater(page)
result2 = pro_connect_to_renater(page)
assert result1["sub"] == result2["sub"]
@pytest.mark.skipif(
"TEST_E2E_AC" not in os.environ, reason="Depends on staging deployment"
"TEST_E2E_PC" not in os.environ, reason="Depends on staging deployment"
)
def test_agent_connect_to_renater_student_not_allowed(page: Page):
agent_connect_login(page, email="jean.dupont@formation.renater.fr")
def test_pro_connect_to_renater_student_not_allowed(page: Page):
pro_connect_login(page, email="jean.dupont@formation.renater.fr")
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(