From 4567948fa22c871fbafec9929e3aca350ec91341 Mon Sep 17 00:00:00 2001 From: gabdevele Date: Thu, 18 Jun 2026 13:58:15 +0200 Subject: [PATCH] feat: update README and JSON files for Visione integration, enhance error handling, and add new templates for user consent and login --- README.md | 90 +---- federation_authority/dumps/example.json | 14 +- provider/dumps/example.json | 10 +- .../__pycache__/settingslocal.cpython-310.pyc | Bin 2087 -> 0 bytes provider/provider/settings.py | 2 +- provider/templates/op_base.html | 253 +++++++++++++ provider/templates/op_user_consent.html | 68 ++++ provider/templates/op_user_login.html | 65 ++++ relying_party/dumps/example.json | 4 +- .../__pycache__/settingslocal.cpython-310.pyc | Bin 2233 -> 0 bytes .../__pycache__/visione_views.cpython-310.pyc | Bin 3146 -> 0 bytes relying_party/relying_party/rp_overrides.py | 337 ++++++++++++++++++ relying_party/relying_party/settings.py | 2 +- relying_party/relying_party/urls.py | 5 + relying_party/relying_party/visione_views.py | 14 + relying_party/templates/rp_error.html | 16 + 16 files changed, 790 insertions(+), 90 deletions(-) delete mode 100644 provider/provider/__pycache__/settingslocal.cpython-310.pyc create mode 100644 provider/templates/op_base.html create mode 100644 provider/templates/op_user_consent.html create mode 100644 provider/templates/op_user_login.html delete mode 100644 relying_party/relying_party/__pycache__/settingslocal.cpython-310.pyc delete mode 100644 relying_party/relying_party/__pycache__/visione_views.cpython-310.pyc create mode 100644 relying_party/relying_party/rp_overrides.py create mode 100644 relying_party/templates/rp_error.html diff --git a/README.md b/README.md index 37d6d52..1361f9c 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,22 @@ # Visione CIE Auth Stack -This folder contains deployable CIE auth services for Visione: +Fork di [italia/spid-cie-oidc-django](https://github.com/italia/spid-cie-oidc-django) riadattato leggermente al progetto Visione. + +## Setup rapido + +1. Copia `.env.example` in `.env`. +2. Imposta domini pubblici, secret Django, `DJANGO_RP_BACKEND_EXCHANGE_URL` e `DJANGO_RP_SHARED_SECRET`. +3. Avvia tutto con `docker compose up --build -d`. + +Stack: - `federation-authority` - `provider` - `relying-party` -It is now structured to work with Docker Compose and Dokploy without host networking. +Il RP Visione usa `/visione/start` per iniziare il flow e `/visione/complete` per chiuderlo verso backend Visione. -## Configuration -Copy [.env.example](/workspace/cie-auth/.env.example) to `.env` and set only what changes in real deploy: -- public domains / internal URLs -- Django secret keys -- shared secret between relying party and Visione backend +## Note -Main variables: -- `CIE_AUTH_ALLOWED_HOSTS` -- `DJANGO_TRUST_ANCHOR` -- `DJANGO_PROVIDER_ISSUER` -- `DJANGO_RP_BACKEND_EXCHANGE_URL` -- `DJANGO_RP_SHARED_SECRET` -- `FEDERATION_AUTHORITY_SECRET_KEY` -- `PROVIDER_SECRET_KEY` -- `RELYING_PARTY_SECRET_KEY` - -## Deploy with Dokploy -Use [docker-compose.yml](/workspace/cie-auth/docker-compose.yml) as compose source. -Expose: -- `federation-authority` on port `8000` -- `provider` on port `8002` -- `relying-party` on port `8001` - -Each service persists its own SQLite DB in a named volume. - -## Local start - -```bash -docker compose up --build -d -``` - -## Visione backend -Visione backend must expose exchange endpoint configured in `DJANGO_RP_BACKEND_EXCHANGE_URL`. -Shared secret must match backend `cie_auth.shared_secret`. - -## Bootstrap behavior -On first boot, each Django service: -1. runs migrations -2. loads `dumps/example.json` only if DB is empty - -Disable fixture import per service with: -- `FEDERATION_AUTHORITY_LOAD_FIXTURE_ON_EMPTY_DB=false` -- `PROVIDER_LOAD_FIXTURE_ON_EMPTY_DB=false` -- `RELYING_PARTY_LOAD_FIXTURE_ON_EMPTY_DB=false` - -## Smoke test - -```bash -curl -sS -X POST http://127.0.0.1:5555/api/v1/auth/cie/start \ - -H 'Content-Type: application/json' \ - -d '{"frontend_redirect_uri":"http://localhost:3000/auth/cie/callback"}' -``` - -Then complete login and poll: - -```bash -curl -sS http://127.0.0.1:5555/api/v1/auth/cie/session/ -``` - -## Reset data - -```bash -docker compose down -v -``` - -If the stack was started before June 17, 2026 DB path fix, also remove legacy SQLite files that were created outside named volumes: - -```bash -rm -f federation_authority/db.sqlite3 provider/db.sqlite3 relying_party/db.sqlite3 -``` - -## Logs -```bash -docker compose logs -f -``` +- Il RP si chiama `Visione`. +- Rifiuto / annullo tornano sempre a Visione. +- Grafica provider allineata a schermata CIE, senza QR e senza lettura carta. diff --git a/federation_authority/dumps/example.json b/federation_authority/dumps/example.json index d78012f..99b8637 100644 --- a/federation_authority/dumps/example.json +++ b/federation_authority/dumps/example.json @@ -257,7 +257,7 @@ ] }, "openid_relying_party": { - "organization_name": "that fancy RP", + "organization_name": "Visione", "application_type": "web", "client_id": "http://127.0.0.1:8000/oidc/rp", "client_registration_types": [ @@ -285,7 +285,7 @@ }, "jwks_uri": "http://127.0.0.1:8000/oidc/rp/openid_relying_party/jwks.json", "signed_jwks_uri": "http://127.0.0.1:8000/oidc/rp/openid_relying_party/jwks.jose", - "client_name": "Name of an example organization", + "client_name": "Visione", "contacts": [ "ops@rp.example.it" ], @@ -1461,7 +1461,7 @@ } ] }, - "client_name": "Name of an example organization", + "client_name": "Visione", "contacts": [ "ops@rp.example.it" ], @@ -1475,7 +1475,8 @@ "response_types": [ "code" ], - "subject_type": "pairwise" + "subject_type": "pairwise", + "organization_name": "Visione" } }, "trust_marks": [], @@ -1726,7 +1727,7 @@ } ] }, - "client_name": "Name of an example organization", + "client_name": "Visione", "contacts": [ "ops@rp.example.it" ], @@ -1740,7 +1741,8 @@ "response_types": [ "code" ], - "subject_type": "pairwise" + "subject_type": "pairwise", + "organization_name": "Visione" } }, "trust_marks": [], diff --git a/provider/dumps/example.json b/provider/dumps/example.json index c13bc4b..631df73 100644 --- a/provider/dumps/example.json +++ b/provider/dumps/example.json @@ -670,7 +670,7 @@ } ] }, - "client_name": "Name of an example organization", + "client_name": "Visione", "contacts": [ "ops@rp.example.it" ], @@ -684,7 +684,8 @@ "response_types": [ "code" ], - "subject_type": "pairwise" + "subject_type": "pairwise", + "organization_name": "Visione" } }, "trust_marks": [], @@ -744,7 +745,7 @@ } ] }, - "client_name": "Name of an example organization", + "client_name": "Visione", "contacts": [ "ops@rp.example.it" ], @@ -758,7 +759,8 @@ "response_types": [ "code" ], - "subject_type": "pairwise" + "subject_type": "pairwise", + "organization_name": "Visione" } }, "trust_marks": [], diff --git a/provider/provider/__pycache__/settingslocal.cpython-310.pyc b/provider/provider/__pycache__/settingslocal.cpython-310.pyc deleted file mode 100644 index 0ab14c69cdab10168e79184bb041334aa165f0f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2087 zcmZuyTXWM!6qaOJz9dcpxsnUF5Q=GIC*h$C)9D~=#c{FaRFY}Y%Z}pRC`x6^)vhz3 z=|lfO|3Mx~XZp~;w6A?Bk9}nd=~+7l60m0FqwkzO=R22`qCz1r!EgP~o#S6}lJvKD z`1l8y^cWVsElU!XupCP6=wEhaA|ETir#`x?B>hSjm#bmL&ES-q#j2abX*Z8GcY;j# zvO9@0ZUJZADPMA@agIy`GR_b06Q_zhgC}tckOGm8uiz;>{hJI=GoZPOueh_Y=3vd^ ztKew?&$^4ggy;T{-6cHlUc(D`(Fc2X886{$LtZOH!^^J}ymG1lzKU1fHN56t2WB^5 z-Gp@uUw3cg8{lsp-^6Lyc?;hTMAUVV@8Au5_f#-i_htMA=z3?o`Q|R%# z8nD+YKt-9SS;V{$4D+3+*CQPUym^KaDiTO$#-7Td*TX^2=konzMYu%35D6B@>vW<) zkI_<_lDIgTEL>2R#_D7aAA3DNDnT3;2M48L36j~%=ztw1E0;jAh4dJE*(Wqvyrewm z`l+G|Nq9c&SiZc2c`WE01|gyR+CdaC%3`mN0w_e_Ax66W&|@T-yO;pm&NC3*_eq?rpVJrzD20&+b3seQ_i3pyeBe2+-$y+Uxbwn=NZ^MYdV`R? zPX`~A4!q7!q=#vV{u~C3JV^2Yp3Bfv%`j_q)8MM9H;p76u+4xa6UHEp`lM_{T@w8B z7N$VqnP;Pcg?5mW-XIJglNTP8fk3yqui@}ZTsw^7uE&xG8}~d4W6~u>dUNBRpjzDA z-Vt(0PDnTc9*XDl1{5jJKnH0Q5}vLaPujI)K8$=H#;Ihpn0Vbi7_Jb8C&hE4D2&8B zJ#@#@ozSC{rwz+$SzwVp8X41gNzlqco%@~-{o-of+--@hM|Fu)~ydV;1kSh`8YTsi=(1{=xX@c&64v{QE zVw@1-*(bVfplaRXdE2O1hJ&6O&*KH*G_O~ibrbFD&K{rC_xBC6itL7N@A1`Ey;|8d zs;Fx0>g|Su9II_RNH;5cEsM{OwI8E4UmC;vR_j^4YFG$_-Fm~|cR$fI^&e5CW!i=b zzUxh6*Mfog5Y{rjZ`Un|KZ?-Qt*16$9(w`brc-yG3;)AFHZN4d0M3ImdYACYJ;&Lv zKxW|9=2sgnm^uXhz-PrlZL7g&0c*9Li(qbKDD(xr(P;eu4E9>KWAkaf?d+kp4M{ay zRinYD>Za|04Ol?sY(C={&HaY%7^u?Itkh>TwkGc&@5Dx@Zcp zZs=yMt=9~wMb+RrQ3>R>;DBTy6sm7OG)Gcnd&}gSz#Nya2?waXF9IP^NVDC10&#Zh zMx$zTZP-=it_lS@Ij!2iBY7(k3*^dl4rl^ zMtBgCZ^fyjzrrmmW#v^xRaAL+Dca~?R#oLpEbTunKP7AL)U?oRV3$>t8Nk!(nUYrI rNm+fTo~fDh{j#cPijq=f1uT^_xHORAQs08V+(=S~nEF + + + + + {% block page_title %}Visione CIE{% endblock page_title %} + + {% block extra_head %}{% endblock extra_head %} + + +
+
+
+
+ +
+ {% trans "Ministero dell'interno" %} + {% trans "Accesso CIE" %} +
+
+
+ +
+ {% trans "Relying party" %} + Visione +
+
+
+
+
+ {% block container %}{% endblock container %} +
+
+ + diff --git a/provider/templates/op_user_consent.html b/provider/templates/op_user_consent.html new file mode 100644 index 0000000..023231d --- /dev/null +++ b/provider/templates/op_user_consent.html @@ -0,0 +1,68 @@ +{% extends 'op_base.html' %} +{% load i18n %} + +{% block page_title %}Visione CIE Consent{% endblock page_title %} + +{% block container %} +

