⛔ STATUS: CRITICAL — PR MUST NOT MERGE
The OID identity service implements OAuth 2.0/OIDC correctly with strong cryptography (RS256 JWT, Argon2 passwords, PKCE-mandatory authorization code flow) and parameterized SQL queries throughout. However, a CRITICAL access-control gap exists: 9 management endpoints (tenants, users, roles, OAuth clients) have NO authentication whatsoever. Any anonymous caller can create tenants, register admin users, assign arbitrary permissions, and register OAuth clients — completely bypassing the identity system. Additionally, internal error messages are leaked to HTTP clients, a login timing side-channel enables email enumeration, and the CORS middleware is declared as a dependency but never configured.
🔴 Action Items (Priority Order)
1
[CRITICAL] Add AuthenticatedUser extractor + RBAC role checks to all 9 management endpoints
src/main.rs + src/api/{tenants,users,roles,clients}.rs
2
[MEDIUM] Sanitize internal error responses — return generic message, log full error server-side
src/api/{users,tenants,roles,clients,auth,oauth}.rs
3
[MEDIUM] Add dummy password hash verification for non-existent users (timing attack mitigation)
src/service/auth.rs:168
4
[MEDIUM] Configure JSON body size limit (4KB) and explicit CORS policy in App setup
src/main.rs:133
5
[MEDIUM] Install cargo-audit and add to CI pipeline for dependency advisory scanning
Cargo.toml / .github/workflows/
6
[MEDIUM] Replace SHA-256 refresh token hashing with Argon2 or HMAC-SHA256 with server secret
src/domain/refresh_token.rs:29
OWASP Top 10 Findings
Every repository uses sqlx parameterized queries ($1, $2, …). No string interpolation in SQL. Reviewed: tenant_repo.rs, user_repo.rs, role_repo.rs, client_repo.rs, auth_code_repo.rs, refresh_token_repo.rs.
TokenValidator uses jsonwebtoken with RS256, validates iss and aud claims. Expired token tests pass.
src/domain/token.rs:167
User passwords hashed with argon2 crate. Verification uses argon2::verify_encoded for constant-time comparison at the hash level.
In AuthService::login, find_by_email_and_tenant returns NotFound early — before verify_password is called. Argon2 hashing takes ~100-300ms; requests for non-existent users return in <1ms. An attacker can enumerate valid email/tenant combinations via response timing.
src/service/auth.rs:168
💡 Always call verify_password against a static dummy_hash constant when the user is not found, discarding the result.
hash_token() delegates to compute_s256_challenge() — a raw SHA-256 with no salt or work factor. The comment reads "not password, so no argon2 needed". However, if the token_hash column is leaked via SQL injection or DB breach, SHA-256 hashes of 64-char tokens can be reversed offline with GPU acceleration in hours.
src/domain/refresh_token.rs:29
💡 Use Argon2 or HMAC-SHA256 with a server-side secret key (SECRET_KEY env var) for refresh token hashing.
.gitignore excludes: .env, *.pem, *.key, *.crt, *.p12, *.pfx. Secrets scan found only test fixture strings in #[cfg(test)] blocks.
Multiple handlers return e.to_string() as error_description in HTTP 500 responses. OidError::Internal wraps raw strings that may include PostgreSQL error text, connection string fragments, or internal paths. Examples: users.rs:107, tenants.rs:84, auth.rs:74.
src/api/users.rs:107, src/api/tenants.rs:84, src/api/auth.rs:74
💡 Return only a generic "An internal error occurred" message for server errors. Log the full error with tracing::error! and a correlation ID server-side.
JWT Claims includes email field. JWT payload is base64-encoded, not encrypted — readable by anyone with the token. Acceptable per OIDC spec but worth noting for data sensitivity.
src/domain/token.rs:21
No XML parsing anywhere in the service. All request/response bodies use serde_json.
The following routes are registered in main.rs with no AuthenticatedUser extractor and no auth middleware:
POST /v1/tenants — anyone can create a tenant
GET /v1/tenants/{id} — anyone can read tenant data
POST /v1/users — anyone can create users (incl. admin)
GET /v1/users/{id} — anyone can read any user profile
PATCH /v1/users/{id} — anyone can modify any user
PUT /v1/users/{id}/roles — anyone can grant admin roles to any user
POST /v1/roles — anyone can create roles with any permissions
GET /v1/roles — anyone can enumerate all roles
POST /v1/clients — anyone can register OAuth clients
GET /v1/clients — anyone can list OAuth clients
The AuthenticatedUser extractor EXISTS (src/api/extractors.rs) and works correctly, but is NOT used in any of these handlers. Attack chain: create tenant → create admin user → assign full permissions → register OAuth client → full system takeover.
src/main.rs:149-164 + src/api/{tenants,users,roles,clients}.rs
💡 Add AuthenticatedUser as a parameter to all management handlers. Enforce role checks (admin-only). Tenant creation may need a bootstrap mechanism using service-level client_credentials.
Row-Level Security enabled on tenants, users, user_roles, roles, clients, auth_codes, refresh_tokens. All policies gate on app.tenant_id context variable.
code_challenge required; only S256 accepted. Plain method explicitly rejected. Single-use codes atomically consumed via UPDATE … WHERE used = false.
src/domain/auth_code.rs:77
redirect_uri validated against registered list. HTTPS required (localhost exempt). Fragment (#) in redirect_uri rejected to prevent token leakage.
actix-cors 0.7 is in Cargo.toml but no Cors middleware is added to App::new() in main.rs. CORS policy is undefined — browsers apply same-origin by default, but this should be explicit.
src/main.rs:133 / Cargo.toml:9
💡 Configure Cors::default().allowed_origin(…) explicitly, or remove actix-cors if not needed for this service.
No web::JsonConfig with .limit() registered. Default actix-web limit is 32KB. Login/token endpoints should be capped at 4KB to reduce memory pressure under flood conditions.
src/main.rs:133
💡 Add .app_data(web::JsonConfig::default().limit(4096)) to App configuration.
KeyPair::generate() runs on each boot (main.rs:67). All issued JWTs become invalid after any restart or deployment. No graceful key rotation. JWKS endpoint serves only the current ephemeral key.
src/main.rs:67
💡 Store RSA private key in Vault or a secrets manager. On startup, load existing key if present; generate only on first boot. Serve old keys in JWKS for their remaining TTL during rotation.
RUST_LOG defaults to "info". No cfg(debug_assertions) gated production code paths. TracingLogger used for structured logging.
All responses are JSON. No server-side HTML template rendering. XSS not applicable.
All request bodies deserialized with #[derive(Deserialize)] via serde_json. No unsafe transmute, no custom Deserialize with side effects, no untyped serde_json::Value used for security decisions.
cargo audit returned "no such command". RustSec advisory database scan could not be performed. Dependencies include: actix-web 4, sqlx 0.8, jsonwebtoken 9, rsa 0.9, argon2 0.5, rdkafka 0.36, chrono 0.4, rand 0.9.
Cargo.toml
💡 Run: cargo install cargo-audit && cargo audit. Add to CI: cargo audit --deny warnings.
login.succeeded and login.failed CloudEvents emitted to Redpanda on every auth attempt. TracingLogger middleware on all requests.
src/api/auth.rs:44
Catch-all error arms return e.to_string() as error_description. For OidError::Internal, this exposes database error messages, internal paths, and other implementation details to clients. This is both an A03 and A10 concern.
src/api/auth.rs:74
💡 Log full error with tracing::error!(error=?e, ...). Return only generic "server_error" externally.
TracingLogger::default() is used but X-Correlation-Id header is not extracted or generated. Cross-service request tracing is not possible without correlation IDs.
src/main.rs:135
💡 Extract X-Correlation-Id header or generate a UUID per request and include in all log spans via tracing::Span.
Additional Checks
Unsafe Code
✅ PASS
grep for unsafe in src/**/*.rs returned zero matches. No unsafe blocks in any production code.
Secrets Scan
✅ PASS
No hardcoded credentials found. One test fixture string ("test-token-value") in #[cfg(test)] block — not a real secret. .gitignore covers .env, *.pem, *.key, *.crt.
Dependency Review
✅ PASS (with note)
All dependencies are well-maintained crates from reputable sources (RustCrypto, Actix, sqlx). One unused dependency:
actix-cors (declared, never used in App setup)
Cargo Audit
⚠ UNKNOWN
cargo-audit not installed. Advisory scan could not run.
cargo install cargo-audit
cargo audit
Input Validation
✅ PASS
Email validated (@ + domain). Password: 8-128 chars, upper+lower+digit required. Slug: 3-63 chars, alphanumeric+hyphens. Redirect URIs: HTTPS required, fragments rejected. PKCE verifier: RFC 7636 compliant.
SQL Injection
✅ PASS
All queries use sqlx parameterized binding (.bind() calls). Zero raw SQL string interpolation found. No raw queries with user-controlled values.