Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,23 @@ PUBLIC_MATOMO_CDN_URL="https://stats.data.gouv.fr/"
PUBLIC_MATOMO_SITE_ID="315"

#### FranceConnect login proxy. This should be empty in production.
PUBLIC_FC_PROXY=""
PUBLIC_FC_PROXY_BASE_URL=""

#### FranceConnect AMI variables
FC_AMI_CLIENT_ID="33fe498cc172fe691778912a2967baa650b24f1ae0ebbe47ae552f37b2d25ead"
FC_AMI_CLIENT_SECRET="fake secret for AMI"

#### Pro Connect AMI admin variables
PRO_CONNECT_AGENT_ADMIN_CLIENT_ID="1c68542c1a7a427dd526c78e02293a3d1c101709c08294e691ce28926dd48ee1"
PRO_CONNECT_AGENT_ADMIN_CLIENT_SECRET="fake secret for AMI admin"

#### FranceConnect variables
PUBLIC_FC_BASE_URL="https://fcp-low.sbx.dev-franceconnect.fr"
PUBLIC_FC_LOGOUT_ENDPOINT="/api/v2/session/end"
FC_SCOPE="openid identite_pivot preferred_username email cnaf_enfants cnaf_adresse"

#### Pro Connect variables
#### AMI FI variables
FI_CLIENT_ID="cad76d3d47ff27acfe961b6fb72b4ac035a9d840e3608483135acc7eb5267d48"
FI_CLIENT_SECRET="fake secret for AMI-FI"
FI_IDP_ID="fe64c556-9728-49e2-83cc-a72acc546ced"

#### Pro Connect AMI admin variables
PRO_CONNECT_AGENT_ADMIN_CLIENT_ID="1c68542c1a7a427dd526c78e02293a3d1c101709c08294e691ce28926dd48ee1"
PRO_CONNECT_AGENT_ADMIN_CLIENT_SECRET="fake secret for AMI admin"
PRO_CONNECT_BASE_URL="https://fca.integ01.dev-agentconnect.fr"

#### API Particulier variables
Expand Down
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
DEBUG="true"
DJANGO_SECRET_KEY="django-insecure-08fl(&lb$**45l!h!n$e!n(+)$+#p-gnw-d7$msk^^73xj&#$d"
PUBLIC_MATOMO_ENABLED="false"
FI_HASH_SALT="insecure-salt"
PUBLIC_WEBSITE_PUBLIC="Website is public"
PUBLIC_FEATUREFLAG_REQUESTS_ENABLED="true"
6 changes: 3 additions & 3 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
AUTH_COOKIE_JWT_SECRET: secret
PARTNERS_PSL_SECRET: secret
PUBLIC_WEBSITE_PUBLIC: true
PUBLIC_FC_PROXY: https://ami-fc-proxy-dev.osc-fr1.scalingo.io/
PUBLIC_FC_PROXY_BASE_URL: https://ami-fc-proxy-dev.osc-fr1.scalingo.io
run: make lint-and-format

run-tests:
Expand Down Expand Up @@ -73,7 +73,7 @@ jobs:

- name: Run tests
env:
PUBLIC_FC_PROXY: https://ami-fc-proxy-dev.osc-fr1.scalingo.io/
PUBLIC_FC_PROXY_BASE_URL: https://ami-fc-proxy-dev.osc-fr1.scalingo.io
run: make test-ci

mobile-app-tests:
Expand Down Expand Up @@ -103,5 +103,5 @@ jobs:
AUTH_COOKIE_JWT_SECRET: secret
PARTNERS_PSL_SECRET: secret
PUBLIC_WEBSITE_PUBLIC: true
PUBLIC_FC_PROXY: https://ami-fc-proxy-dev.osc-fr1.scalingo.io/
PUBLIC_FC_PROXY_BASE_URL: https://ami-fc-proxy-dev.osc-fr1.scalingo.io
run: npm test
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ It needs to be set through Scalingo in the staging and review apps,
and for local development, it needs to be set in the `.env.local` file.

```
PUBLIC_FC_PROXY="https://ami-fc-proxy-dev.osc-fr1.scalingo.io/"
PUBLIC_FC_PROXY_BASE_URL="https://ami-fc-proxy-dev.osc-fr1.scalingo.io"
```

## agent-admin space ("Espace Partenaire AMI")
Expand Down
2 changes: 2 additions & 0 deletions ami/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from ami.agenda import api_urls as agenda_api_urls
from ami.authentication import api_urls as authentication_api_urls
from ami.fi import api_urls as fi_api_urls
from ami.followup import api_urls as followup_api_urls
from ami.notification import api_urls as notification_api_urls
from ami.partner import api_urls as partner_api_urls
Expand All @@ -12,6 +13,7 @@

