1use http_body_util::BodyExt;
2use std::{
3 convert::Infallible,
4 task::{Context, Poll},
5};
6
7use axum::{body::Body, extract::Request, response::Response};
8use futures_util::future::BoxFuture;
9use hex;
10use hmac::{Hmac, Mac};
11use sha2::Sha256;
12use tower::{Layer, Service};
13
14#[derive(Clone)]
15pub struct SlackAuthConfig {
16 pub version_number: String,
17 pub slack_signing_secret: String,
18}
19
20#[derive(Clone)]
21pub struct SlackAuthLayer {
22 config: SlackAuthConfig,
23}
24
25impl SlackAuthLayer {
26 #[must_use]
27 pub const fn new(config: SlackAuthConfig) -> Self {
28 Self { config }
29 }
30}
31
32impl<S> Layer<S> for SlackAuthLayer {
33 type Service = SlackAuthService<S>;
34
35 fn layer(&self, inner: S) -> Self::Service {
36 Self::Service {
37 inner,
38 config: self.config.clone(),
39 }
40 }
41}
42
43#[derive(Clone)]
44pub struct SlackAuthService<S> {
45 inner: S,
46 config: SlackAuthConfig,
47}
48
49impl<S> Service<Request<Body>> for SlackAuthService<S>
50where
51 S: Service<Request<Body>, Response = Response<Body>, Error = Infallible>
52 + Clone
53 + Send
54 + 'static,
55 S::Future: Send + 'static,
56{
57 type Response = S::Response;
58 type Error = S::Error;
59 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
60
61 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
62 self.inner.poll_ready(cx)
63 }
64
65 fn call(&mut self, req: Request<Body>) -> Self::Future {
66 let clone = self.inner.clone();
67 let mut inner = std::mem::replace(&mut self.inner, clone);
68 let clone = self.config.clone();
69 let config = std::mem::replace(&mut self.config, clone);
70 Box::pin(async move {
71 let deny = || {
72 let response = Response::builder()
73 .status(401)
74 .body(Body::empty())
75 .expect("Building an empty response should not fail.");
76 Ok(response)
77 };
78
79 let (parts, body) = req.into_parts();
80 let bytes = match body.collect().await {
81 Ok(bytes) => bytes.to_bytes(),
82 Err(_) => return deny(),
83 };
84 let request_body = std::str::from_utf8(&bytes).expect(
85 "Since we are collecting the body before into bytes, this should not fail.",
86 );
87 let slack_signature = match parts.headers.get("x-slack-signature") {
88 Some(signature) => match signature.to_str() {
89 Ok(signature) => signature,
90 Err(_) => return deny(),
91 },
92 None => return deny(),
93 };
94 let Some(slack_request_timestamp) = parts.headers.get("x-slack-request-timestamp")
95 else {
96 return deny();
97 };
98 let slack_request_timestamp = slack_request_timestamp
99 .to_str()
100 .unwrap_or("")
101 .parse::<i64>()
102 .unwrap_or(0);
103 let Some(parsed_slack_request_timestamp) =
104 chrono::DateTime::from_timestamp(slack_request_timestamp, 0)
105 else {
106 return deny();
107 };
108 if chrono::offset::Utc::now()
109 .signed_duration_since(parsed_slack_request_timestamp)
110 .num_seconds()
111 > 60 * 5
112 {
113 return deny();
114 }
115 let signer =
116 SecretSigner::new(config, request_body.to_string(), slack_request_timestamp);
117 let generated_hash = match signer.sign() {
118 Ok(hash) => hash,
119 Err(_) => return deny(),
120 };
121 if generated_hash != slack_signature {
122 return deny();
123 }
124 let req = Request::from_parts(parts, Body::from(bytes));
125 inner.call(req).await
126 })
127 }
128}
129
130pub struct SecretSigner {
131 config: SlackAuthConfig,
132 request_body: String,
133 timestamp: i64,
134}
135
136impl SecretSigner {
137 #[must_use]
138 pub const fn new(config: SlackAuthConfig, request_body: String, timestamp: i64) -> Self {
139 Self {
140 config,
141 request_body,
142 timestamp,
143 }
144 }
145
146 pub fn sign(&self) -> Result<String, hmac::digest::InvalidLength> {
147 let base_string = format!(
148 "{version_number}:{timestamp}:{request_body}",
149 version_number = self.config.version_number,
150 timestamp = self.timestamp,
151 request_body = self.request_body
152 );
153 let hash = self.hmac_signature(&base_string)?;
154 Ok(format!(
155 "{version_number}={hash}",
156 version_number = self.config.version_number,
157 hash = hash
158 ))
159 }
160
161 fn hmac_signature(&self, msg: &str) -> Result<String, hmac::digest::InvalidLength> {
162 type HmacSha256 = Hmac<Sha256>;
163
164 let mut mac = HmacSha256::new_from_slice(self.config.slack_signing_secret.as_bytes())?;
165 mac.update(msg.as_bytes());
166 let code_bytes = mac.finalize().into_bytes();
167 Ok(hex::encode(code_bytes))
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use axum::body::Body;
175 use axum::http::{Request, StatusCode};
176 use axum::response::Response;
177 use tokio;
178 use tower::util::service_fn;
179 use tower::ServiceExt;
180
181 fn create_test_service() -> (
182 SlackAuthConfig,
183 impl Service<Request<Body>, Response = Response<Body>, Error = Infallible>,
184 ) {
185 let config = SlackAuthConfig {
186 version_number: "v0".to_string(),
187 slack_signing_secret: "8f742231b10e8888abcd99yyyzzz85a5".to_string(),
188 };
189 let layer = SlackAuthLayer::new(config.clone());
190 let service = layer.layer(service_fn(|_req| async {
191 Ok::<_, Infallible>(Response::new(Body::from("OK")))
192 }));
193 (config, service)
194 }
195
196 fn create_request_body() -> &'static str {
197 concat!(
198 "token=xyzz0WbapA4vBCDEFasx0q6G",
199 "&team_id=T1DC2JH3J",
200 "&team_domain=testteamnow",
201 "&channel_id=G8PSS9T3V",
202 "&channel_name=foobar",
203 "&user_id=U2CERLKJA",
204 "&user_name=roadrunner",
205 "&command=%2Fwebhook-collect",
206 "&text=",
207 "&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN",
208 "&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c",
209 )
210 }
211
212 #[test]
213 fn sign() {
214 let config = SlackAuthConfig {
215 version_number: "v0".to_string(),
216 slack_signing_secret: "8f742231b10e8888abcd99yyyzzz85a5".to_string(),
217 };
218 let request_body = create_request_body();
219 let signer = SecretSigner::new(config, request_body.to_string(), 1531420618);
220 let hash = signer.sign().unwrap();
221
222 assert_eq!(
223 hash,
224 "v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503"
225 );
226 }
227
228 #[tokio::test]
229 async fn valid_request() {
230 let (config, service) = create_test_service();
231 let request_body = create_request_body();
232 let timestamp = chrono::Utc::now().timestamp().to_string();
233 let signer = SecretSigner::new(
234 config.clone(),
235 request_body.to_string(),
236 timestamp.parse().unwrap(),
237 );
238 let signature = signer.sign().unwrap();
239
240 let request = Request::builder()
241 .header("x-slack-signature", signature)
242 .header("x-slack-request-timestamp", timestamp)
243 .body(Body::from(request_body))
244 .unwrap();
245
246 let response = service.oneshot(request).await.unwrap();
247 assert_eq!(response.status(), StatusCode::OK);
248 }
249
250 #[tokio::test]
251 async fn missing_signature_header() {
252 let (_, service) = create_test_service();
253 let request_body = create_request_body();
254 let timestamp = chrono::Utc::now().timestamp().to_string();
255
256 let request = Request::builder()
257 .header("x-slack-request-timestamp", timestamp)
258 .body(Body::from(request_body))
259 .unwrap();
260
261 let response = service.oneshot(request).await.unwrap();
262 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
263 }
264
265 #[tokio::test]
266 async fn invalid_signature_header() {
267 let (_, service) = create_test_service();
268 let request_body = create_request_body();
269 let timestamp = chrono::Utc::now().timestamp().to_string();
270 let signature = "invalid_signature";
271
272 let request = Request::builder()
273 .header("x-slack-signature", signature)
274 .header("x-slack-request-timestamp", timestamp)
275 .body(Body::from(request_body))
276 .unwrap();
277
278 let response = service.oneshot(request).await.unwrap();
279 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
280 }
281
282 #[tokio::test]
283 async fn missing_timestamp_header() {
284 let (config, service) = create_test_service();
285 let request_body = create_request_body();
286 let signer = SecretSigner::new(
287 config.clone(),
288 request_body.to_string(),
289 chrono::Utc::now().timestamp(),
290 );
291 let signature = signer.sign().unwrap();
292
293 let request = Request::builder()
294 .header("x-slack-signature", signature)
295 .body(Body::from(request_body))
296 .unwrap();
297
298 let response = service.oneshot(request).await.unwrap();
299 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
300 }
301
302 #[tokio::test]
303 async fn invalid_timestamp_header() {
304 let (config, service) = create_test_service();
305 let request_body = create_request_body();
306 let timestamp = "invalid_timestamp";
307 let signer = SecretSigner::new(
308 config.clone(),
309 request_body.to_string(),
310 chrono::Utc::now().timestamp(),
311 );
312 let signature = signer.sign().unwrap();
313
314 let request = Request::builder()
315 .header("x-slack-signature", signature)
316 .header("x-slack-request-timestamp", timestamp)
317 .body(Body::from(request_body))
318 .unwrap();
319
320 let response = service.oneshot(request).await.unwrap();
321 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
322 }
323
324 #[tokio::test]
325 async fn expired_timestamp() {
326 let (config, service) = create_test_service();
327 let request_body = create_request_body();
328 let timestamp = (chrono::Utc::now().timestamp() - 60 * 6).to_string();
329 let signer = SecretSigner::new(
330 config.clone(),
331 request_body.to_string(),
332 timestamp.parse().unwrap(),
333 );
334 let signature = signer.sign().unwrap();
335
336 let request = Request::builder()
337 .header("x-slack-signature", signature)
338 .header("x-slack-request-timestamp", timestamp)
339 .body(Body::from(request_body))
340 .unwrap();
341
342 let response = service.oneshot(request).await.unwrap();
343 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
344 }
345
346 #[tokio::test]
347 async fn mismatched_signature() {
348 let (_, service) = create_test_service();
349 let request_body = create_request_body();
350 let timestamp = chrono::Utc::now().timestamp().to_string();
351 let signature = "v0=some_invalid_signature";
352
353 let request = Request::builder()
354 .header("x-slack-signature", signature)
355 .header("x-slack-request-timestamp", timestamp)
356 .body(Body::from(request_body))
357 .unwrap();
358
359 let response = service.oneshot(request).await.unwrap();
360 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
361 }
362}