Skip to content

Commit 161832e

Browse files
committed
feat: add Bearer session token auth for trustless dashboard sessions
1 parent aa59b75 commit 161832e

File tree

2 files changed

+73
-0
lines changed

2 files changed

+73
-0
lines changed

crates/rpc-server/src/auth.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,73 @@ fn canonicalize_json_value(value: &serde_json::Value) -> String {
197197
}
198198
}
199199

200+
/// Parse and verify a Bearer session token.
201+
///
202+
/// Token format: `{hotkey_hex}:{expiry_ms}:{nonce}:{signature_hex}`
203+
/// Signed message: `session:{hotkey_ss58}:{expiry_ms}:{nonce}`
204+
///
205+
/// Returns the hotkey hex string if the token is valid.
206+
/// Does NOT check revocation (that requires a WASM call to the auth challenge).
207+
pub fn verify_bearer_token(token: &str) -> Result<BearerTokenInfo, AuthError> {
208+
let parts: Vec<&str> = token.splitn(4, ':').collect();
209+
if parts.len() != 4 {
210+
return Err(AuthError::InvalidSignature);
211+
}
212+
213+
let hotkey_hex = parts[0];
214+
let expiry_str = parts[1];
215+
let nonce = parts[2];
216+
let signature_hex = parts[3];
217+
218+
// Parse hotkey
219+
let hotkey = Hotkey::from_hex(hotkey_hex).ok_or(AuthError::InvalidHotkey)?;
220+
221+
// Parse expiry
222+
let expiry: i64 = expiry_str.parse().map_err(|_| AuthError::InvalidNonce)?;
223+
224+
// Check expiry
225+
let now_ms = chrono::Utc::now().timestamp_millis();
226+
if now_ms > expiry {
227+
return Err(AuthError::MessageExpired);
228+
}
229+
230+
// Verify signature
231+
let hotkey_ss58 = hotkey.to_ss58();
232+
let message = format!("session:{}:{}:{}", hotkey_ss58, expiry, nonce);
233+
234+
match verify_validator_signature(hotkey_hex, &message, signature_hex)? {
235+
true => Ok(BearerTokenInfo {
236+
hotkey_hex: hotkey_hex.to_string(),
237+
expiry,
238+
nonce: nonce.to_string(),
239+
}),
240+
false => Err(AuthError::VerificationFailed),
241+
}
242+
}
243+
244+
/// Parsed and verified bearer token info.
245+
pub struct BearerTokenInfo {
246+
pub hotkey_hex: String,
247+
pub expiry: i64,
248+
pub nonce: String,
249+
}
250+
251+
/// Extract Bearer token from Authorization header.
252+
pub fn extract_bearer_token(headers: &HashMap<String, String>) -> Option<String> {
253+
let headers_lower: HashMap<String, String> = headers
254+
.iter()
255+
.map(|(k, v)| (k.to_lowercase(), v.clone()))
256+
.collect();
257+
258+
headers_lower
259+
.get("authorization")
260+
.and_then(|v| {
261+
v.strip_prefix("Bearer ")
262+
.or_else(|| v.strip_prefix("bearer "))
263+
})
264+
.map(|t| t.trim().to_string())
265+
}
266+
200267
#[cfg(test)]
201268
mod tests {
202269
use super::*;

crates/rpc-server/src/server.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,12 @@ async fn challenge_route_handler(
428428
} else {
429429
None
430430
}
431+
})
432+
// Fallback: try Bearer session token
433+
.or_else(|| {
434+
let token = crate::auth::extract_bearer_token(&headers_map)?;
435+
let info = crate::auth::verify_bearer_token(&token).ok()?;
436+
Some(info.hotkey_hex)
431437
});
432438

433439
// Enforce auth on routes that require it

0 commit comments

Comments
 (0)