urlpatterns = [
path("", include(authentication_api_urls)),
path("api/v1/fi/", include(fi_api_urls)),
path("", include(util_api_urls)),
path("", include(notification_api_urls.root_urlpatterns)),
path("api/v1/", include(agenda_api_urls.root_urlpatterns)),
Expand Down
1 change: 1 addition & 0 deletions ami/authentication/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ def logout(request):
RevokedAuthToken.objects.create(jti=request.ami_payload["jti"])
response = Response({}, status=201)
response.delete_cookie(settings.AUTH_COOKIE_JWT_NAME)
response.delete_cookie(settings.USERINFO_COOKIE_JWT_NAME)
return response
4 changes: 3 additions & 1 deletion ami/authentication/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ async def get_fc_token(
await nonce.adelete()

# FC - Step 5
redirect_uri: str = settings.PUBLIC_FC_PROXY or settings.FC_AMI_REDIRECT_URL
redirect_uri: str = settings.FC_AMI_REDIRECT_URL
if settings.PUBLIC_FC_PROXY_BASE_URL:
redirect_uri = settings.PUBLIC_FC_PROXY_BASE_URL + "/"
client_id: str = settings.FC_AMI_CLIENT_ID
data: dict[str, str] = {
"grant_type": "authorization_code",
Expand Down
19 changes: 0 additions & 19 deletions ami/authentication/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,3 @@ def decoded_id_token() -> dict[str, Any]:
"iat": 1763455959,
"iss": "https://fcp-low.sbx.dev-franceconnect.fr/api/v2",
}


@pytest.fixture
def userinfo() -> dict[str, Any]:
return {
"sub": "fake sub",
"given_name": "Angela Claire Louise",
"given_name_array": ["Angela", "Claire", "Louise"],
"family_name": "DUBOIS",
"birthdate": "1962-08-24",
"birthcountry": "99100",
"birthplace": "75107",
"gender": "female",
"email": "angela@dubois.fr",
"aud": "fake aud",
"exp": 1753877658,
"iat": 1753877598,
"iss": "https://fcp-low.sbx.dev-franceconnect.fr/api/v2",
}
4 changes: 2 additions & 2 deletions ami/authentication/tests/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ def test_login_france_connect(
app,
monkeypatch: pytest.MonkeyPatch,
) -> None:
settings.PUBLIC_FC_PROXY_BASE_URL = "https://ami-fc-proxy-dev.osc-fr1.scalingo.io"
FAKE_NONCE = "some-random-nonce"
monkeypatch.setattr("ami.authentication.views.generate_nonce", lambda: FAKE_NONCE)
response = app.get("/login-france-connect")
redirected_url = response.headers["location"]
assert Nonce.objects.count() == 1
nonce = Nonce.objects.get()
assert nonce.nonce == FAKE_NONCE
Expand All @@ -29,7 +29,7 @@ def test_login_france_connect(
)
assert url_contains_param(
"redirect_uri",
settings.PUBLIC_FC_PROXY or settings.FC_AMI_REDIRECT_URL,
f"{settings.PUBLIC_FC_PROXY_BASE_URL}/",
redirected_url,
)
assert url_contains_param("response_type", "code", redirected_url)
Expand Down
5 changes: 5 additions & 0 deletions ami/authentication/tests/test_login_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ def fake_jwt_decode(*args: Any, **params: Any):
assert token
assert token["jti"] is not None

assert (
response.client.cookies[settings.USERINFO_COOKIE_JWT_NAME].value
== '"fake userinfo jwt token"'
)

assert Nonce.objects.count() == 0

assert User.objects.count() == 1
Expand Down
1 change: 1 addition & 0 deletions ami/authentication/tests/test_logout.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def test_logout(
response = app.post("/logout")
assert response.status_code == 201
assert not response.client.cookies.get(settings.AUTH_COOKIE_JWT_NAME)
assert not response.client.cookies.get(settings.USERINFO_COOKIE_JWT_NAME)
assert RevokedAuthToken.objects.count() == 1
revoked_auth_token = RevokedAuthToken.objects.get()
assert revoked_auth_token.jti == token["jti"]
Expand Down
1 change: 1 addition & 0 deletions ami/authentication/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@

urlpatterns = [
path("login-france-connect", views.login_france_connect),
path("login-ami-fi", views.login_ami_fi),
path("login-callback", views.login_callback),
]
63 changes: 57 additions & 6 deletions ami/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,18 @@ def login_france_connect(request):
NONCE = generate_nonce()
nonce = Nonce.objects.create(nonce=NONCE)

redirect_uri: str = settings.FC_AMI_REDIRECT_URL
if settings.PUBLIC_FC_PROXY_BASE_URL:
redirect_uri = f"{settings.PUBLIC_FC_PROXY_BASE_URL}/"
state: str = str(nonce.id)
if settings.PUBLIC_FC_PROXY_BASE_URL:
state = f"{settings.FC_AMI_REDIRECT_URL}?state={nonce.id}"
params = {
"scope": settings.FC_SCOPE,
"redirect_uri": settings.PUBLIC_FC_PROXY or settings.FC_AMI_REDIRECT_URL,
"redirect_uri": redirect_uri,
"response_type": "code",
"client_id": settings.FC_AMI_CLIENT_ID,
"state": (
f"{settings.FC_AMI_REDIRECT_URL}?state={nonce.id}"
if settings.PUBLIC_FC_PROXY
else str(nonce.id)
),
"state": state,
"nonce": NONCE,
"acr_values": "eidas1",
"prompt": "login",
Expand All @@ -57,6 +59,47 @@ def login_france_connect(request):
return redirect(f"{settings.PUBLIC_APP_URL}/#/technical-error")


@require_GET
def login_ami_fi(request):
try:
NONCE = generate_nonce()
nonce = Nonce.objects.create(nonce=NONCE)

redirect_uri: str = settings.FC_AMI_REDIRECT_URL
if settings.PUBLIC_FC_PROXY_BASE_URL:
redirect_uri = f"{settings.PUBLIC_FC_PROXY_BASE_URL}/"
state: str = str(nonce.id)
if settings.PUBLIC_FC_PROXY_BASE_URL:
state = f"{settings.FC_AMI_REDIRECT_URL}?state={nonce.id}"
params = {
"scope": settings.FC_SCOPE,
"redirect_uri": redirect_uri,
"response_type": "code",
"client_id": settings.FC_AMI_CLIENT_ID,
"state": state,
"nonce": NONCE,
"acr_values": "eidas1",
"prompt": "login",
"idp_hint": settings.FI_IDP_ID,
}

login_url = (
f"{settings.PUBLIC_FC_BASE_URL}{settings.FC_AUTHORIZATION_ENDPOINT}?{urlencode(params)}"
)
if settings.PUBLIC_FC_PROXY_BASE_URL:
params = {
"from_url": f"{settings.PUBLIC_API_URL}/",
"fc_url": login_url,
}
login_url = (
f"{settings.PUBLIC_FC_PROXY_BASE_URL}/ami-fi-authorize-request/?{urlencode(params)}"
)
return redirect(login_url)
except Exception as e:
logging.exception(e)
return redirect(f"{settings.PUBLIC_APP_URL}/#/technical-error")


@require_GET
async def login_callback(request):
try:
Expand Down Expand Up @@ -138,6 +181,14 @@ async def login_callback(request):
httponly=True,
samesite="None",
)
response.set_cookie(
key=settings.USERINFO_COOKIE_JWT_NAME,
value=userinfo_result["user_data"],
max_age=365 * 10 * 24 * 3600,
secure=True,
httponly=True,
samesite="None",
)
return response

except FCError as e:
Expand Down
19 changes: 19 additions & 0 deletions ami/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,25 @@ def otv_cert_keys_for_signature() -> Dict[str, str]:
}


@pytest.fixture
def userinfo() -> dict[str, Any]:
return {
"sub": "fake sub",
"given_name": "Angela Claire Louise",
"given_name_array": ["Angela", "Claire", "Louise"],
"family_name": "DUBOIS",
"birthdate": "1962-08-24",
"birthcountry": "99100",
"birthplace": "75107",
"gender": "female",
"email": "angela@dubois.fr",
"aud": "fake aud",
"exp": 1753877658,
"iat": 1753877598,
"iss": "https://fcp-low.sbx.dev-franceconnect.fr/api/v2",
}


@pytest.fixture
def user() -> User:
fc_hash = build_fc_hash(
Expand Down
Empty file added ami/fi/__init__.py
Empty file.
31 changes: 31 additions & 0 deletions ami/fi/api_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from rest_framework.exceptions import APIException


class MissingCookie(APIException):
status_code = 403
default_detail = "Cookie manquant"
default_code = "missing-cookie"


class MissingAuthHeader(APIException):
status_code = 403
default_detail = "Header d'authentification manquant"
default_code = "missing-auth-header"


class WrongFormatAuthHeader(APIException):
status_code = 403
default_detail = "Header d'authentification mal formé"
default_code = "wrong-format-auth-header"


class FISessionExpired(APIException):
status_code = 403
default_detail = "Session de connexion à AMI-FI expirée"
default_code = "fi-session-expired"


class FISessionNotFound(APIException):
status_code = 403
default_detail = "Session de connexion à AMI-FI non trouvée"
default_code = "fi-session-not-found"
10 changes: 10 additions & 0 deletions ami/fi/api_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.urls import path

from ami.fi import api_views

urlpatterns = [
path("authorize/", api_views.authorize),
path("token/", api_views.token),
path("userinfo/", api_views.userinfo),
path("logout/", api_views.logout),
]
Loading
Loading