Skip to main content

shardline_server/
auth.rs

1use axum::http::{HeaderMap, header::AUTHORIZATION};
2use shardline_protocol::{TokenClaims, TokenCodecError, TokenScope};
3use shardline_server_core::{AuthError, AuthProvider};
4use subtle::ConstantTimeEq;
5
6use crate::ServerError;
7
8const MAX_BEARER_TOKEN_BYTES: usize = 8192;
9
10/// Verified request authorization context.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct AuthContext {
13    claims: TokenClaims,
14}
15
16impl AuthContext {
17    /// Creates an authorization context from verified token claims.
18    #[must_use]
19    pub const fn new(claims: TokenClaims) -> Self {
20        Self { claims }
21    }
22
23    /// Returns the verified claims.
24    #[must_use]
25    pub const fn claims(&self) -> &TokenClaims {
26        &self.claims
27    }
28}
29
30/// Bearer-token verifier backed by a pluggable [`AuthProvider`].
31#[derive(Clone)]
32pub struct ServerAuth {
33    provider: std::sync::Arc<dyn AuthProvider>,
34}
35
36impl ServerAuth {
37    /// Creates a bearer-token verifier from a signing key using the local
38    /// Ed25519 provider.
39    ///
40    /// # Errors
41    ///
42    /// Returns [`ServerError`] when the signing key is invalid.
43    pub fn new(signing_key: &[u8]) -> Result<Self, ServerError> {
44        let provider = shardline_server_core::auth::LocalEd25519Provider::new(signing_key)?;
45        Ok(Self {
46            provider: std::sync::Arc::new(provider),
47        })
48    }
49
50    /// Creates a bearer-token verifier from a boxed [`AuthProvider`].
51    #[must_use]
52    pub fn from_provider(provider: Box<dyn AuthProvider>) -> Self {
53        Self {
54            provider: std::sync::Arc::from(provider),
55        }
56    }
57
58    /// Returns a reference to the underlying [`AuthProvider`].
59    #[must_use]
60    pub fn provider(&self) -> &dyn AuthProvider {
61        self.provider.as_ref()
62    }
63
64    /// Returns a clone of the underlying [`AuthProvider`] as an [`Arc`].
65    #[must_use]
66    pub fn provider_arc(&self) -> std::sync::Arc<dyn AuthProvider> {
67        self.provider.clone()
68    }
69
70    /// Validates the request token and required scope.
71    ///
72    /// # Errors
73    ///
74    /// Returns [`ServerError`] when the authorization header is missing, malformed, or
75    /// insufficient for the requested scope.
76    pub fn authorize(
77        &self,
78        headers: &HeaderMap,
79        required_scope: TokenScope,
80    ) -> Result<AuthContext, ServerError> {
81        let header = headers
82            .get(AUTHORIZATION)
83            .ok_or(ServerError::MissingAuthorization)?;
84        let header = header
85            .to_str()
86            .map_err(|_error| ServerError::InvalidAuthorizationHeader)?;
87        let token = parse_bearer_token(header)?;
88        let claims = self.provider.verify_token(token)?;
89        if !scope_allows(claims.scope(), required_scope) {
90            return Err(ServerError::InsufficientScope);
91        }
92
93        Ok(AuthContext::new(claims))
94    }
95}
96
97impl std::fmt::Debug for ServerAuth {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("ServerAuth")
100            .field("provider", &"<dyn AuthProvider>")
101            .finish()
102    }
103}
104
105fn parse_bearer_token(header: &str) -> Result<&str, ServerError> {
106    let Some(token) = header.strip_prefix("Bearer ") else {
107        return Err(ServerError::InvalidAuthorizationHeader);
108    };
109    if token.trim().is_empty() {
110        return Err(ServerError::InvalidAuthorizationHeader);
111    }
112    if token.len() > MAX_BEARER_TOKEN_BYTES {
113        return Err(ServerError::InvalidAuthorizationHeader);
114    }
115    if token.bytes().any(|byte| byte.is_ascii_whitespace()) {
116        return Err(ServerError::InvalidAuthorizationHeader);
117    }
118
119    Ok(token)
120}
121
122pub(crate) fn authorize_static_bearer_token(
123    headers: &HeaderMap,
124    expected_token: &[u8],
125) -> Result<(), ServerError> {
126    let header = headers
127        .get(AUTHORIZATION)
128        .ok_or(ServerError::MissingAuthorization)?;
129    let header = header
130        .to_str()
131        .map_err(|_error| ServerError::InvalidAuthorizationHeader)?;
132    let token = parse_bearer_token(header)?;
133    let actual = token.as_bytes();
134
135    use sha2::{Digest, Sha256};
136    let actual_hash = Sha256::digest(actual);
137    let expected_hash = Sha256::digest(expected_token);
138    if bool::from(actual_hash.ct_eq(&expected_hash)) {
139        return Ok(());
140    }
141
142    Err(ServerError::InvalidAuthorizationHeader)
143}
144
145const fn scope_allows(actual_scope: TokenScope, required_scope: TokenScope) -> bool {
146    match required_scope {
147        TokenScope::Read => actual_scope.allows_read(),
148        TokenScope::Write => actual_scope.allows_write(),
149    }
150}
151
152impl From<TokenCodecError> for ServerError {
153    fn from(error: TokenCodecError) -> Self {
154        Self::InvalidToken(error)
155    }
156}
157
158impl From<AuthError> for ServerError {
159    fn from(error: AuthError) -> Self {
160        match error {
161            AuthError::InvalidToken => Self::InvalidToken(TokenCodecError::InvalidFormat),
162            AuthError::ExpiredToken => Self::InvalidToken(TokenCodecError::Expired),
163            AuthError::InsufficientScope => Self::InsufficientScope,
164            AuthError::ProviderError(_msg) => Self::InvalidToken(TokenCodecError::InvalidFormat),
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use axum::http::{
172        HeaderMap,
173        header::{AUTHORIZATION, HeaderValue},
174    };
175    use shardline_protocol::{
176        RepositoryProvider, RepositoryScope, TokenClaims, TokenScope, TokenSigner,
177    };
178
179    use super::{MAX_BEARER_TOKEN_BYTES, ServerAuth, authorize_static_bearer_token};
180    use crate::ServerError;
181
182    #[test]
183    fn server_auth_rejects_missing_header() {
184        let auth = ServerAuth::new(b"test-signing-key-32-bytes-long!!");
185        assert!(auth.is_ok());
186        let Ok(auth) = auth else {
187            return;
188        };
189
190        assert!(matches!(
191            auth.authorize(&HeaderMap::new(), TokenScope::Read),
192            Err(ServerError::MissingAuthorization)
193        ));
194    }
195
196    #[test]
197    fn server_auth_rejects_insufficient_scope() {
198        let auth = ServerAuth::new(b"test-signing-key-32-bytes-long!!");
199        assert!(auth.is_ok());
200        let Ok(auth) = auth else {
201            return;
202        };
203        let signer = TokenSigner::new(b"test-signing-key-32-bytes-long!!");
204        assert!(signer.is_ok());
205        let Ok(signer) = signer else {
206            return;
207        };
208        let repository =
209            RepositoryScope::new(RepositoryProvider::GitHub, "team", "assets", Some("main"));
210        assert!(repository.is_ok());
211        let Ok(repository) = repository else {
212            return;
213        };
214        let claims = TokenClaims::new(
215            "local",
216            "provider-user-1",
217            TokenScope::Read,
218            repository,
219            u64::MAX,
220        );
221        assert!(claims.is_ok());
222        let Ok(claims) = claims else {
223            return;
224        };
225        let token = signer.sign(&claims);
226        assert!(token.is_ok());
227        let Ok(token) = token else {
228            return;
229        };
230        let mut headers = HeaderMap::new();
231        let header_value = HeaderValue::from_str(&format!("Bearer {token}"));
232        assert!(header_value.is_ok());
233        let Ok(header_value) = header_value else {
234            return;
235        };
236        headers.insert(AUTHORIZATION, header_value);
237
238        assert!(matches!(
239            auth.authorize(&headers, TokenScope::Write),
240            Err(ServerError::InsufficientScope)
241        ));
242    }
243
244    #[test]
245    fn server_auth_rejects_oversized_bearer_token_before_decoding() {
246        let auth = ServerAuth::new(b"test-signing-key-32-bytes-long!!");
247        assert!(auth.is_ok());
248        let Ok(auth) = auth else {
249            return;
250        };
251        let token = "a".repeat(MAX_BEARER_TOKEN_BYTES + 1);
252        let mut headers = HeaderMap::new();
253        let header_value = HeaderValue::from_str(&format!("Bearer {token}"));
254        assert!(header_value.is_ok());
255        let Ok(header_value) = header_value else {
256            return;
257        };
258        headers.insert(AUTHORIZATION, header_value);
259
260        assert!(matches!(
261            auth.authorize(&headers, TokenScope::Read),
262            Err(ServerError::InvalidAuthorizationHeader)
263        ));
264    }
265
266    #[test]
267    fn server_auth_rejects_bearer_token_with_whitespace() {
268        let auth = ServerAuth::new(b"test-signing-key-32-bytes-long!!");
269        assert!(auth.is_ok());
270        let Ok(auth) = auth else {
271            return;
272        };
273        let mut headers = HeaderMap::new();
274        headers.insert(
275            AUTHORIZATION,
276            HeaderValue::from_static("Bearer abc.def ghi"),
277        );
278
279        assert!(matches!(
280            auth.authorize(&headers, TokenScope::Read),
281            Err(ServerError::InvalidAuthorizationHeader)
282        ));
283    }
284
285    #[test]
286    fn server_auth_accepts_valid_write_token() {
287        let auth = ServerAuth::new(b"test-signing-key-32-bytes-long!!");
288        assert!(auth.is_ok());
289        let Ok(auth) = auth else {
290            return;
291        };
292        let signer = TokenSigner::new(b"test-signing-key-32-bytes-long!!");
293        assert!(signer.is_ok());
294        let Ok(signer) = signer else {
295            return;
296        };
297        let repository =
298            RepositoryScope::new(RepositoryProvider::GitHub, "team", "assets", Some("main"));
299        assert!(repository.is_ok());
300        let Ok(repository) = repository else {
301            return;
302        };
303        let claims = TokenClaims::new(
304            "local",
305            "provider-user-1",
306            TokenScope::Write,
307            repository,
308            u64::MAX,
309        );
310        assert!(claims.is_ok());
311        let Ok(claims) = claims else {
312            return;
313        };
314        let token = signer.sign(&claims);
315        assert!(token.is_ok());
316        let Ok(token) = token else {
317            return;
318        };
319        let mut headers = HeaderMap::new();
320        let header_value = HeaderValue::from_str(&format!("Bearer {token}"));
321        assert!(header_value.is_ok());
322        let Ok(header_value) = header_value else {
323            return;
324        };
325        headers.insert(AUTHORIZATION, header_value);
326
327        let context = auth.authorize(&headers, TokenScope::Read);
328
329        assert!(context.is_ok());
330        let Ok(context) = context else {
331            return;
332        };
333        assert_eq!(context.claims().subject(), "provider-user-1");
334        assert_eq!(context.claims().scope(), TokenScope::Write);
335    }
336
337    #[test]
338    fn static_bearer_token_rejects_missing_header() {
339        let result = authorize_static_bearer_token(&HeaderMap::new(), b"metrics-token");
340
341        assert!(matches!(result, Err(ServerError::MissingAuthorization)));
342    }
343
344    #[test]
345    fn static_bearer_token_rejects_wrong_value() {
346        let mut headers = HeaderMap::new();
347        headers.insert(
348            AUTHORIZATION,
349            HeaderValue::from_static("Bearer wrong-token"),
350        );
351
352        let result = authorize_static_bearer_token(&headers, b"metrics-token");
353
354        assert!(matches!(
355            result,
356            Err(ServerError::InvalidAuthorizationHeader)
357        ));
358    }
359
360    #[test]
361    fn static_bearer_token_accepts_matching_value() {
362        let mut headers = HeaderMap::new();
363        headers.insert(
364            AUTHORIZATION,
365            HeaderValue::from_static("Bearer metrics-token"),
366        );
367
368        let result = authorize_static_bearer_token(&headers, b"metrics-token");
369
370        assert!(result.is_ok());
371    }
372}