{% trans "Richiesta d'accesso CIE livello 2" %}

+

{{ client_organization_name|default:"Visione" }}

+ +
+
+
+

{% trans "Conferma rilascio dati" %}

+

+ {% blocktrans with service=client_organization_name|default:"Visione" %} + Per completare accesso a {{ service }} devi autorizzare rilascio degli attributi richiesti. + {% endblocktrans %} +

+
+ {% csrf_token %} + {% for form_field in form.visible_fields %} +
+ + {% if form_field.field.widget.input_type %} + + {% else %} + + {% endif %} + {% if form_field.errors %} +
{{ form_field.errors }}
+ {% endif %} +
+ {% endfor %} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} +
+ + {% trans "Annulla" %} + + +
+
+
+ +
+
+{% endblock container %} diff --git a/provider/templates/op_user_login.html b/provider/templates/op_user_login.html new file mode 100644 index 0000000..b148c3e --- /dev/null +++ b/provider/templates/op_user_login.html @@ -0,0 +1,65 @@ +{% extends 'op_base.html' %} +{% load i18n %} + +{% block page_title %}Visione CIE Login{% endblock page_title %} + +{% block container %} +

{% trans "Richiesta d'accesso CIE livello 2" %}

+

{{ client_organization_name|default:"Visione" }}

