Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 877f2f896b | |||
| 5884e28292 | |||
| b62fae53ec | |||
| 4a4a180ef1 | |||
| 138bc431fd | |||
| fbb190b455 | |||
| 2cb64a42a8 | |||
| 917313bb5a | |||
| 769342494f | |||
| 8ee0bacee0 | |||
| dda5bb918d | |||
| 46c7a7eea7 | |||
| 5a2d3bb35b | |||
| 6dc94af598 | |||
| 44b9879a3b | |||
| 7c3a0fccc9 | |||
| c224e31a66 | |||
| 4892192b85 | |||
| 79aee346a2 | |||
| 4e84ec0f2b | |||
| 3401da37ff | |||
| 1124893498 | |||
| 1557f7ba22 | |||
| 6c344fae05 | |||
| cb8352787d | |||
| 9a33149e9e | |||
| a56ce17b34 | |||
| a8bf8f61b4 | |||
| 7b1571b0ed | |||
| 3ecbf1a5a0 | |||
| 445485ecc5 | |||
| 1bceed1373 | |||
| 850636970c | |||
| 9a1803b3e6 | |||
| c51c95e007 | |||
| 2687868465 | |||
| 38707412b3 | |||
| 8cd89621b2 | |||
| 425365d22a | |||
| 63806bde22 | |||
| 96299e1386 | |||
| bf015c9238 | |||
| 589d447f19 | |||
| ddc09cca50 | |||
| ae2d4a573b | |||
| 5be05fe43e | |||
| f90df5c2c6 | |||
| 6bbcd95069 | |||
| b839c32b50 | |||
| 9b062aa27d | |||
| 4a2ece217b | |||
| a7577ff85e | |||
| 4e81e1e750 | |||
| c27ebfe6ae | |||
| d84f2b32d5 | |||
| 9cb67bfb87 | |||
| ba1774bdbf | |||
| cb8ff5d878 | |||
| a6e01c071e | |||
| 5a0f2da202 | |||
| 3b2d50d657 | |||
| 52d7de305c | |||
| 64a0d4dd3d | |||
| ffbd161e29 | |||
| ba1c14291d | |||
| 4c4ccc88ba | |||
| c1b33453c3 |
@@ -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! 👌
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -1,3 +1,3 @@
|
||||
[submodule "secrets"]
|
||||
path = secrets
|
||||
url = ../secrets
|
||||
url = ../../numerique-gouv/secrets
|
||||
|
||||
@@ -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
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,2 +1,2 @@
|
||||
Flask==3.0.3
|
||||
oic==1.6.1
|
||||
oic==1.7.0
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
-5
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
+5
-5
@@ -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
|
||||
+4
@@ -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)
|
||||
@@ -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
|
||||
@@ -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="S’identifier 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(
|
||||
|
||||
Reference in New Issue
Block a user