forked from
baileytownsend.dev/pds-gatekeeper
Microservice to bring 2FA to self hosted PDSes
1use crate::helpers::json_error_response;
2use axum::extract::Request;
3use axum::http::header::AUTHORIZATION;
4use axum::http::{HeaderMap, StatusCode};
5use axum::middleware::Next;
6use axum::response::IntoResponse;
7use jwt_compact::alg::{Hs256, Hs256Key};
8use jwt_compact::{AlgorithmExt, Claims, Token, UntrustedToken, ValidationError};
9use serde::{Deserialize, Serialize};
10use std::env;
11use tracing::log;
12
13#[derive(Clone, Debug)]
14pub struct Did(pub Option<String>);
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum AuthScheme {
18 Bearer,
19 DPoP,
20}
21
22#[derive(Serialize, Deserialize)]
23pub struct TokenClaims {
24 pub sub: String,
25}
26
27pub async fn extract_did(mut req: Request, next: Next) -> impl IntoResponse {
28 let auth = extract_auth(req.headers());
29
30 match auth {
31 Ok(auth_opt) => {
32 match auth_opt {
33 None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "")
34 .expect("Error creating an error response"),
35 Some((scheme, token_str)) => {
36 // For Bearer, validate JWT and extract DID from `sub`.
37 // For DPoP, we currently only pass through and do not validate here; insert None DID.
38 match scheme {
39 AuthScheme::Bearer => {
40 let token = UntrustedToken::new(&token_str);
41 if token.is_err() {
42 return json_error_response(
43 StatusCode::BAD_REQUEST,
44 "TokenRequired",
45 "",
46 )
47 .expect("Error creating an error response");
48 }
49 let parsed_token = token.expect("Already checked for error");
50 let claims: Result<Claims<TokenClaims>, ValidationError> =
51 parsed_token.deserialize_claims_unchecked();
52 if claims.is_err() {
53 return json_error_response(
54 StatusCode::BAD_REQUEST,
55 "TokenRequired",
56 "",
57 )
58 .expect("Error creating an error response");
59 }
60
61 let key = Hs256Key::new(
62 env::var("PDS_JWT_SECRET")
63 .expect("PDS_JWT_SECRET not set in the pds.env"),
64 );
65 let token: Result<Token<TokenClaims>, ValidationError> =
66 Hs256.validator(&key).validate(&parsed_token);
67 if token.is_err() {
68 return json_error_response(
69 StatusCode::BAD_REQUEST,
70 "InvalidToken",
71 "",
72 )
73 .expect("Error creating an error response");
74 }
75 let token = token.expect("Already checked for error,");
76 // Not going to worry about expiration since it still goes to the PDS
77 req.extensions_mut()
78 .insert(Did(Some(token.claims().custom.sub.clone())));
79 }
80 AuthScheme::DPoP => {
81 //Not going to worry about oauth email update for now, just always forward to the PDS
82 req.extensions_mut().insert(Did(None));
83 }
84 }
85
86 next.run(req).await
87 }
88 }
89 }
90 Err(err) => {
91 log::error!("Error extracting token: {err}");
92 json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "")
93 .expect("Error creating an error response")
94 }
95 }
96}
97
98fn extract_auth(headers: &HeaderMap) -> Result<Option<(AuthScheme, String)>, String> {
99 match headers.get(axum::http::header::AUTHORIZATION) {
100 None => Ok(None),
101 Some(hv) => {
102 match hv.to_str() {
103 Err(_) => Err("Authorization header is not valid".into()),
104 Ok(s) => {
105 // Accept forms like: "Bearer <token>" or "DPoP <token>" (case-sensitive for the scheme here)
106 let mut parts = s.splitn(2, ' ');
107 match (parts.next(), parts.next()) {
108 (Some("Bearer"), Some(tok)) if !tok.is_empty() =>
109 Ok(Some((AuthScheme::Bearer, tok.to_string()))),
110 (Some("DPoP"), Some(tok)) if !tok.is_empty() =>
111 Ok(Some((AuthScheme::DPoP, tok.to_string()))),
112 _ => Err("Authorization header must be in format 'Bearer <token>' or 'DPoP <token>'".into()),
113 }
114 }
115 }
116 }
117 }
118}