Skip to main content

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().r#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    // If the JWT secret is not set, skip authentication.
78    if JWT_SECRET.get().is_none() {
79        return Ok(next.run(request).await);
80    }
81
82    // Deconstruct the request to extract the auth token.
83    let (mut parts, body) = request.into_parts();
84    let auth: TypedHeader<Authorization<Bearer>> =
85        parts.extract().await.map_err(|_| StatusCode::UNAUTHORIZED.into_response())?;
86
87    if let Err(err) = decode::<Claims>(
88        auth.token(),
89        &DecodingKey::from_secret(JWT_SECRET.get().unwrap()),
90        &Validation::new(Algorithm::HS256),
91    ) {
92        warn!("Request authorization error: {err}");
93        return Err(StatusCode::UNAUTHORIZED.into_response());
94    }
95
96    // Reconstruct the request.
97    let request = Request::from_parts(parts, body);
98
99    Ok(next.run(request).await)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use base64::prelude::*;
106    use snarkvm::prelude::{Address, MainnetV0};
107
108    #[test]
109    fn check_const_jwt_value() {
110        // Arbitrary input values to check against the expected value.
111        let secret = "FVPjEPVAKh2f0EkRCpQkqA==";
112        let timestamp = 174437065;
113
114        let secret_bytes = BASE64_STANDARD.decode(secret).unwrap();
115
116        // A fixed seed, as the address also forms part of the JWT.
117        let mut rng = TestRng::fixed(12345);
118        let pk = PrivateKey::<MainnetV0>::new(&mut rng).unwrap();
119        let addr = Address::try_from(pk).unwrap();
120
121        let claims = Claims::new(addr, Some(secret_bytes), Some(timestamp));
122        let jwt_str = claims.to_jwt_string().unwrap();
123
124        assert_eq!(
125            jwt_str,
126            "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.\
127            eyJzdWIiOiJhbGVvMTBrbmtlbHZuZDU1ZnNhYX\
128            JtMjV3Y2g3cDlzdWYydHFsZ3d5NWs0bnh3bXM2\
129            ZDI2Mnh5ZnFtMnRjY3IiLCJpYXQiOjE3NDQzNz\
130            A2NSwiZXhwIjo0ODk3OTcwNjV9.HcTvPC7jQyq\
131            NaPqsC2XHZl3Yji_OHxo5TyKLSKVxirI"
132        );
133    }
134}