No reviewers
Labels
No labels
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#65
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-51-compte-maximus-oauth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
get_edition()now checks account subscription status — Premium overrides Basetauri-plugin-deep-linkconfigured forsimpl-resultat://schemeIssues
Closes #51, partially addresses #53 (client-side ready, needs server API #49)
Test plan
npm run build+cargo check)🤖 Generated with Claude Code
🔍 PR Review —
feat: Maximus Account OAuth2 + machine activation (#51, #53)Verdict: REQUEST_CHANGES ⛔
Bonne PR dans l'ensemble — l'architecture OAuth2 PKCE est solide, l'intégration deep-link est propre, et la discipline i18n/changelog est respectée. Cependant, deux problèmes bloquants (sécurité + correctness) doivent être corrigés avant le merge.
Problèmes bloquants
1.
extract_auth_codene décode pas l'URL du code d'autorisationsrc-tauri/src/lib.rs:181— Le paramètrecodeest extrait brut du query string sans URL-decoding. Si le provider OIDC (Logto) percent-encode des caractères dans le code (courant avec+,=,/), l'échange de token échouera silencieusement avec une erreur "invalid grant".Fix: URL-décoder la valeur. Le crate
urlencodingest déjà en dépendance :2.
Mutex::lock().unwrap()peut paniquer et crasher l'appsrc-tauri/src/commands/auth_commands.rs:139,160— Si un code path panique en tenant le lock (improbable aujourd'hui mais fragile), le Mutex est empoisonné et ces.unwrap()crashent l'app entière.Fix: Remplacer par
.lock().map_err(|_| "Internal OAuth state error".to_string())?aux deux endroits.Suggestions (non-bloquantes)
auth_commands.rs:12-13) — Le commentaire reconnaît que c'est acceptable pour v1. Tracker l'intégration OS keychain comme issue de suivi.base64_url_encoderéimplémente base64 manuellement (auth_commands.rs:97-128) — Le cratebase64est déjà en dépendance (via jsonwebtoken). Utiliserbase64::engine::general_purpose::URL_SAFE_NO_PAD.encode()serait plus simple.img-src https:trop large (tauri.conf.json:21) — Restreindre au domaine Logto plutôt que tout HTTPS.check_subscription_statusappellerefresh_auth_tokenqui est aussi un#[tauri::command]— Extraire la logique dans un helper privérefresh_token_internal().🤖 Review by Claude Code
🔍 PR Review — REQUEST_CHANGES
Summary
Solid OAuth2 PKCE flow and machine activation implementation with good architectural decisions (PKCE verifier in memory, graceful degradation, daily check throttling, CSP). Three blocking issues need to be addressed before merge.
Blocking Issues
auth_commands.rs:147-182— Hand-rolled base64 encoder is fragile and unnecessary. Thebase64crate is already available as a transitive dependency (viajsonwebtokenwhich pullsbase64 0.22.1). The custombase64_encodemodule is unnecessary complexity and a potential source of subtle bugs in the PKCE challenge computation. If the challenge does not match exactly, the token exchange will silently fail with an opaque error from Logto. Replace withbase64::engine::general_purpose::URL_SAFE_NO_PAD.auth_commands.rs:265— Token file written without restrictive permissions. The file is written viafs::write()which inherits the process umask. On Linux with a permissive umask,tokens.json(containing refresh tokens) could be world-readable. The code comment says "acceptable because app data dir has user-only permissions" but Tauri'sapp_data_dirdoes NOT guarantee0700permissions on all platforms. At minimum, on Unix, set the file to0600after writing, or usestd::os::unix::fs::OpenOptionsExtto create it with restricted permissions.auth_commands.rs:453—chrono_now()uses.unwrap()onSystemTime. WhileSystemTime::now()succeeding is virtually guaranteed, theduration_since(UNIX_EPOCH)can technically panic if the system clock is set before epoch. Use.unwrap_or_default()or return an error to stay consistent with the "fail closed" philosophy used elsewhere (e.g.current_edition).Non-blocking Suggestions
simpl-resultat-desktop): Ensure this matches the actual Logto application configuration. Consider adding a comment/TODO.AccountInfo.emaildefaults to empty string if OIDC provider doesn't return one — considerOption<String>or returning an error.LicenseCard.tsx—loadMachinesonly loads on first expand: If user activates/deactivates from another device, they need to close and re-open. Consider loading each time the section is expanded.img-src https:is quite broad — tighten to the specific domain serving profile pictures if possible.check_account_editionreads from disk on everycurrent_edition()call — sincecheck_entitlementcan be called frequently, consider caching in memory with a TTL.🤖 Reviewed by Claude Code (
/pr-review)🔍 PR Review — REQUEST_CHANGES
Summary
The OAuth2 PKCE flow and machine activation features are well-structured overall, with good separation of concerns (Rust commands, TS services, React hooks/components). However, there is one security issue that must be fixed before merge.
Blocking Issues
src-tauri/src/commands/auth_commands.rs(lines ~217, ~290) —account.jsonis written withfs::write()(world-readable by default) instead ofwrite_restricted(). This file contains PII (email, name, picture URL) and thesubscription_statusfield that directly gates Premium features. An unprivileged local process could read or overwrite it to bypass the paywall. Both write sites (handle_auth_callbackandrefresh_auth_token) should usewrite_restricted()— consistent with the treatment oftokens.json.Non-blocking Suggestions
CSP
img-srcoverly broad (tauri.conf.json):img-src 'self' data: https:allows loading images from any HTTPS origin. Consider restricting to the Logto avatar domain.logoutdoes not clear the PKCE verifier (auth_commands.rs): If a user starts OAuth then logs out, the stale verifier remains in memory. Harmless but could cause a confusing error if the old callback arrives later.Missing
useAuthhook in architecture doc (docs/architecture.md): The hooks count was bumped to 14 but theuseAuthrow is not in the table.Review by
pr-reviewskillPR Review —
feat: Maximus Account OAuth2 + machine activation (#51, #53)Verdict: REQUEST_CHANGES
Solid implementation overall — clean PKCE flow, proper verifier lifetime management via Tauri managed state, good CSP, well-structured i18n, and graceful degradation on network errors. Two issues to address before merge.
Blocking Issues
Issue 1 —
last_checkfile not using restricted permissionssrc-tauri/src/commands/auth_commands.rsline ~399: Thelast_checktimestamp file is written viafs::write()(default permissions), whiletokens.jsonandaccount.jsoncorrectly usewrite_restricted()with0o600. For consistency across theauth/directory, usewrite_restricted()for this file as well — or set directory-level0o700permissions onauth/creation so all files inherit restricted access.Issue 2 —
useAuthmissing from architecture docs hook tabledocs/architecture.md: The hook count was bumped to 14, butuseAuthis not listed in the hook table. Should be added for documentation accuracy.Suggestions (non-blocking)
S1 —
.keys-temp/in.gitignoreConfirmed no private key material was committed in git history. Good practice to add the gitignore entry, but consider removing the directory from the repo entirely if it still exists locally, and document the key generation process in a separate secure location.
S2 —
subscription_statusfromcustom_datais fragileauth_commands.rsreadsinfo["custom_data"]["subscription_status"]from the Logto userinfo endpoint. This depends on Logto being configured to includecustom_datain the userinfo response. If the field is absent, Premium will silently never activate. Consider adding alog::debug!oreprintln!warning whencustom_datais missing to aid debugging.S3 —
auth-callback-errorpayload typinguseAuth.ts:listen<string>("auth-callback-error", ...)assumes the Rust side emits a plainString. This works today, but if the error is ever serialized as a JSON-wrapped string, the UI would display spurious quotes. Consider documenting this contract.Review by Claude Code (
/pr-review)PR Review —
#65feat: Maximus Account OAuth2 + machine activationVerdict: REQUEST_CHANGES
Solid implementation of OAuth2 PKCE and machine activation. Architecture is clean, i18n is complete, PKCE flow is well-designed with in-memory verifier. Two blocking issues need attention before merge.
Blocking Issues
1. CSP
img-src https:leaks user IP to third-party servers📄
src-tauri/tauri.conf.jsonThe directive
img-src 'self' data: https:allows loading images from any HTTPS server. WhenAccountCard.tsxrendersstate.account.picture(e.g. a Google/Gravatar avatar URL from the OIDC provider), the app will fetch the image directly from a third-party server — leaking the user's IP address and the fact that they use Simpl'Résultat.For a privacy-first app, this should be restricted:
Or better: proxy/cache profile pictures locally on first fetch.
2.
.keys-temp/in.gitignoreraises key-material concerns📄
.gitignoreAdding
.keys-temp/to.gitignoreimplies cryptographic key material was generated in the repo directory. The production Ed25519 public key was swapped in (license_commands.rs), so a private key was generated somewhere. Please:.keys-temp/gitignore entry from this PR (it's noise and a red flag)Suggestions (non-blocking)
Token storage (plain JSON): The comment in
auth_commands.rsacknowledges this and plans OS keychain for a future iteration. Acceptable for now — track as a follow-up issue.Offline Premium persistence: In
check_subscription_status, when refresh fails, thelast_checktimestamp is still updated. A user could be offline for weeks and retain Premium from the cachedaccount.json. Consider degrading to Base after N consecutive failed checks.Event listener cleanup in
useAuth.ts: Thelisten()calls use.then()to push unlisten functions. If the component unmounts before the promises resolve, the cleanup runs with an empty array and listeners leak. Consider an async IIFE pattern inside the effect.Logto app ID default:
logto_app_id()defaults to"simpl-resultat-desktop"— verify this matches the actual Logto app configuration (Logto usually generates random IDs).🤖 Review by Claude Code
🔍 PR Review — REQUEST_CHANGES
Reviewer: Claude Code (adversarial review)
La PR est bien structurée et couvre OAuth2 PKCE, activation de machines, et détection Premium. Cependant, il y a des problèmes bloquants.
Issues bloquantes
1.
src-tauri/src/commands/license_commands.rs:25-26— La clé publique de production casse les tests existants.La clé PEM a été changée de la clé de test RFC 8410 vers une clé de production. Les tests
license_commands.rs(#[cfg(test)]) signent des JWTs avec la clé privée correspondant à l'ancienne clé publique. Avec la nouvelle clé,cargo testdevrait échouer. Il faut soit conditionner la clé en#[cfg(test)], soit adapter les tests.2.
src-tauri/src/commands/auth_commands.rs:55-60— Tokens OAuth stockés en clair sur disque.Le commentaire mentionne "encrypted at rest in a future iteration via OS keychain". Le refresh token en particulier permet un accès persistant. Action requise : documenter cette limitation dans la PR body et créer une issue de suivi pour l'intégration keychain.
3.
src-tauri/src/lib.rs:152—handle_auth_callbackest exposé comme commande Tauri invocable depuis JS ET appelé depuis le deep-link handler.Le deep-link handler appelle déjà
commands::handle_auth_callbackdirectement côté Rust. L'exposer aussi comme commande Tauri dansinvoke_handlercrée une surface d'attaque inutile. Retirercommands::handle_auth_callbackde la listeinvoke_handler.Suggestions (non bloquantes)
logto_app_id()retourne"simpl-resultat-desktop"— vérifier que c'est bien le client ID configuré dans Logto (souvent un UUID).staten'est pas utilisé. PKCE mitigue le risque principal, maisstateprotège contre le login CSRF.useAuth.ts:107-115— leslisten()retournent des promises sans.catch().LicenseCard.tsxgère 6useState— envisager d'extraire un hookuseMachineActivation.fetch_userinfoutilise/oidc/me— vérifier que c'est le bon endpoint Logto (standard OIDC =/oidc/userinfo).