Skip to main content

slack_auth_middleware/
middleware.rs

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}