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:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -200,7 +200,6 @@ def layout(
|
||||
_password_section(),
|
||||
html.Hr(className="mt-4"),
|
||||
_danger_section(),
|
||||
_logout_section(),
|
||||
]
|
||||
)
|
||||
return account_shell("admin", contenu)
|
||||
|
||||
Reference in New Issue
Block a user