fix(auth): corrections runtime LinkedIn OAuth (#88)

- 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 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-06-25 00:38:01 +02:00
parent 5a702ac77e
commit 590b927e31
9 changed files with 48 additions and 9 deletions
+1
View File
@@ -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,
)
-4
View File
@@ -78,10 +78,6 @@ button[disabled] {
color: #666;
}
button:hover:not([disabled]) {
background-color: #fee;
}
/* Global Link Styles */
#_pages_content a {
color: #993333;
+4 -1
View File
@@ -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",
},
)
+12 -2
View File
@@ -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:
+4 -1
View File
@@ -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:
-1
View File
@@ -200,7 +200,6 @@ def layout(
_password_section(),
html.Hr(className="mt-4"),
_danger_section(),
_logout_section(),
]
)
return account_shell("admin", contenu)