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#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct AuthContext {
13 claims: TokenClaims,
14}
15
16impl AuthContext {
17 #[must_use]
19 pub const fn new(claims: TokenClaims) -> Self {
20 Self { claims }
21 }
22
23 #[must_use]
25 pub const fn claims(&self) -> &TokenClaims {
26 &self.claims
27 }
28}
29
30#[derive(Clone)]
32pub struct ServerAuth {
33 provider: std::sync::Arc<dyn AuthProvider>,
34}
35
36impl ServerAuth {
37 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 #[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 #[must_use]
60 pub fn provider(&self) -> &dyn AuthProvider {
61 self.provider.as_ref()
62 }
63
64 #[must_use]
66 pub fn provider_arc(&self) -> std::sync::Arc<dyn AuthProvider> {
67 self.provider.clone()
68 }
69
70 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}