mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 15:40:18 +00:00
fix: token hash mismatch — hash hex string, not raw bytes
Critical auth bug: token creation hashed the raw 32 bytes (hasher.update(token_bytes)) but authentication hashed the hex-encoded string (hash_token(candidate) where candidate is the hex string the user sends). This meant newly created tokens could never authenticate. Fixed all 4 token creation sites (users, tokens, invitations create, invitations accept) to use hash_token(&plaintext_token) which hashes the hex string consistently with the auth lookup path. Removed now-unused sha2::Digest imports from handlers. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
@@ -5,7 +5,6 @@ use std::sync::Arc;
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use rand::RngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::channels::web::auth::{AdminUser, AuthenticatedUser};
|
||||
@@ -37,11 +36,7 @@ pub async fn invitations_create_handler(
|
||||
let mut token_bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut token_bytes);
|
||||
let plaintext_token = hex::encode(token_bytes);
|
||||
|
||||
// SHA-256 hash for storage — plaintext is never persisted.
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(token_bytes);
|
||||
let hash: [u8; 32] = hasher.finalize().into();
|
||||
let hash = crate::channels::web::auth::hash_token(&plaintext_token);
|
||||
|
||||
let invitation_id = Uuid::new_v4();
|
||||
let invitation = InvitationRecord {
|
||||
@@ -129,15 +124,9 @@ pub async fn invitations_accept_handler(
|
||||
.to_string();
|
||||
|
||||
// Hash the provided token to look up the invitation.
|
||||
let token_bytes = hex::decode(invite_token).map_err(|_| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid invite token format".to_string(),
|
||||
)
|
||||
})?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&token_bytes);
|
||||
let hash: [u8; 32] = hasher.finalize().into();
|
||||
// Hash the plaintext token string (not decoded bytes) — must match
|
||||
// how it was hashed during invitation creation via hash_token().
|
||||
let hash = crate::channels::web::auth::hash_token(invite_token);
|
||||
|
||||
// Look up the invitation by hash.
|
||||
let invitation = store
|
||||
@@ -187,10 +176,7 @@ pub async fn invitations_accept_handler(
|
||||
let mut api_token_bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut api_token_bytes);
|
||||
let plaintext_api_token = hex::encode(api_token_bytes);
|
||||
|
||||
let mut api_hasher = Sha256::new();
|
||||
api_hasher.update(api_token_bytes);
|
||||
let api_hash: [u8; 32] = api_hasher.finalize().into();
|
||||
let api_hash = crate::channels::web::auth::hash_token(&plaintext_api_token);
|
||||
|
||||
let api_prefix = &plaintext_api_token[..8];
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use axum::{
|
||||
};
|
||||
use rand::RngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::channels::web::auth::AuthenticatedUser;
|
||||
@@ -41,14 +40,12 @@ pub async fn tokens_create_handler(
|
||||
expires_in_days.map(|days| chrono::Utc::now() + chrono::Duration::days(days as i64));
|
||||
|
||||
// Generate 32 random bytes for the token.
|
||||
// Hash the hex-encoded plaintext (what the user sends as Bearer token),
|
||||
// NOT the raw bytes — must match hash_token() in auth.rs.
|
||||
let mut token_bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut token_bytes);
|
||||
let plaintext_token = hex::encode(token_bytes);
|
||||
|
||||
// SHA-256 hash for storage — plaintext is never persisted.
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(token_bytes);
|
||||
let hash: [u8; 32] = hasher.finalize().into();
|
||||
let hash = crate::channels::web::auth::hash_token(&plaintext_token);
|
||||
|
||||
// First 8 chars of the hex token as a prefix for identification.
|
||||
let token_prefix = &plaintext_token[..8];
|
||||
|
||||
@@ -9,7 +9,6 @@ use axum::{
|
||||
};
|
||||
use rand::RngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::channels::web::auth::{AdminUser, AuthenticatedUser};
|
||||
@@ -71,12 +70,12 @@ pub async fn users_create_handler(
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Generate a first API token so the new user can authenticate immediately.
|
||||
// Hash the hex-encoded plaintext (what the user sends as Bearer token),
|
||||
// NOT the raw bytes — must match hash_token() in auth.rs.
|
||||
let mut token_bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut token_bytes);
|
||||
let plaintext_token = hex::encode(token_bytes);
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(token_bytes);
|
||||
let token_hash: [u8; 32] = hasher.finalize().into();
|
||||
let token_hash = crate::channels::web::auth::hash_token(&plaintext_token);
|
||||
let token_prefix = &plaintext_token[..8];
|
||||
|
||||
let _token_record = store
|
||||
|
||||
Reference in New Issue
Block a user