snarkos_node_rest/helpers/
auth.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use snarkvm::prelude::*;
17
18use ::time::OffsetDateTime;
19use anyhow::{Result, anyhow};
20use axum::{
21    RequestPartsExt,
22    body::Body,
23    http::{Request, StatusCode},
24    middleware::Next,
25    response::{IntoResponse, Response},
26};
27use axum_extra::{
28    TypedHeader,
29    headers::authorization::{Authorization, Bearer},
30};
31use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
32use once_cell::sync::OnceCell;
33use serde::{Deserialize, Serialize};
34
35/// The time a jwt token is valid for.
36pub const EXPIRATION: i64 = 10 * 365 * 24 * 60 * 60; // 10 years.
37
38/// The JWT secret for the REST server.
39static JWT_SECRET: OnceCell<Vec<u8>> = OnceCell::new();
40
41/// The Json web token claims.
42#[derive(Debug, Deserialize, Serialize)]
43pub struct Claims {
44    /// The subject (user).
45    sub: String,
46    /// The UTC timestamp the token was issued at.
47    iat: i64,
48    /// Expiration time (as UTC timestamp).
49    exp: i64,
50}
51
52impl Claims {
53    pub fn new<N: Network>(address: Address<N>, jwt_secret: Option<Vec<u8>>, jwt_timestamp: Option<i64>) -> Self {
54        if let Some(secret) = jwt_secret {
55            JWT_SECRET.set(secret)
56        } else {
57            JWT_SECRET.set({
58                let seed: [u8; 16] = ::rand::thread_rng().gen();
59                seed.to_vec()
60            })
61        }
62        .expect("Failed to set JWT secret: already initialized");
63
64        let issued_at = jwt_timestamp.unwrap_or_else(|| OffsetDateTime::now_utc().unix_timestamp());
65        let expiration = issued_at.saturating_add(EXPIRATION);
66
67        Self { sub: address.to_string(), iat: issued_at, exp: expiration }
68    }
69
70    /// Returns the json web token string.
71    pub fn to_jwt_string(&self) -> Result<String> {
72        encode(&Header::default(), &self, &EncodingKey::from_secret(JWT_SECRET.get().unwrap())).map_err(|e| anyhow!(e))
73    }
74}
75
76pub async fn auth_middleware(request: Request<Body>, next: Next) -> Result<Response, Response> {
77    // Deconstruct the request to extract the auth token.
78    let (mut parts, body) = request.into_parts();
79    let auth: TypedHeader<Authorization<Bearer>> =
80        parts.extract().await.map_err(|_| StatusCode::UNAUTHORIZED.into_response())?;
81
82    if let Err(err) = decode::<Claims>(
83        auth.token(),
84        &DecodingKey::from_secret(JWT_SECRET.get().unwrap()),
85        &Validation::new(Algorithm::HS256),
86    ) {
87        warn!("Request authorization error: {err}");
88        return Err(StatusCode::UNAUTHORIZED.into_response());
89    }
90
91    // Reconstruct the request.
92    let request = Request::from_parts(parts, body);
93
94    Ok(next.run(request).await)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use base64::prelude::*;
101    use snarkvm::prelude::{Address, MainnetV0};
102
103    #[test]
104    fn check_const_jwt_value() {
105        // Arbitrary input values to check against the expected value.
106        let secret = "FVPjEPVAKh2f0EkRCpQkqA==";
107        let timestamp = 174437065;
108
109        let secret_bytes = BASE64_STANDARD.decode(secret).unwrap();
110
111        // A fixed seed, as the address also forms part of the JWT.
112        let mut rng = TestRng::fixed(12345);
113        let pk = PrivateKey::<MainnetV0>::new(&mut rng).unwrap();
114        let addr = Address::try_from(pk).unwrap();
115
116        let claims = Claims::new(addr, Some(secret_bytes), Some(timestamp));
117        let jwt_str = claims.to_jwt_string().unwrap();
118
119        assert_eq!(
120            jwt_str,
121            "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.\
122            eyJzdWIiOiJhbGVvMTBrbmtlbHZuZDU1ZnNhYX\
123            JtMjV3Y2g3cDlzdWYydHFsZ3d5NWs0bnh3bXM2\
124            ZDI2Mnh5ZnFtMnRjY3IiLCJpYXQiOjE3NDQzNz\
125            A2NSwiZXhwIjo0ODk3OTcwNjV9.HcTvPC7jQyq\
126            NaPqsC2XHZl3Yji_OHxo5TyKLSKVxirI"
127        );
128    }
129}