@@ -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) ]
201268mod tests {
202269 use super :: * ;
0 commit comments