siderite_core/security/
http_auth.rs1use super::{
4 NOT_AUTHENTICATED, authorization_credentials, bearer_token, document_scheme, unauthorized,
5};
6use crate::error::ApiError;
7use crate::extract::FromRequestParts;
8use base64::Engine as _;
9use base64::engine::general_purpose::STANDARD;
10use http::request::Parts;
11use serde_json::json;
12use siderite_openapi::{Operation, SchemaRegistry};
13use std::fmt;
14
15const BASIC_CHALLENGE: &str = r#"Basic realm="api""#;
16
17#[derive(Clone, PartialEq, Eq)]
23pub struct HttpBearer {
24 pub token: String,
26}
27
28impl fmt::Debug for HttpBearer {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 f.debug_struct("HttpBearer")
31 .field("token", &"[REDACTED]")
32 .finish()
33 }
34}
35
36impl FromRequestParts for HttpBearer {
37 async fn from_request_parts(parts: &mut Parts) -> Result<Self, ApiError> {
38 bearer_token(parts).map(|token| Self { token })
39 }
40
41 fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
42 document_scheme(
43 op,
44 registry,
45 "HTTPBearer",
46 json!({"type": "http", "scheme": "bearer"}),
47 &[],
48 );
49 }
50}
51
52#[derive(Clone, PartialEq, Eq)]
59pub struct HttpBasic {
60 pub username: String,
62 pub password: String,
64}
65
66impl fmt::Debug for HttpBasic {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.debug_struct("HttpBasic")
69 .field("username", &self.username)
70 .field("password", &"[REDACTED]")
71 .finish()
72 }
73}
74
75fn decode_basic(encoded: &str) -> Option<HttpBasic> {
77 let bytes = STANDARD.decode(encoded.trim()).ok()?;
78 let text = String::from_utf8(bytes).ok()?;
79 let (username, password) = text.split_once(':')?;
80 Some(HttpBasic {
81 username: username.to_owned(),
82 password: password.to_owned(),
83 })
84}
85
86impl FromRequestParts for HttpBasic {
87 async fn from_request_parts(parts: &mut Parts) -> Result<Self, ApiError> {
88 let encoded = authorization_credentials(parts, "Basic")
89 .ok_or_else(|| unauthorized(BASIC_CHALLENGE, NOT_AUTHENTICATED).absent())?;
90 decode_basic(encoded)
91 .ok_or_else(|| unauthorized(BASIC_CHALLENGE, "Invalid basic credentials."))
92 }
93
94 fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
95 document_scheme(
96 op,
97 registry,
98 "HTTPBasic",
99 json!({"type": "http", "scheme": "basic"}),
100 &[],
101 );
102 }
103}
104
105#[cfg(test)]
106#[allow(clippy::unwrap_used)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn decodes_basic_credentials() {
112 let user = decode_basic(&STANDARD.encode("ann:s3:cret")).unwrap();
113 assert_eq!(user.username, "ann");
114 assert_eq!(user.password, "s3:cret");
115 }
116
117 #[test]
118 fn rejects_malformed_basic_credentials() {
119 assert!(decode_basic("!!!not-base64").is_none());
120 assert!(decode_basic(&STANDARD.encode("no-colon")).is_none());
121 assert!(decode_basic(&STANDARD.encode([0xff, b':', 0xfe])).is_none());
122 }
123
124 #[test]
125 fn debug_output_redacts_secrets() {
126 let basic = HttpBasic {
127 username: "ann".into(),
128 password: "hunter2".into(),
129 };
130 let bearer = HttpBearer {
131 token: "tok-123".into(),
132 };
133 let text = format!("{basic:?} {bearer:?}");
134 assert!(!text.contains("hunter2") && !text.contains("tok-123"));
135 }
136}