Skip to main content

forest/rpc/
auth_layer.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use crate::auth::{JWT_IDENTIFIER, verify_token};
5use crate::key_management::KeyStore;
6use crate::rpc::{CANCEL_METHOD_NAME, Permission, RpcMethod as _, chain};
7use ahash::{HashMap, HashMapExt as _};
8use futures::future::Either;
9use http::{
10    HeaderMap,
11    header::{AUTHORIZATION, HeaderValue},
12};
13use itertools::Itertools as _;
14use jsonrpsee::MethodResponse;
15use jsonrpsee::core::middleware::{Batch, BatchEntry, BatchEntryErr, Notification};
16use jsonrpsee::server::middleware::rpc::RpcServiceT;
17use jsonrpsee::types::Id;
18use jsonrpsee::types::{ErrorObject, error::ErrorCode};
19use parking_lot::RwLock;
20use std::sync::{Arc, LazyLock};
21use tower::Layer;
22use tracing::debug;
23
24static METHOD_NAME2REQUIRED_PERMISSION: LazyLock<HashMap<&str, Permission>> = LazyLock::new(|| {
25    let mut access = HashMap::new();
26
27    macro_rules! insert {
28        ($ty:ty) => {
29            access.insert(<$ty>::NAME, <$ty>::PERMISSION);
30
31            if let Some(alias) = <$ty>::NAME_ALIAS {
32                access.insert(alias, <$ty>::PERMISSION);
33            }
34        };
35    }
36    super::for_each_rpc_method!(insert);
37
38    access.insert(chain::CHAIN_NOTIFY, Permission::Read);
39    access.insert(CANCEL_METHOD_NAME, Permission::Read);
40
41    access
42});
43
44fn is_allowed(required_by_method: Permission, claimed_by_user: &[String]) -> bool {
45    let needle = match required_by_method {
46        Permission::Admin => "admin",
47        Permission::Sign => "sign",
48        Permission::Write => "write",
49        Permission::Read => "read",
50    };
51    claimed_by_user.iter().any(|haystack| haystack == needle)
52}
53
54#[derive(Clone)]
55pub struct AuthLayer {
56    pub headers: HeaderMap,
57    pub keystore: Arc<RwLock<KeyStore>>,
58}
59
60impl<S> Layer<S> for AuthLayer {
61    type Service = Auth<S>;
62
63    fn layer(&self, service: S) -> Self::Service {
64        Auth {
65            headers: self.headers.clone(),
66            keystore: self.keystore.clone(),
67            service,
68        }
69    }
70}
71
72#[derive(Clone)]
73pub struct Auth<S> {
74    headers: HeaderMap,
75    keystore: Arc<RwLock<KeyStore>>,
76    service: S,
77}
78
79impl<S> Auth<S> {
80    fn authorize<'a>(&self, method_name: &str) -> Result<(), ErrorObject<'a>> {
81        match check_permissions(&self.keystore, self.headers.get(AUTHORIZATION), method_name) {
82            Ok(true) => Ok(()),
83            Ok(false) => {
84                tracing::warn!("Unauthorized access attempt for method {method_name}");
85                Err(ErrorObject::borrowed(
86                    http::StatusCode::UNAUTHORIZED.as_u16() as _,
87                    "Unauthorized",
88                    None,
89                ))
90            }
91            Err(code) => {
92                tracing::warn!("Authorization error for method {method_name}: {code:?}");
93                Err(ErrorObject::from(code))
94            }
95        }
96    }
97}
98
99impl<S> RpcServiceT for Auth<S>
100where
101    S: RpcServiceT<
102            MethodResponse = MethodResponse,
103            NotificationResponse = MethodResponse,
104            BatchResponse = MethodResponse,
105        > + Send
106        + Sync
107        + Clone
108        + 'static,
109{
110    type MethodResponse = S::MethodResponse;
111    type NotificationResponse = S::NotificationResponse;
112    type BatchResponse = S::BatchResponse;
113
114    fn call<'a>(
115        &self,
116        req: jsonrpsee::types::Request<'a>,
117    ) -> impl Future<Output = Self::MethodResponse> + Send + 'a {
118        match self.authorize(req.method_name()) {
119            Ok(()) => Either::Left(self.service.call(req)),
120            Err(e) => Either::Right(async move { MethodResponse::error(req.id(), e) }),
121        }
122    }
123
124    fn notification<'a>(
125        &self,
126        n: Notification<'a>,
127    ) -> impl Future<Output = Self::NotificationResponse> + Send + 'a {
128        match self.authorize(n.method_name()) {
129            Ok(()) => Either::Left(self.service.notification(n)),
130            Err(e) => Either::Right(async move { MethodResponse::error(Id::Null, e) }),
131        }
132    }
133
134    fn batch<'a>(&self, batch: Batch<'a>) -> impl Future<Output = Self::BatchResponse> + Send + 'a {
135        let entries = batch
136            .into_iter()
137            .filter_map(|entry| match entry {
138                Ok(BatchEntry::Call(req)) => Some(match self.authorize(req.method_name()) {
139                    Ok(()) => Ok(BatchEntry::Call(req)),
140                    Err(e) => Err(BatchEntryErr::new(req.id(), e)),
141                }),
142                Ok(BatchEntry::Notification(n)) => match self.authorize(n.method_name()) {
143                    Ok(_) => Some(Ok(BatchEntry::Notification(n))),
144                    Err(_) => None,
145                },
146                Err(err) => Some(Err(err)),
147            })
148            .collect_vec();
149        self.service.batch(Batch::from(entries))
150    }
151}
152
153/// Verify JWT Token and return the token's permissions.
154fn auth_verify(token: &str, keystore: &RwLock<KeyStore>) -> anyhow::Result<Vec<String>> {
155    let key_info = keystore.read().get(JWT_IDENTIFIER)?;
156    Ok(verify_token(token, key_info.private_key())?)
157}
158
159fn check_permissions(
160    keystore: &RwLock<KeyStore>,
161    auth_header: Option<&HeaderValue>,
162    method: &str,
163) -> Result<bool, ErrorCode> {
164    let claims = match auth_header {
165        Some(token) => {
166            let token = token
167                .to_str()
168                .map_err(|_| ErrorCode::ParseError)?
169                .trim_start_matches("Bearer ");
170
171            debug!("JWT from HTTP Header: {}", token);
172
173            auth_verify(token, keystore).map_err(|_| ErrorCode::InvalidRequest)?
174        }
175        // If no token is passed, assume read behavior
176        None => vec!["read".to_owned()],
177    };
178    debug!("Decoded JWT Claims: {}", claims.join(","));
179
180    match METHOD_NAME2REQUIRED_PERMISSION.get(&method) {
181        Some(required_by_method) => Ok(is_allowed(*required_by_method, &claims)),
182        None => Err(ErrorCode::MethodNotFound),
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use self::chain::ChainHead;
189    use super::*;
190    use crate::rpc::wallet;
191    use chrono::Duration;
192
193    #[test]
194    fn check_permissions_no_header() {
195        let keystore = Arc::new(RwLock::new(
196            KeyStore::new(crate::KeyStoreConfig::Memory).unwrap(),
197        ));
198
199        let res = check_permissions(&keystore, None, ChainHead::NAME);
200        assert_eq!(res, Ok(true));
201
202        let res = check_permissions(&keystore, None, "Cthulhu.InvokeElderGods");
203        assert_eq!(res.unwrap_err(), ErrorCode::MethodNotFound);
204
205        let res = check_permissions(&keystore, None, wallet::WalletNew::NAME);
206        assert_eq!(res, Ok(false));
207    }
208
209    #[test]
210    fn check_permissions_invalid_header() {
211        let keystore = Arc::new(RwLock::new(
212            KeyStore::new(crate::KeyStoreConfig::Memory).unwrap(),
213        ));
214
215        let auth_header = HeaderValue::from_static("Bearer Azathoth");
216        let res = check_permissions(&keystore, Some(&auth_header), ChainHead::NAME);
217        assert_eq!(res.unwrap_err(), ErrorCode::InvalidRequest);
218
219        let auth_header = HeaderValue::from_static("Cthulhu");
220        let res = check_permissions(&keystore, Some(&auth_header), ChainHead::NAME);
221        assert_eq!(res.unwrap_err(), ErrorCode::InvalidRequest);
222    }
223
224    #[test]
225    fn check_permissions_valid_header() {
226        use crate::auth::*;
227        let keystore = Arc::new(RwLock::new(
228            KeyStore::new(crate::KeyStoreConfig::Memory).unwrap(),
229        ));
230
231        // generate a key and store it in the keystore
232        let key_info = generate_priv_key();
233        keystore
234            .write()
235            .put(JWT_IDENTIFIER, key_info.clone())
236            .unwrap();
237        let token_exp = Duration::hours(1);
238        let token = create_token(
239            ADMIN.iter().map(ToString::to_string).collect(),
240            key_info.private_key(),
241            token_exp,
242        )
243        .unwrap();
244
245        // Should work with the `Bearer` prefix
246        let auth_header = HeaderValue::from_str(&format!("Bearer {token}")).unwrap();
247        let res = check_permissions(&keystore, Some(&auth_header), ChainHead::NAME);
248        assert_eq!(res, Ok(true));
249
250        let res = check_permissions(&keystore, Some(&auth_header), wallet::WalletNew::NAME);
251        assert_eq!(res, Ok(true));
252
253        // Should work without the `Bearer` prefix
254        let auth_header = HeaderValue::from_str(&token).unwrap();
255        let res = check_permissions(&keystore, Some(&auth_header), wallet::WalletNew::NAME);
256        assert_eq!(res, Ok(true));
257    }
258}