1use alibi_core::utils::cookie_utils::sign_cookie_value;
4use alibi_core::{
5 AuthContext, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute, AuthSchema,
6 BeforeRequestAction,
7};
8use async_trait::async_trait;
9use base64::{
10 Engine, alphabet,
11 engine::{GeneralPurpose, GeneralPurposeConfig},
12};
13use hmac::{Hmac, KeyInit, Mac};
14use indexmap::IndexMap;
15use sha2::Sha256;
16
17#[derive(Clone, Debug, Default)]
19pub struct BearerConfig {
20 pub require_signature: bool,
22}
23
24#[derive(Clone, Debug, Default)]
25pub struct BearerPlugin {
26 config: BearerConfig,
27}
28
29impl BearerPlugin {
30 #[must_use]
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 #[must_use]
36 pub const fn with_config(config: BearerConfig) -> Self {
37 Self { config }
38 }
39
40 fn signed_token(&self, authorization: &str, secret: &str) -> Option<String> {
41 let (scheme, token) = authorization.split_once(' ')?;
42 if !scheme.eq_ignore_ascii_case("bearer") {
43 return None;
44 }
45 let token = token
46 .trim_matches(|ch: char| (ch.is_whitespace() && ch != '\u{85}') || ch == '\u{feff}');
47 if token.is_empty() {
48 return None;
49 }
50 let decoded = if token.contains('.') {
51 try_decode(token)
52 } else {
53 if self.config.require_signature {
54 return None;
55 }
56 try_decode(&sign_cookie_value(token, secret))
57 };
58 let mut components = decoded.split('.');
59 let payload = components.next()?;
60 let signature = components.next()?;
61 let selected = if signature.contains(['-', '_']) {
65 &alphabet::URL_SAFE
66 } else {
67 &alphabet::STANDARD
68 };
69 let engine = GeneralPurpose::new(
70 selected,
71 GeneralPurposeConfig::new()
72 .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent)
73 .with_decode_allow_trailing_bits(true),
74 );
75 let signature = engine.decode(signature.split('=').next()?).ok()?;
76 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).ok()?;
77 mac.update(payload.as_bytes());
78 mac.verify_slice(&signature).ok()?;
79 Some(decoded)
80 }
81}
82
83fn try_decode(value: &str) -> String {
84 let mut bytes = value.bytes();
87 while let Some(byte) = bytes.next() {
88 if byte == b'%'
89 && !(bytes.next().is_some_and(|byte| byte.is_ascii_hexdigit())
90 && bytes.next().is_some_and(|byte| byte.is_ascii_hexdigit()))
91 {
92 return value.to_owned();
93 }
94 }
95 urlencoding::decode(value).map_or_else(|_| value.to_owned(), std::borrow::Cow::into_owned)
96}
97
98fn encode_component(value: &str) -> String {
99 urlencoding::encode(value)
102 .replace("%21", "!")
103 .replace("%27", "'")
104 .replace("%28", "(")
105 .replace("%29", ")")
106 .replace("%2A", "*")
107}
108
109fn replace_cookie(header: &str, name: &str, value: &str) -> String {
110 let mut cookies = IndexMap::<String, String>::new();
111 for item in header.split(';') {
112 let Some((key, value)) = item.split_once('=') else {
113 continue;
114 };
115 let key = key.trim_matches([' ', '\t']);
116 let value = value.trim_matches([' ', '\t']);
117 let value = value
118 .strip_prefix('"')
119 .and_then(|v| v.strip_suffix('"'))
120 .unwrap_or(value);
121 if key.is_empty()
122 || !key
123 .bytes()
124 .all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b))
125 || !value
126 .bytes()
127 .all(|b| matches!(b, 0x20..=0x21 | 0x23..=0x3a | 0x3c..=0x5b | 0x5d..=0x7e))
128 {
129 continue;
130 }
131 _ = cookies.insert(key.to_owned(), try_decode(value));
132 }
133 _ = cookies.insert(name.to_owned(), value.to_owned());
134 cookies
135 .into_iter()
136 .map(|(key, value)| format!("{key}={}", encode_component(&value)))
137 .collect::<Vec<_>>()
138 .join("; ")
139}
140
141#[async_trait]
142impl<S: AuthSchema> AuthPlugin<S> for BearerPlugin {
143 route_openapi_metadata!(S);
144
145 fn name(&self) -> &'static str {
146 "bearer"
147 }
148 fn routes(&self) -> Vec<AuthRoute> {
149 Vec::new()
150 }
151
152 async fn before_request(
153 &self,
154 req: &AuthRequest,
155 ctx: &AuthContext<S>,
156 ) -> AuthResult<Option<BeforeRequestAction>> {
157 let Some(authorization) = req
158 .headers
159 .iter()
160 .find_map(|(key, value)| key.eq_ignore_ascii_case("authorization").then_some(value))
161 else {
162 return Ok(None);
163 };
164 let Some(token) = self.signed_token(authorization, ctx.config.current_secret()) else {
165 return Ok(None);
166 };
167 let existing = req
168 .headers
169 .iter()
170 .find_map(|(key, value)| key.eq_ignore_ascii_case("cookie").then_some(value.as_str()))
171 .unwrap_or("");
172 let cookie = replace_cookie(existing, &ctx.config.session.cookie_name, &token);
173 let mut headers = req.headers.clone();
174 headers.retain(|key, _| !key.eq_ignore_ascii_case("cookie"));
175 _ = headers.insert("cookie".to_owned(), cookie);
176 Ok(Some(BeforeRequestAction::ReplaceHeaders { headers }))
177 }
178
179 async fn on_request(
180 &self,
181 _req: &AuthRequest,
182 _ctx: &AuthContext<S>,
183 ) -> AuthResult<Option<AuthResponse>> {
184 Ok(None)
185 }
186
187 async fn after_request(
188 &self,
189 _req: &AuthRequest,
190 ctx: &AuthContext<S>,
191 mut response: AuthResponse,
192 ) -> AuthResult<AuthResponse> {
193 let token = response
194 .headers
195 .get_all("set-cookie")
196 .filter_map(|header| cookie::Cookie::parse(header.as_str()).ok())
197 .filter(|cookie| cookie.name() == ctx.config.session.cookie_name)
198 .last();
199 if let Some(cookie) = token.filter(|cookie| {
200 !cookie.value().is_empty()
201 && cookie.max_age().is_none_or(|age| age.whole_seconds() != 0)
202 }) {
203 let token = try_decode(cookie.value());
204 let mut exposed = Vec::new();
205 for name in response
206 .headers
207 .get("access-control-expose-headers")
208 .map_or("", String::as_str)
209 .split(',')
210 .map(str::trim)
211 .filter(|v| !v.is_empty())
212 {
213 if !exposed.contains(&name) {
214 exposed.push(name);
215 }
216 }
217 if !exposed.contains(&"set-auth-token") {
218 exposed.push("set-auth-token");
219 }
220 let exposed = exposed.join(", ");
221 _ = response.headers.insert("set-auth-token", token);
222 _ = response
223 .headers
224 .insert("access-control-expose-headers", exposed);
225 }
226 Ok(response)
227 }
228}