+ +
+
+
+

{% trans "Entra con le tue credenziali CIE" %}

+

+ {% blocktrans with acr=acr_value|upper %} + Autenticazione richiesta con livello {{ acr }}. Inserisci le credenziali associate alla tua identità digitale. + {% endblocktrans %} +

+
+ {% csrf_token %} + {% for form_field in form.visible_fields %} +
+ + {% if form_field.field.widget.input_type %} + + {% endif %} + {% if form_field.errors %} +
{{ form_field.errors }}
+ {% endif %} +
+ {% endfor %} + {% for hidden in hidden_form.hidden_fields %} + {{ hidden }} + {% endfor %} +
+ + {% trans "Annulla" %} + + +
+
+
+ +
+
+{% endblock container %} diff --git a/relying_party/dumps/example.json b/relying_party/dumps/example.json index 671820d..781362d 100644 --- a/relying_party/dumps/example.json +++ b/relying_party/dumps/example.json @@ -87,7 +87,7 @@ }, "openid_relying_party": { "application_type": "web", - "organization_name": "Example RP", + "organization_name": "Visione", "client_id": "http://127.0.0.1:8001", "client_registration_types": [ "automatic" @@ -114,7 +114,7 @@ } ] }, - "client_name": "Name of an example organization", + "client_name": "Visione", "contacts": [ "ops@rp.example.it" ], diff --git a/relying_party/relying_party/__pycache__/settingslocal.cpython-310.pyc b/relying_party/relying_party/__pycache__/settingslocal.cpython-310.pyc deleted file mode 100644 index 28d2c5dfe1eb11bfc7bad8174af2f0655bdd76e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2233 zcmZuyOLN;c5EdzldiW71e#VdH=_E=k%66vHOxo#$mS~HO9xh3x2`?C0fN8pv$P%qx zFW^~w5zm3gB|L9mbu)P355Zo>i}ng$!dG3ew^#8pUK#RRBQjonDdM#=5#(RsFYIgh zntdG@-hh4+`Yn9jzKw5yzjb^Q=fLt7zU^@q>oC8A@8WxBoZ-4F;49^7+m4hJqkc=n`l$dZoS>c=!*dQJza(cbsco0*i zOG#9oOcusqWdtX4_z3(53Iq~I-o7%-Y%+UM3Vf243qo!}2622E5}GVt0MC>0DbR;3 zIWAZ%U$lsw*y|m5KB4UDeh|boj+_vAkZI3BF@aP%kjlC7K!AW3pC*grQl7sc;=p)X zl!*AJkbV?8QGCksK*4oMl-xS!GTKG)0|%3cD#UfE(i|>W(FsE|a3BCSHMSl2AqUQ} zAHRkxcy{or1CbiX;mgKy+;>F2?b{qvEsTx6_ zc>lbCS`%6RX?hLdOvuUL*!Lfk7Y;8gII{jLcv><{hKlGraq?jOop|?jY)cYr^BO8^Bhg{dz54O6Hw-N zX9;`M+4l(|FFYFa1L7w z+tKQ%uI;GZriCn{Yg$Nc*BTvz&5zKJrRE1CL;JC-8(^K{E!B8pvgMJHUf0@|ZaqhP zM&~Ik43kaOd=E<6O5Ync*|4m=8t^jImdUO(J5VzS{DIG!g}O$Q&4R4cwZ?_TRFUJW zPjyr8v^BJ?)}HXNv}d)3+TPWM*7GCOY^a=OnisY-5|4r&5oC*>L3Z?}#-`L}v-1tn{Ee+Wa9V@_ANZ)+ePG`-{jW(0H%(B@U zcYwNkd?VCSpP_cQwGI31=vuRGGI@CRCd)(8AicYPWg;DFVmQ41)*Bo|4u6U(!#8>C z{iu$wD2aLL(%~PK5}vZccl`h#`{a9mOX;sL6fy;2Rg^?Y7&cMnUrBt&yZoPAnik}@ zQZB_(9T{{BqBsM1PI@QiL}5~p-b(MJe2T*;%A%MR1rapGchF>*LzCWs-(otGhBE1m I^t$x#|IiP<=Kufz diff --git a/relying_party/relying_party/__pycache__/visione_views.cpython-310.pyc b/relying_party/relying_party/__pycache__/visione_views.cpython-310.pyc deleted file mode 100644 index a706e09dcdd9514c21b604c836321e876d967b3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3146 zcmahLU2ojRkxTMO9*=jH^$FAT*lis7Vi|GIhNL*@y%5yxp2Q1} zq?db=b)bp@&SUcnk^&NnKBO;w=^yCN=)U%;&jAX=Xq(O~Nn2Hvmc$NchBLc6A3L*2 zv*~K^{PFiUCm%O8?Qck&eM}(Ufsy?d0M#gov|;fjL!xMXsH?9rG}X7JzItR$?V(Lj z)`*;GeORBmLwDL3HWbf{ylHdTB-(wA)~NM~MlEJeT9Zr9jo||DZ0Z2dVNIYf16`*s z(5|8vDcROKjlaW=v_XeRCu2f{@m|(3eY&R8bo@>%{CqakM_ zAy?0Gw;4ZRyw7>UVN&x+$PP!i#Mws&;vE=YE(2g1x&X{4RR2U98q|PonR3w|B-IL= zK};jYM>dF858?#I28;~-OEu6VgD

