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)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user