Skip to content

fix(aver-server): shared store, consent recovery, OAuth hardening#7

Open
5queezer wants to merge 1 commit into
masterfrom
fix/server-hardening
Open

fix(aver-server): shared store, consent recovery, OAuth hardening#7
5queezer wants to merge 1 commit into
masterfrom
fix/server-hardening

Conversation

@5queezer

@5queezer 5queezer commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Fixes the server-hardening code-review findings against crates/aver-server. Re-verified each finding against master after 35a36be (token TTLs + loopback CORS already landed upstream; AuthDb::revoke_consent already revoked tokens at the DB layer — the missing pieces were the HTTP route and the skip-loop gate).

Findings → fixes

1. Per-session Store broke single-writer (MAJOR)

build_router now opens AverTools once at startup and the session factory clones the same Arc<Mutex<AverTools>> into AverMcpService::from_shared_tools (http.rs, mcp.rs). rusqlite connections are Send-not-Sync, so the existing Arc<Mutex<_>> pattern (same as AuthDb) is used. Per-session state stays limited to auth/scope request extensions. No busy_timeout added — that belongs to the core PR.

  • Test: tests/shared_store.rs — two concurrent MCP sessions write claims and recall sees both; mcp.rs unit test services_sharing_tools_observe_each_others_writes (threaded writers).

2. Consent empty-grant trap (MAJOR)

Zero-scope approvals recorded granted_scopes="" and consent_covers([], []) == true skipped the consent screen forever, minting tokens that fail every require_scope. Conservative fix (chosen over mapping empty→all SUPPORTED, which would grant more than asked): the skip path now requires a non-empty grant (consent.rs), so the screen re-renders and record_consent's upsert overwrites the empty row. New POST /oauth/consent/revoke route (session-cookie + origin validated) exposes the previously unreachable AuthDb::revoke_consent.

  • Tests: empty_scope_approval_does_not_trap_user_in_skip_loop (regression for the stuck loop), revoke_route_revokes_consent_and_tokens_and_allows_reconsent. Existing approve_..._skips_screen updated to grant claims:read (it previously pinned the trap).

3. alpha validated then discarded (MAJOR)

recall no longer advertises alpha (removed from the MCP schema and tools::RecallParams). Threading it was not viable: hybrid recall needs an EmbeddingClient and aver-server has none (aver-core built without the ollama feature; wiring one would break the offline/deterministic test rule). aver-core's recall_hybrid_claims_with_alpha is untouched for CLI/BEAM use. Instructions card needed no change (it never mentioned alpha).

  • Tests: alpha fields removed from tests/tools.rs / tests/scope_mcp.rs; obsolete recall_tool_rejects_alpha_outside_unit_interval deleted.

4. OAuth gaps (MAJOR)

  • Token response now includes expires_in (ACCESS_TOKEN_TTL_SECS, now pub).
  • Refresh tokens rotate on use; presenting a rotated/revoked refresh token revokes the whole (user, client) token family (RFC 6819 §5.2.2.3 reuse detection). tests/refresh_and_cors.rs updated (it pinned reuse) + new refresh_token_reuse_revokes_token_family.
  • Consent revocation invalidating tokens: already implemented by upstream in AuthDb::revoke_consent — now reachable via the revoke route (covered end-to-end by the route test).