EzOKYV`*^qRO;hYnxlj=X-u;{Y45XuGM-5* zW`{E#@j9p~@Z#+-9`m3#3fX!v6Z<{Rq9gE!p9WkUoudxIEKFkNAAosTH$9RIy!3_- zi=wX}4YUjmHOc&qZ`#s5FFydA9p8_GDf4~l`u;Sbvj}m|_n*#!sQAMdtNp&(IlqiN z2Y?6TOLT^#c|>u()%P472x~4pDNNZGV5W)|Zn}CbKx#0Qm*;@wWTKrAN>0hKUgA37 zMu{6jpBSh5v02h4&?eBeT<7iF$leq+T6+$Om!q~tt=n3Tx)!woLoJIsw>4qYdTxFO zshZb5(|%{7MyD~5wM+zp$(1orV!>hx&MtD$pYc#SDNhbUh_18&8;22_-}q&iWe~)+ zQfw!@eX(k@3TUywcPjE?>DB6YwV)5H%ov}ydr3$~J)ZV3gAyLj0|9yRi%Ra!yndtH z+MSLgjr;u_Y3wn<(dY8&lLy-m9u4~bz5aW5KX|y~Ki+!uWMSNxRHLeibNw4Z(E;uOWDu^hN5lfrojOp{z$dvH-6GudBPnCA!`s z=GXJ(^O@3xoe#b+`4VVxJj#3pK^wuV6%?j2bQOLm>CN% zf?3dBm7sryf>0(N2{lN&3sV%*)y#EBV)K-wYYM9Y4^k9(7VvH;%m$v78@!jBFf16g z>@A^FN2RX;$!g_=4ph}sUFkGlVjg0&`MG^;3u9vDHf_;MpX$essGV5Co;W9Ux^PPJ z@+;g)5W zY<0+_Ra5~ukF(tgHpWJ|5!HU&amX`K=+&d3M0pp?>*z1Y4_=U4&@kPMLB9z^!n1j8 zCdTVOn=b@u8igZONqUnki97n-+fW5itnVD9Y`*l-dTGr1cDNS@V#b*?A3xgOfnILX z080BM^rAIoP?cz>7Ould3A+e-2;M^QV*t{5m=r6`Z&v=<4MrbBt!;A#^TrI3qUg2{ zLzr$R+EJzR`gtApY_uQ5d#pVMKQVf%J@0(02n}gFPQ=UUJ8S$6On^25Rp?B#Ue@Sr znr716p9UjoKpQWr=So#0w>R%@_3!!H{f#XsuNI4y`|bT8+mFIsX=VGt&7b^K*7w;n z8t&n>rZ!7c=o%K>2S9?qiN3O{JI`7*@4I(5-tQ0Y`TdVJHt!DZ_x%sH9!hU_7Dm)h zQWi6=I<7R4kToC^F6)~+JCD_Eh~tew#X;3+j%`)e`_D!!#U9aVsk&V(?w&aWs($xS z_eThRh-!8k97Rb$rNe_me>b5=vYu6u=M5CGiY-Y4LLi+gLTBo-s>E#&MRT)V04=Ee z9%<=1RQqKEJAiKJ1Yhg|F0sC9)lK+1#3cH^98>?NWtk|28voW!>npFW>mG3$i~n$8 zCX+0~yGESfw8(XU`q%T-GWuuhxoRW)d$55syL;6&Zom(f2dZlz)mh?qQTh&ow*g46 zh+J3socEE{Dp_S4lnpw8L`=G)1a5h%-Bui|B`Uwp#KkM6)%}j+EvN|05{7fD%>VLQ os-mvi8OPmSWQ25X7uUI;!x_^uw8YV{!Q$)b%it_= 400: + frontend_redirect = _visione_error_redirect( + request, + "request_rejected", + "Authentication request rejected", + ) + if frontend_redirect: + return frontend_redirect + return response + + +def visione_rp_error_context(request): + frontend_redirect_uri = _visione_redirect_uri(request) + if not frontend_redirect_uri: + return {} + params = { + "state": request.session.get("visione_state", ""), + "error": "invalid_request", + } + separator = "&" if "?" in frontend_redirect_uri else "?" + return { + "visione_return_url": f"{frontend_redirect_uri}{separator}{urlencode(params)}", + } + + +class VisioneRpCallbackView(SpidCieOidcRpCallbackView): + def get(self, request, *args, **kwargs): + if "error" in request.GET and _visione_redirect_uri(request): + return _visione_error_redirect( + request, + request.GET.get("error", "invalid_request"), + request.GET.get("error_description", ""), + ) + request_args = {k: v for k, v in request.GET.items()} + authz = OidcAuthentication.objects.filter(state=request_args.get("state")) + try: + self.validate_json_schema( + request.GET.dict(), + "authn_response", + "Authn response object validation failed", + ) + except ValidationException: + return _visione_error_redirect( + request, + "invalid_request", + "Authn response object validation failed", + ) or render( + request, + self.error_template, + { + "error": "invalid_request", + "error_description": "Authn response object validation failed", + **visione_rp_error_context(request), + }, + status=400, + ) + + if not authz: + return _visione_error_redirect( + request, + "unauthorized_request", + "Authentication not found", + ) or render( + request, + self.error_template, + { + "error": "unauthorized request", + "error_description": _("Authentication not found"), + **visione_rp_error_context(request), + }, + status=401, + ) + + authz = authz.last() + if request.GET.get("iss") and request.GET["iss"] != authz.provider_id: + return _visione_error_redirect( + request, + "invalid_request", + "authn response validation failed: mixups attack prevention.", + ) or render( + request, + self.error_template, + { + "error": "invalid request", + "error_description": _( + "authn response validation failed: mixups attack prevention." + ), + **visione_rp_error_context(request), + }, + status=400, + ) + + code = request.GET.get("code") + authz_token = OidcAuthenticationToken.objects.create(authz_request=authz, code=code) + self.rp_conf = FederationEntityConfiguration.objects.filter( + sub=authz_token.authz_request.client_id + ).first() + if not self.rp_conf: + return _visione_error_redirect( + request, + "invalid_request", + "Relying party not found", + ) or render( + request, + self.error_template, + { + "error": "invalid request", + "error_description": _("Relying party not found"), + **visione_rp_error_context(request), + }, + status=400, + ) + + authz_data = json.loads(authz.data) + token_response = self.access_token_request( + redirect_uri=authz_data["redirect_uri"], + state=authz.state, + code=code, + issuer_id=authz.provider_id, + client_conf=self.rp_conf, + token_endpoint_url=authz.provider_configuration["token_endpoint"], + audience=[authz.provider_id], + code_verifier=authz_data.get("code_verifier"), + ) + if not token_response: + return _visione_error_redirect( + request, + "invalid_token_response", + "Token response seems not to be valid", + ) or render( + request, + self.error_template, + { + "error": "invalid token response", + "error_description": _("Token response seems not to be valid"), + **visione_rp_error_context(request), + }, + status=400, + ) + + try: + self.validate_json_schema( + token_response, + "token_response", + "Token response object validation failed", + ) + except ValidationException: + return _visione_error_redirect( + request, + "invalid_request", + "Token response object validation failed", + ) or render( + request, + self.error_template, + { + "error": "invalid_request", + "error_description": "Token response object validation failed", + **visione_rp_error_context(request), + }, + status=400, + ) + + jwks = get_jwks(authz.provider_configuration) + access_token = token_response["access_token"] + id_token = token_response["id_token"] + op_ac_jwk = get_jwk_from_jwt(access_token, jwks) + op_id_jwk = get_jwk_from_jwt(id_token, jwks) + if not op_ac_jwk or not op_id_jwk: + return _visione_error_redirect( + request, + "invalid_token", + "Authentication token seems not to be valid.", + ) or render( + request, + self.error_template, + { + "error": "invalid_token", + "error_description": _("Authentication token seems not to be valid."), + **visione_rp_error_context(request), + }, + status=403, + ) + + try: + verify_jws(access_token, op_ac_jwk) + verify_jws(id_token, op_id_jwk) + decoded_id_token = unpad_jwt_payload(id_token) + verify_at_hash(decoded_id_token, access_token) + except Exception as exc: + logger.warning("Token validation error: %s", exc) + return _visione_error_redirect( + request, + "token_verification_failed", + "Authentication token validation error.", + ) or render( + request, + self.error_template, + { + "error": "token verification failed", + "error_description": _("Authentication token validation error."), + **visione_rp_error_context(request), + }, + status=403, + ) + + decoded_access_token = unpad_jwt_payload(access_token) + authz_token.access_token = access_token + authz_token.id_token = id_token + authz_token.scope = token_response.get("scope") + authz_token.token_type = token_response["token_type"] + authz_token.expires_in = token_response["expires_in"] + authz_token.save() + + userinfo = self.get_userinfo( + authz.state, + authz_token.access_token, + authz.provider_configuration, + verify=HTTPC_PARAMS.get("connection", {}).get("ssl", True), + ) + if not userinfo: + return _visione_error_redirect( + request, + "invalid_userinfo_response", + "UserInfo response seems not to be valid", + ) or render( + request, + self.error_template, + { + "error": "invalid userinfo response", + "error_description": _("UserInfo response seems not to be valid"), + **visione_rp_error_context(request), + }, + status=400, + ) + + user_attrs = process_user_attributes(userinfo, RP_ATTR_MAP, authz.__dict__) + if not user_attrs: + return _visione_error_redirect( + request, + "missing_user_attributes", + "No user attributes have been processed", + ) or render( + request, + self.error_template, + { + "error": "missing user attributes", + "error_description": _("No user attributes have been processed"), + **visione_rp_error_context(request), + }, + status=403, + ) + + user = self.user_reunification(user_attrs) + if not user: + return _visione_error_redirect( + request, + "no_user_found", + "No user found", + ) or render( + request, + self.error_template, + { + "error": _("No user found"), + "error_description": "", + **visione_rp_error_context(request), + }, + status=403, + ) + + request.session["rt_expiration"] = 0 + if token_response.get("refresh_token"): + refresh_token = token_response["refresh_token"] + authz_token.refresh_token = refresh_token + decoded_refresh_token = unpad_jwt_payload(refresh_token) + request.session["rt_expiration"] = decoded_refresh_token["exp"] - iat_now() + request.session["rt_jti"] = decoded_refresh_token["jti"] + + from django.contrib.auth import login + login(request, user) + request.session["oidc_rp_user_attrs"] = user_attrs + request.session["at_expiration"] = decoded_access_token["exp"] - iat_now() + request.session["at_jti"] = decoded_access_token["jti"] + authz_token.user = user + authz_token.save() + return HttpResponseRedirect("/visione/complete") diff --git a/relying_party/relying_party/settings.py b/relying_party/relying_party/settings.py index e9f5f63..ac624ca 100644 --- a/relying_party/relying_party/settings.py +++ b/relying_party/relying_party/settings.py @@ -28,7 +28,7 @@ ROOT_URLCONF = 'relying_party.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ diff --git a/relying_party/relying_party/urls.py b/relying_party/relying_party/urls.py index 33ae8f2..5043015 100644 --- a/relying_party/relying_party/urls.py +++ b/relying_party/relying_party/urls.py @@ -21,6 +21,7 @@ from django.views.static import serve from scripts.build_probe import build_probe_view, diag_view from spid_cie_oidc.entity.urls import urlpatterns as ta_urlpatterns from .visione_views import VisioneCompleteView, VisioneStartView +from .rp_overrides import VisioneRpBeginView, VisioneRpCallbackView admin.site.site_header = "OIDC Federation Relying Party Administration" admin.site.site_title = "OIDC Federation Relying Party" @@ -47,6 +48,10 @@ urlpatterns.extend(ta_urlpatterns) if 'spid_cie_oidc.relying_party' in settings.INSTALLED_APPS: from spid_cie_oidc.relying_party.urls import urlpatterns as rp_urlpatterns urlpatterns.extend(rp_urlpatterns) + urlpatterns = [ + path("oidc/rp/authorization", VisioneRpBeginView.as_view(), name="spid_cie_rp_begin"), + path("oidc/rp/callback", VisioneRpCallbackView.as_view(), name="spid_cie_rp_callback"), + ] + [u for u in urlpatterns if getattr(u, "name", None) not in {"spid_cie_rp_begin", "spid_cie_rp_callback"}] from spid_cie_oidc.entity.views import entity_configuration, resolve_entity_statement diff --git a/relying_party/relying_party/visione_views.py b/relying_party/relying_party/visione_views.py index cabfa76..39d02be 100644 --- a/relying_party/relying_party/visione_views.py +++ b/relying_party/relying_party/visione_views.py @@ -171,3 +171,17 @@ class VisioneCompleteView(View): if redirect_url: return HttpResponseRedirect(redirect_url) return HttpResponseServerError("Visione backend exchange did not redirect") + + +def visione_error_redirect(request, error, error_description=""): + frontend_redirect_uri = request.session.get("visione_frontend_redirect_uri") + if not frontend_redirect_uri: + return None + params = { + "state": request.session.get("visione_state", ""), + "error": error, + } + if error_description: + params["error_description"] = error_description + separator = "&" if "?" in frontend_redirect_uri else "?" + return HttpResponseRedirect(f"{frontend_redirect_uri}{separator}{urlencode(params)}") diff --git a/relying_party/templates/rp_error.html b/relying_party/templates/rp_error.html new file mode 100644 index 0000000..f9bb1a9 --- /dev/null +++ b/relying_party/templates/rp_error.html @@ -0,0 +1,16 @@ +{% extends 'rp_base.html' %} +{% load i18n %} + +{% block centered_container %} +

+
+
{% trans error %}
+

{% trans error_description %}

+ {% if visione_return_url %} +

+ {% trans "Torna a Visione" %} +

+ {% endif %} +
+
+{% endblock %}