From 590b927e31eab9378f1050b3045b52c09b19de5b Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 25 Jun 2026 00:38:01 +0200 Subject: [PATCH] fix(auth): corrections runtime LinkedIn OAuth (#88) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Workaround nonce OIDC LinkedIn (non-conformité) : capture MissingClaimError après échange de code réussi et fetch userinfo séparément - Ajout token_endpoint_auth_method: client_secret_post (requis par LinkedIn) - suppress_callback_exceptions=True pour corriger les erreurs Dash multi-pages - Suppression bouton déconnexion redondant sur /compte/admin et style hover incorrect sur la navbar Co-Authored-By: Claude Sonnet 4.6 --- src/app.py | 1 + src/assets/css/style.css | 4 ---- src/auth/oauth.py | 5 ++++- src/auth/routes.py | 14 ++++++++++++-- src/db.py | 5 ++++- src/pages/compte_admin.py | 1 - tests/test.parquet | Bin 10626 -> 10792 bytes tests/users.test.sqlite | Bin 36864 -> 53248 bytes uv.lock | 27 +++++++++++++++++++++++++++ 9 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/app.py b/src/app.py index 913d23b..fdb5023 100644 --- a/src/app.py +++ b/src/app.py @@ -67,6 +67,7 @@ app: Dash = Dash( server=server, title="decp.info", use_pages=True, + suppress_callback_exceptions=True, compress=True, meta_tags=META_TAGS, ) diff --git a/src/assets/css/style.css b/src/assets/css/style.css index ed1681f..6fe3c2f 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -78,10 +78,6 @@ button[disabled] { color: #666; } -button:hover:not([disabled]) { - background-color: #fee; -} - /* Global Link Styles */ #_pages_content a { color: #993333; diff --git a/src/auth/oauth.py b/src/auth/oauth.py index 1e73b28..a53f4bd 100644 --- a/src/auth/oauth.py +++ b/src/auth/oauth.py @@ -17,5 +17,8 @@ def init_oauth(app: Flask) -> None: client_id=os.getenv("LINKEDIN_CLIENT_ID"), client_secret=os.getenv("LINKEDIN_CLIENT_SECRET"), server_metadata_url=LINKEDIN_DISCOVERY_URL, - client_kwargs={"scope": "openid profile email"}, + client_kwargs={ + "scope": "openid profile email", + "token_endpoint_auth_method": "client_secret_post", + }, ) diff --git a/src/auth/routes.py b/src/auth/routes.py index 57b2281..ea1d217 100644 --- a/src/auth/routes.py +++ b/src/auth/routes.py @@ -264,12 +264,22 @@ def linkedin_callback(): # L'utilisateur a refusé / annulé l'autorisation côté LinkedIn. return _redirect_with_error("/connexion", "oauth_cancelled") try: - token = oauth.linkedin.authorize_access_token() + # LinkedIn ne retourne pas le nonce dans l'ID token (non-conformité OIDC) : + # authorize_access_token() lève MissingClaimError("nonce") après avoir échangé + # le code avec succès. Le token est déjà stocké dans oauth.linkedin.token à ce + # moment, donc on capture cette erreur précise et on continue. + try: + oauth.linkedin.authorize_access_token() + except Exception as exc: + if "nonce" not in str(exc) or not oauth.linkedin.token: + raise + resp = oauth.linkedin.get("https://api.linkedin.com/v2/userinfo") + resp.raise_for_status() + userinfo = resp.json() except Exception: logger.exception("Échec de l'échange de token LinkedIn") return _redirect_with_error("/connexion", "oauth_failed") - userinfo = token.get("userinfo") or {} subject = userinfo.get("sub") email = (userinfo.get("email") or "").strip().lower() if not subject or not email: diff --git a/src/db.py b/src/db.py index cc148ed..0152d6d 100644 --- a/src/db.py +++ b/src/db.py @@ -14,13 +14,16 @@ from src.utils import get_last_modified, logger def should_rebuild(db_path: Path, parquet_path: str) -> bool: db_path = Path(db_path) if not db_path.exists(): + logger.info("Fichier DuckDB inexistant.") return True dev = os.getenv("DEVELOPMENT", "False").lower() == "true" force = os.getenv("REBUILD_DUCKDB", "False").lower() == "true" if dev and not force: return False last_modified: float = get_last_modified(parquet_path) - return last_modified > db_path.stat().st_mtime + fresh_parquet = last_modified > db_path.stat().st_mtime + logger.info(f"Parquet plus récent : {str(fresh_parquet)}") + return fresh_parquet def _load_source_frame() -> pl.DataFrame: diff --git a/src/pages/compte_admin.py b/src/pages/compte_admin.py index 09143a9..812d92b 100644 --- a/src/pages/compte_admin.py +++ b/src/pages/compte_admin.py @@ -200,7 +200,6 @@ def layout( _password_section(), html.Hr(className="mt-4"), _danger_section(), - _logout_section(), ] ) return account_shell("admin", contenu) diff --git a/tests/test.parquet b/tests/test.parquet index d75858a2ca547fd9a9214cafe9a0a9d1d07807b3..9b8fc9b653f06d7556b5c5e3337693fe5277e38b 100644 GIT binary patch delta 1726 zcmZuyZAe>J7``#dF=~>l=(*m+lhl}c%wqkBpRKK}t=jrgKbp$e#y0F~Sv#hTehfNB zb(sw5+N|yCLRN;8BH}jZ7zPPN#C*(DhkEX6&<-be;ox3^sb)3%ep=^g=jA#@T=}aG;gB4 zCIauoy^s2n=yDDE(nTTJ z_%0g4bS;fQ$aoD+rqeBnKsE}ZH5(nX5tu+%QIOE33XBy=z)|(2V73Pg2K70Cz|h|i6Xk{BT(SXgh%E#(N+naEg|rd49A5{?e**apJucSSd@Qmn&&%}#ptcw0|h>W|Eo%-%j9DYs`1i?n6fC0a) zhEoW)tKqx}_vqBPKPh$`w&08EX+*>wYPgURJB_B|XVudoj9*m4$#i^H4cBZe>{g_M zL_;|7jUByd7k*tuue)Q+qcq0x*b{qlI5!4Z%!>gw3u31eMYwl|c&kKxvX4lia@?kh z1oy?l;r;jx^>n@x_w8I9s~Yc0Yt)y`=?FI4N(Gc`ll7O*bzrj;C%q;Gb(A+~nc&g+ zWSvhCq^r$1P3k>@vjjS%hbQUjPNj4I1sQ0@k~F*x`Q8Y-M z5?5X-vmXQf!a*Cgm$T9qaK-aPe zbeg;##Sa(>@}KGII&(o*cSg3fO;(4?1Ui@v6~v=C2CMiQ&qnKbsTOm%R2mEtdye8H z{QW2E{k3hfvN5*635Nf(j{ntB<|tK(cFk%;Da^AY^}La0_7M0nk!sdsvyAcV(|WG8 zkq?&~9;%oSmnknTEcD6B#$>+Z(9s4i6Y;^cBU3Xe_UL7vebB&(-edOki8)I%E|+!v z1S`+SjksB$A?8?x7H_uVV@&72P1NUSJgbeoi3XUVeP!_y#pW1NkeO{r0~`@Pj-8CF z0-iWa9E$nH01*zD*aqA?K;UbuEm!eGhKO%Eb^i3(Z$FiH0!BE?@jo5M6>L@yu`i04 zqZy|tt@zo~gH71a>UWsHUt$hXjFhqITbpWSzA%;z&a%=po}PI+!Sp;9gx14;-#?|& BtQ-IU delta 1705 zcmZuxeN0nV6u++*Xz3>o?(>yCpwRKeqClyDlRE(g6cDH%G0U=SBB0`&IK?HBMVgKn z<5&kh&anMqHo_lHw}u3=FyhQU$U=88UQINY?nN53uLV7Dr8+IoR56mhs_bwjT0Ejr~F zU7a<}l!!k{6o}da&~5k9TV))s+PmrP7dUtwSLpBM9O~RT@YvDixU*G&X`r!25eE#U zD5HZZj}1K$1A9p(gg8}snX=;pYI1*I6m9CtsmvM$}*=U{ERIkYD3?M-F- zb?;&tdqp=xYuORq{LG1F&uZx@R~)gJ!TMwM7ToNZjvn;HnSaQQbM)ptZP^;x9n5pa4FlT7YS~Tn~^Q-gzg>V?0hUSTrv7hS_73XflYBXn&P8I zi2U0wmL}h;WI05;Z%~=lSwfOhoQk2Njag-bd{hog+B3t68OytvEC;JZ#=`^^Dx~b# zs5<0uBgTfRSS``QMiqXPva)b=#XIyB`xS8@!LR_$S1~Wq#*@L~^yp)qX_qG&nIcEF zv3jE2wZH*qb_GtlNcV3lQN)4i(;|I#n@GQJQ(1!EvGI-0<6SMsyH?unljIReTip)D zF3*dUbKBWgqAfb$fiw3oE(y{moM|FNSvisTi9k(mX9YyN^xV^{Ft&EKwQmpXUm?CI z$>WlSYlYdSGGA$t_$YA;SuQpq%hF1%nN&EN?ooSF1zM66Ny7M55g(}**LRDl5KP>d zfqRJnLs^M}Uxa)uAk)=M!+*FI%&r0tZY&}a11%>zPARJo#Qh|2%hjxyXcyDrRhQR; z8~s1H2LgAsM&O23h`I_haN|WqKUVaMilJsi%+`pC!7TXORT$F(yFA#8uOw8_GSt-%_o`)~hJY6a9#=jS#$PRJiUua;^yk1ef3p-EXk9ShLVHX=4Rn`ceS7DQl7DqXG!pcc8b9p%7@Q0 z*2TohE}suzXlkcu<>tmnW}XM7N_0|IQdHzqRv*Z4S-{TKiB29DLQ|$YCS?wJOoYbk z1jCJD6@Ibi#H7tG-wfdOJgQ?&MEi9U{E?ZTjY@~~{~oP5!7y1bMr*DVDzoy_V>*aR zA}@>RQa#Hg+MUfRylMBRqbec^QA9Yr8^iq#A{;7L!QybPL$SoLv9aZ3>yht1RbmE$ zhXpv=z_t+W`itOl=jnp#u}$p8PUjog9-=K&z!`U*^FQ*6+0!C>V%O+b?$%^e5+fu~ Hwa51l!3mrG diff --git a/tests/users.test.sqlite b/tests/users.test.sqlite index 8c70b44aab8e57f665b2ae7ec5889176b1b7feed..16de7617d850c7ffc948e96a5acfc449d610c873 100644 GIT binary patch delta 626 zcmZozz|^pSd4jYc3j+fKFA%c=F%uATP1G?~Wns|k{l&|r#=yni#=uv`vyAr~w>?iZ z#~=O{_BL)2uKoO;{GJ;dnb>Wcin-av#l;z0T}u*^a#Bl+Q;Ukh1dnr&t7C|(LWrZ2 zkE?P>&T9<(C1CC<41i!OuTL!7tRu2jZ;aQkJBw)a1$Y_{Aoh^U6;C&n+h(5aj9W z7!;}C?HZ||0n@CbP+SU9S)w_)i`RvPPmy8f#Kx&g!MtqZ+Tx7GnRzLx6`3g&@!%kZ z_#Vt;n(WMH%%sUP`8>Y{BkyK=?ng{4d?y$}CpHF7F5?Sg;Xlf-XJX?HHhV5M@nGG_ zi98bO=-!AoL>J)(CJF0_jpmCK1U4%OJm8-=L1B^s3mYTw-I0*pY+48%-8`S<*r|LZd*EHY340EQ~6F#rGn delta 224 zcmZozz}&EaX@ayM69WSSClJE`%S0VxStbU(vQA$99}FDa_6&R{_|Ni{@hS2w<2}c1 z&l9~_P~b4vW_#{OOiaNnlMiyqGqO&8$Yngaj4w#PiHVV2TwI*7Ik_Y;DJQkGIJKx4 zOfWeIxjKfpDug&X`M4^8g%mWnCLd>u2uvzhjV2T;&3y#fy{8<_R D35Gp^ diff --git a/uv.lock b/uv.lock index 83a3f6e..110706c 100644 --- a/uv.lock +++ b/uv.lock @@ -66,6 +66,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "authlib" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, +] + [[package]] name = "backports-datetime-fromisoformat" version = "2.0.3" @@ -854,6 +867,7 @@ name = "decp-info" version = "2.8.0" source = { virtual = "." } dependencies = [ + { name = "authlib" }, { name = "boto3" }, { name = "brevo-python" }, { name = "cryptography" }, @@ -898,6 +912,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "authlib" }, { name = "boto3" }, { name = "brevo-python", specifier = "==5.0.0rc1" }, { name = "cryptography" }, @@ -1316,6 +1331,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] +[[package]] +name = "joserfc" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/90/25cb27518750218e4f850be63d8bbb2343efaad1c01c3571aaa4b3c33bd7/joserfc-1.7.1.tar.gz", hash = "sha256:77d0b76514879c68c6f433bc5b7357a4ab72008ff1e33d8379fd11d72bd8ca81", size = 233181, upload-time = "2026-06-08T07:21:33.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/00/fa62404c3e347f946faa13aa21085205f9cc06ad17671e37f81a51662ae8/joserfc-1.7.1-py3-none-any.whl", hash = "sha256:b3e3d655612e2e1ef67b2600f2f420e12e537b020208fab1761fad647319c164", size = 70423, upload-time = "2026-06-08T07:21:32.001Z" }, +] + [[package]] name = "jsbeautifier" version = "1.15.4"