5. Minor batch

  • /oauth/token returns RFC 6749 §5.2 JSON errors (invalid_grant / invalid_request / unsupported_grant_type) — oauth_token_route_returns_rfc6749_json_errors.
  • 401s carry WWW-Authenticate: Bearer (RFC 6750 §3) — asserted in http_routes.rs.
  • tracing_unavailable_warn stub removed: authenticate_request returns Result; auth-DB failures surface as HTML 500.
  • let _ = ALTER TABLE migrations → apply_column_migration: ignores only the duplicate-column error (rusqlite 0.32 maps it to ErrorCode::Unknown/ext 1 + message; comment explains), propagates everything else; expiry backfills propagate too.
  • PKCE verification reuses the constant-time constant_time_eq (moved to oauth.rs, shared with the CSRF check).
  • Scope validation rejects empty path segments (///, a//b) and caps length at 256 bytes — rejects_empty_path_segments, rejects_overlong_scope.
  • json_tool_result maps serialization failure to McpError instead of an empty success block.
  • Stale origin.rs module doc corrected; consolidate scope doc reworded to match the "all"-only implementation; "nomic-embed-text" is now DEFAULT_EMBEDDING_MODEL; AVER_PORT parse has .context("invalid AVER_PORT").

6. adapters.rs wiring

Still test-only (only tests/adapter_boundaries.rs references it; no production path in main.rs/http.rs/mcp.rs). Left as-is — intentional ADR-0016 boundary.

Verification

  • cargo fmt applied.
  • cargo clippy -p aver-server --all-targets -- -D warnings — clean.
  • cargo test -p aver-server172 passed, 0 failed, 0 ignored across all test binaries (deterministic/offline, no #[ignore]).

Docs: README endpoint list + consent/token flow paragraph updated (rotation, reuse detection, expires_in, revoke route, empty-grant behavior). No doc/adr/*.md edits; no autoresearch.jsonl changes; aver-core untouched (privacy filtering / log-first ordering preserved).

Summary by CodeRabbit

  • New Features

    • Added browser-based consent revocation.
    • Added refresh-token rotation with reuse detection and token-family revocation.
    • MCP sessions now share stored data consistently.
    • Added RFC-compliant OAuth token errors and bearer authentication challenges.
  • Bug Fixes

    • Fixed zero-scope consent flows so the consent screen appears when required.
    • Improved handling of authentication, migration, scope validation, and token expiry errors.
  • Documentation

    • Documented consent revocation, zero-scope approvals, and refresh-token behavior.

- Share one Store across MCP sessions: build_router opens AverTools once
  and hands every session the same Arc<Mutex<_>>, preserving the
  single-writer invariant (per-session Store::open raced log rotation
  and claim-id pre-allocation on the same memory_dir).
- Consent empty-grant trap: zero-scope approvals no longer skip the
  consent screen (consent_covers([],[]) == true trapped users forever),
  and POST /oauth/consent/revoke now exposes AuthDb::revoke_consent so
  users can revoke + re-consent over HTTP.
- recall drops the advertised-but-discarded alpha parameter; hybrid
  weighting needs an embedding client the server does not have.
- Token endpoint: RFC 6749 expires_in, section 5.2 JSON error bodies;
  refresh tokens rotate on use with RFC 6819 reuse detection (family
  revocation); 401s carry WWW-Authenticate: Bearer.
- Hardening: auth-DB upsert failures surface as 500 instead of a
  swallowed stub; ALTER TABLE migrations only ignore duplicate-column
  errors; PKCE verify reuses the constant-time comparison; scope
  validation rejects empty path segments and caps length at 256;
  serialization failures map to McpError instead of an empty success
  block; stale origin/consolidate docs fixed; embedding-model label is
  a named const; AVER_PORT parse errors carry context.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

OAuth token rotation and consent revocation are added, MCP sessions share one tools store, and scope and recall contracts are tightened. Error responses, authentication handling, documentation, and integration coverage are updated accordingly.

Changes

OAuth integrity

Layer / File(s) Summary
Token lifecycle and protocol responses
crates/aver-server/src/auth.rs, crates/aver-server/src/http.rs, crates/aver-server/src/config.rs, crates/aver-server/tests/http_routes.rs, crates/aver-server/tests/refresh_and_cors.rs, README.md
Token migrations and expiry reporting are updated; refresh tokens rotate with reuse detection and family revocation; token endpoint errors use RFC 6749 JSON responses.
Consent authentication and revocation
crates/aver-server/src/consent.rs, crates/aver-server/src/oauth.rs, crates/aver-server/src/http.rs, crates/aver-server/src/origin.rs, crates/aver-server/tests/oauth_consent_flow.rs, crates/aver-server/tests/http_routes.rs, README.md
Consent authentication errors propagate explicitly, CSRF and PKCE comparisons use constant-time equality, zero-scope grants keep the consent screen active, and POST /oauth/consent/revoke is wired and tested.

Shared MCP storage

Layer / File(s) Summary
Shared MCP service construction and behavior
crates/aver-server/src/http.rs, crates/aver-server/src/mcp.rs
MCP sessions use a shared synchronized tools store, recall no longer accepts alpha, and serialization failures return MCP internal errors.
Shared-session integration coverage
crates/aver-server/tests/shared_store.rs
Independent MCP sessions perform concurrent writes and verify that both claims are visible through recall.

Scope and tool contracts

Layer / File(s) Summary
Scope validation boundaries
crates/aver-server/src/scope_resolution.rs, crates/aver-server/tests/scope_resolution.rs
Scope resolution rejects empty path segments and values longer than 256 bytes while accepting the 256-byte boundary.
Recall and vector tool contracts
crates/aver-server/src/tools.rs, crates/aver-server/src/mcp.rs, crates/aver-server/tests/scope_mcp.rs, crates/aver-server/tests/tools.rs
The recall alpha field and processing are removed, vector ingestion uses a shared model constant, and affected tool tests are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant OAuthToken
  participant AuthDb
  participant TokenFamily
  Client->>OAuthToken: Submit authorization_code or refresh_token grant
  OAuthToken->>AuthDb: Validate grant and load token state
  AuthDb-->>OAuthToken: Return authorization or refresh data
  OAuthToken->>TokenFamily: Rotate token or revoke reused family
  TokenFamily-->>OAuthToken: Return token pair or error
  OAuthToken-->>Client: Return JSON token response
Loading

Possibly related PRs

  • 5queezer/aver#1: Modifies the OAuth consent flow around scope selection and consent decision handling.

Poem

Tokens turn, old secrets fall,
Consent guards the station wall.
Shared tools remember every claim,
Scope paths stand precise and tame.
The system holds. No breach remains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main aver-server changes: shared MCP storage, consent revocation/recovery, and OAuth hardening.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/server-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
crates/aver-server/src/oauth.rs (1)

11-24: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Do not maintain a home-grown constant-time primitive.

Use a vetted implementation already approved by the project, such as a ConstantTimeEq abstraction, instead of making two security boundaries depend on this local loop. The subtle crate provides that dedicated contract. (docs.rs)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aver-server/src/oauth.rs` around lines 11 - 24, Replace the local
constant_time_eq byte loop with the project-approved subtle::ConstantTimeEq
implementation, updating callers in the PKCE and consent-flow comparisons to use
its constant-time equality contract and convert the result to bool as needed.
Remove the home-grown helper while preserving the existing equality behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aver-server/src/auth.rs`:
- Around line 577-582: Update the refresh-token model and rotation flow around
the revoked-token handling in the authentication implementation to persist a
unique family identifier, inherit the existing identifier when issuing rotated
tokens, and assign a new identifier for a fresh login or consent generation.
Change revoke_token_family and its callers to target that family identifier
rather than the broad user/client pair, ensuring reuse revokes only the affected
lineage while preserving current-token behavior.
- Line 531: Update refresh_access_token() so token revocation and
issue_token_pair() persistence execute within one database transaction. Make
revocation conditional on the presented token, require exactly one affected row
before minting, and commit both the revocation and replacement-row inserts
together so any failure rolls back the entire rotation.

In `@crates/aver-server/src/consent.rs`:
- Around line 490-495: Update the error branches in the authentication paths,
including the branches around html_error at the shown locations, to log the
detailed anyhow error server-side and pass a fixed generic message to html_error
instead of interpolating err. Apply the same behavior to all referenced paths
while preserving the existing HTTP status and response structure.
- Around line 989-995: Update AuthDb::revoke_consent in auth.rs to execute the
client_consents, access_tokens, and refresh_tokens updates within a single
database transaction, committing only after all succeed and rolling back on any
failure. Keep the existing error propagation used by the caller in consent.rs.

In `@crates/aver-server/src/http.rs`:
- Around line 452-457: Update the token response around the JSON containing
access_token and refresh_token to include Cache-Control: no-store and Pragma:
no-cache headers, while preserving the existing response body and status
behavior.
- Around line 423-429: Update the "authorization_code" validation branch in the
token request handler to include request.redirect_uri.is_empty() alongside the
existing required-field checks. Return token_error with BAD_REQUEST and
"invalid_request" before database exchange when redirect_uri is omitted, while
preserving validation for the other fields.
- Around line 414-417: Update the oauth_token extractor handling so form parsing
failures, including invalid content types, malformed encoding, and missing
required fields, are handled inside oauth_token rather than short-circuiting.
Route every such failure through token_error and return the token JSON contract
with error set to invalid_request, while preserving normal TokenRequest
processing.

---

Nitpick comments:
In `@crates/aver-server/src/oauth.rs`:
- Around line 11-24: Replace the local constant_time_eq byte loop with the
project-approved subtle::ConstantTimeEq implementation, updating callers in the
PKCE and consent-flow comparisons to use its constant-time equality contract and
convert the result to bool as needed. Remove the home-grown helper while
preserving the existing equality behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdb3d5cd-d82d-4dc5-a676-525b844513ea

📥 Commits

Reviewing files that changed from the base of the PR and between f84fd7e and 146d9bd.

📒 Files selected for processing (17)
  • README.md
  • crates/aver-server/src/auth.rs
  • crates/aver-server/src/config.rs
  • crates/aver-server/src/consent.rs
  • crates/aver-server/src/http.rs
  • crates/aver-server/src/mcp.rs
  • crates/aver-server/src/oauth.rs
  • crates/aver-server/src/origin.rs
  • crates/aver-server/src/scope_resolution.rs
  • crates/aver-server/src/tools.rs
  • crates/aver-server/tests/http_routes.rs
  • crates/aver-server/tests/oauth_consent_flow.rs
  • crates/aver-server/tests/refresh_and_cors.rs
  • crates/aver-server/tests/scope_mcp.rs
  • crates/aver-server/tests/scope_resolution.rs
  • crates/aver-server/tests/shared_store.rs
  • crates/aver-server/tests/tools.rs
💤 Files with no reviewable changes (2)
  • crates/aver-server/tests/scope_mcp.rs
  • crates/aver-server/tests/tools.rs

granted_scopes,
)?;
let refresh_token = existing_refresh_token.unwrap_or_else(random_session_id);
let refresh_token = random_session_id();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/aver-server/src/auth.rs"

echo '--- outline ---'
ast-grep outline "$FILE" --view expanded || true

echo '--- lines 480-640 ---'
sed -n '480,640p' "$FILE" | cat -n

echo '--- search for refresh token / transaction keywords ---'
rg -n "refresh_token|transaction|begin|commit|rollback|affected|execute|query" "$FILE"

Repository: 5queezer/aver

Length of output: 13032


🏁 Script executed:

sed -n '500,620p' crates/aver-server/src/auth.rs | cat -n

Repository: 5queezer/aver

Length of output: 5902


🏁 Script executed:

#!/bin/bash
set -euo pipefail
grep -n "let refresh_token = random_session_id();" -n crates/aver-server/src/auth.rs

Repository: 5queezer/aver

Length of output: 204


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- refresh_access_token call sites ---'
rg -n "refresh_access_token\(" crates -g '!target'

echo '--- transaction usage in auth.rs ---'
rg -n "transaction|BEGIN|COMMIT|ROLLBACK|savepoint|with_transaction|unchecked_transaction" crates/aver-server/src/auth.rs

echo '--- auth db open / connection setup ---'
sed -n '175,240p' crates/aver-server/src/auth.rs | cat -n

Repository: 5queezer/aver

Length of output: 1403


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

path = Path("crates/aver-server/src/auth.rs")
text = path.read_text()

def extract(fn_name):
    m = re.search(rf'(?m)^\s*(?:pub\s+)?fn\s+{re.escape(fn_name)}\b', text)
    if not m:
        return None
    start = m.start()
    brace = text.find("{", m.end())
    depth = 0
    i = brace
    while i < len(text):
        c = text[i]
        if c == "{":
            depth += 1
        elif c == "}":
            depth -= 1
            if depth == 0:
                return text[start:i+1]
        i += 1
    return None

for fn in ["issue_token_pair", "refresh_access_token", "revoke_token_family"]:
    body = extract(fn)
    print(f"--- {fn} ---")
    if body:
        print(body)
    else:
        print("not found")

print("--- transaction keywords in auth.rs ---")
for kw in ["transaction(", "begin", "commit", "rollback", "savepoint"]:
    hits = [m.start() for m in re.finditer(kw, text, flags=re.I)]
    print(kw, len(hits), hits[:10])
PY

Repository: 5queezer/aver

Length of output: 4947


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '420,455p' crates/aver-server/src/http.rs | cat -n

Repository: 5queezer/aver

Length of output: 1807


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- AuthDb struct and open ---'
sed -n '175,240p' crates/aver-server/src/auth.rs | cat -n

echo '--- any Clone/Arc/Mutex around AuthDb in repo ---'
rg -n "AuthDb|Arc<Mutex<|Mutex<.*AuthDb|Connection" crates/aver-server/src -g '!target'

Repository: 5queezer/aver

Length of output: 5596


Move refresh rotation into one transaction. refresh_access_token() revokes the presented token, then issue_token_pair() persists the replacement rows in separate statements. If anything fails between those writes, the old token is dead and no successor exists. Make the revoke conditional, require one affected row, and commit revoke + mint together.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aver-server/src/auth.rs` at line 531, Update refresh_access_token() so
token revocation and issue_token_pair() persistence execute within one database
transaction. Make revocation conditional on the presented token, require exactly
one affected row before minting, and commit both the revocation and
replacement-row inserts together so any failure rolls back the entire rotation.

Comment on lines +577 to +582
if row.revoked_at.is_some() {
// Reuse of an already-rotated (or revoked) refresh token: treat
// as potential token theft and kill the whole family for this
// (user, client), RFC 6819 §5.2.2.3 style.
self.revoke_token_family(&row.user_id, row.client_id.as_deref())?;
anyhow::bail!("refresh token reuse detected; token family revoked");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Track the actual refresh-token family.

Keying reuse revocation by (user_id, client_id) lets any retired token from an older login or consent generation revoke every current login for that client. Persist a family identifier, inherit it during rotation, and revoke only that lineage.

Also applies to: 596-610

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aver-server/src/auth.rs` around lines 577 - 582, Update the
refresh-token model and rotation flow around the revoked-token handling in the
authentication implementation to persist a unique family identifier, inherit the
existing identifier when issuing rotated tokens, and assign a new identifier for
a fresh login or consent generation. Change revoke_token_family and its callers
to target that family identifier rather than the broad user/client pair,
ensuring reuse revokes only the affected lineage while preserving current-token
behavior.

Comment on lines +490 to +495
Err(err) => {
return html_error(
StatusCode::INTERNAL_SERVER_ERROR,
"Server error",
&format!("Failed to authenticate the user: {err}"),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Keep database errors out of browser responses.

These paths expose the full anyhow chain to remote clients. Log the detailed failure server-side; return a fixed generic message to the browser.

Also applies to: 733-738, 917-922

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aver-server/src/consent.rs` around lines 490 - 495, Update the error
branches in the authentication paths, including the branches around html_error
at the shown locations, to log the detailed anyhow error server-side and pass a
fixed generic message to html_error instead of interpolating err. Apply the same
behavior to all referenced paths while preserving the existing HTTP status and
response structure.

Comment on lines +989 to +995
if let Err(err) = auth_db.revoke_consent(&user.id, &form.client_id) {
return html_error(
StatusCode::INTERNAL_SERVER_ERROR,
"Server error",
&format!("Failed to revoke consent: {err}"),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate all revoke_consent definitions and call sites.
rg -n "revoke_consent" crates/aver-server -S

# Map the likely implementation file(s) before reading them.
for f in $(rg -l "revoke_consent" crates/aver-server -S); do
  echo "### AST outline for $f"
  ast-grep outline "$f" --view expanded || true
done

Repository: 5queezer/aver

Length of output: 10093


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the implementation around revoke_consent once located.
for f in $(rg -l "revoke_consent" crates/aver-server -S); do
  echo "### $f"
  rg -n -C 8 "revoke_consent" "$f" -S
done

Repository: 5queezer/aver

Length of output: 5301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrow to the file under review and its surrounding module.
sed -n '930,1035p' crates/aver-server/src/consent.rs

Repository: 5queezer/aver

Length of output: 3254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for transaction handling around consent revocation and token revocation paths.
rg -n -C 4 "revoke_consent|revoke_token|revoke.*consent|refresh token|access token|transaction|begin" crates/aver-server/src -S

Repository: 5queezer/aver

Length of output: 9459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "revoke_consent" crates/aver-server -S
for f in $(rg -l "revoke_consent" crates/aver-server -S); do
  echo "### $f"
  ast-grep outline "$f" --view expanded || true
  rg -n -C 8 "revoke_consent" "$f" -S
done

sed -n '930,1035p' crates/aver-server/src/consent.rs

Repository: 5queezer/aver

Length of output: 18126


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '760,790p' crates/aver-server/src/auth.rs
echo
sed -n '596,615p' crates/aver-server/src/auth.rs

Repository: 5queezer/aver

Length of output: 2168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "transaction\(|Transaction<|BEGIN TRANSACTION|commit\(|rollback\(" crates/aver-server/src/auth.rs -S

Repository: 5queezer/aver

Length of output: 151


Make consent revocation atomic. crates/aver-server/src/auth.rs:765 updates client_consents, access_tokens, and refresh_tokens as separate autocommit writes. A failure between statements can leave consent revoked while live credentials remain usable. Wrap the three updates in one transaction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aver-server/src/consent.rs` around lines 989 - 995, Update
AuthDb::revoke_consent in auth.rs to execute the client_consents, access_tokens,
and refresh_tokens updates within a single database transaction, committing only
after all succeed and rolling back on any failure. Keep the existing error
propagation used by the caller in consent.rs.

Comment on lines 414 to +417
async fn oauth_token(
axum::extract::State(state): axum::extract::State<HttpState>,
Form(request): Form<TokenRequest>,
) -> Result<Json<serde_json::Value>, axum::http::StatusCode> {
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the handler and related error path.
FILE="crates/aver-server/src/http.rs"

echo "== file lines around oauth_token =="
sed -n '360,520p' "$FILE" | cat -n

echo
echo "== search for token_error and TokenRequest =="
rg -n "token_error|TokenRequest|oauth_token|FormRejection|invalid_request" "$FILE"

Repository: 5queezer/aver

Length of output: 4987


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect Axum extractor usage elsewhere in the repo, if any.
rg -n "Result<.*Form<|Form<.*Result|extract::rejection|FormRejection" crates -S

Repository: 5queezer/aver

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read Axum docs in-source references from the repository, if vendored or documented.
rg -n "supports wrapping extractors in Result|FormRejection|invalid_request" . -S

Repository: 5queezer/aver

Length of output: 683


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/aver-server/src/http.rs"
echo "== file excerpt =="
sed -n '360,520p' "$FILE" | cat -n
echo
echo "== relevant symbols =="
rg -n "token_error|TokenRequest|oauth_token|FormRejection|invalid_request" "$FILE" -n

Repository: 5queezer/aver

Length of output: 4947


🌐 Web query:

Axum Form extractor rejection happens before handler Result extractor customize rejections docs.rs FormRejection invalid_request

💡 Result:

In Axum, extractors like Form run and potentially return a rejection before the handler function is ever called [1][2]. If an extraction fails, the request is terminated with an error response, and the handler logic is bypassed [1][3]. To customize the error response for a Form rejection, you have three primary options: 1. Handle the Rejection in the Handler: You can wrap the Form extractor in Result<Form<T>, FormRejection> within your handler's signature [1]. This allows you to match on the FormRejection enum and return a custom response [1][4]. async fn handler(payload: Result<Form, FormRejection>) -> impl IntoResponse { match payload { Ok(Form(form)) => { /* success */ } Err(rejection) => { // Return custom status or body (StatusCode::BAD_REQUEST, format!("Custom error: {}", rejection.body_text)) } } } 2. Use WithRejection (Recommended for reuse): The axum-extra crate provides a WithRejection wrapper [5]. It allows you to transform the default FormRejection into your own custom error type [3][5]. Your custom type must implement IntoResponse and From<FormRejection> [5]. async fn handler(WithRejection(Form(form), _): WithRejection<Form, MyCustomError>) { //... } 3. Create a Custom Extractor: You can implement FromRequest manually for a custom struct [6][7]. Inside your from_request implementation, you call the Form extractor, catch the Result, and map the Err(FormRejection) variant to your own error type before returning it [6]. Note that Form extraction may consume the request body [8][2]. Because of this, it is critical that the Form extractor be placed last in the handler's argument list if multiple extractors are used [8][2]. The FormRejection enum includes variants such as InvalidFormContentType, FailedToDeserializeForm, FailedToDeserializeFormBody, and BytesRejection, which you can inspect to tailor your error handling [9][10].

Citations:


Map form rejections to the token JSON contract. Form<TokenRequest> still short-circuits before oauth_token on bad content type, malformed encoding, or missing required fields, so those failures bypass token_error and return Axum’s default rejection. Wrap the extractor in Result or parse the form yourself so every token-endpoint input error returns invalid_request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aver-server/src/http.rs` around lines 414 - 417, Update the
oauth_token extractor handling so form parsing failures, including invalid
content types, malformed encoding, and missing required fields, are handled
inside oauth_token rather than short-circuiting. Route every such failure
through token_error and return the token JSON contract with error set to
invalid_request, while preserving normal TokenRequest processing.

Comment on lines +423 to +429
"authorization_code" => {
if request.code.is_empty()
|| request.client_id.is_empty()
|| request.code_verifier.is_empty()
{
return Err(token_error(StatusCode::BAD_REQUEST, "invalid_request"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat an omitted redirect_uri as invalid_request.

It is required for this authorization-code contract, but an empty value currently reaches the database exchange and becomes invalid_grant.

Proposed fix
             if request.code.is_empty()
                 || request.client_id.is_empty()
                 || request.code_verifier.is_empty()
+                || request.redirect_uri.is_empty()
             {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"authorization_code" => {
if request.code.is_empty()
|| request.client_id.is_empty()
|| request.code_verifier.is_empty()
{
return Err(token_error(StatusCode::BAD_REQUEST, "invalid_request"));
}
"authorization_code" => {
if request.code.is_empty()
|| request.client_id.is_empty()
|| request.code_verifier.is_empty()
|| request.redirect_uri.is_empty()
{
return Err(token_error(StatusCode::BAD_REQUEST, "invalid_request"));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aver-server/src/http.rs` around lines 423 - 429, Update the
"authorization_code" validation branch in the token request handler to include
request.redirect_uri.is_empty() alongside the existing required-field checks.
Return token_error with BAD_REQUEST and "invalid_request" before database
exchange when redirect_uri is omitted, while preserving validation for the other
fields.

Comment on lines 452 to 457
Ok(Json(serde_json::json!({
"access_token": tokens.access_token,
"refresh_token": tokens.refresh_token,
"token_type": "Bearer",
"expires_in": ACCESS_TOKEN_TTL_SECS,
})))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Prevent caching of issued tokens.

Responses containing access and refresh tokens must include Cache-Control: no-store and Pragma: no-cache; returning bare Json omits both. (rfc-editor.org)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aver-server/src/http.rs` around lines 452 - 457, Update the token
response around the JSON containing access_token and refresh_token to include
Cache-Control: no-store and Pragma: no-cache headers, while preserving the
existing response body and status behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant