1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
use axum::{
Router,
error_handling::HandleErrorLayer,
http::StatusCode,
response::{Html, IntoResponse},
routing::get,
};
use axum_token_auth::{AuthConfig, SessionKey, TokenConfig};
async fn handle_auth_error(err: tower::BoxError) -> (StatusCode, &'static str) {
match err.downcast::<axum_token_auth::ValidationErrors>() {
Ok(err) => {
eprintln!(
"Validation error(s): {:?}",
err.errors().collect::<Vec<_>>()
);
(StatusCode::UNAUTHORIZED, "Request is not authorized")
}
Err(orig_err) => {
eprintln!("Unhandled internal error: {orig_err}");
(StatusCode::INTERNAL_SERVER_ERROR, "internal server error")
}
}
}
async fn user_handler(session_key: SessionKey) -> impl IntoResponse {
session_key.is_present();
Html(format!("hello {:?} <a href=\"/\">index</a>", session_key))
}
fn expand_unspecified(ip: std::net::IpAddr) -> anyhow::Result<Vec<std::net::IpAddr>> {
if ip.is_unspecified() {
// Get all interfaces if IP is unspecified.
Ok(if_addrs::get_if_addrs()?
.iter()
.filter_map(|x| {
let this_ip = x.addr.ip();
// Take only IP addresses from correct family.
if ip.is_ipv4() == this_ip.is_ipv4() {
Some(this_ip)
} else {
None
}
})
.collect())
} else {
Ok(vec![ip])
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// listen globally on port 3000
let http_server_addr = std::net::ToSocketAddrs::to_socket_addrs("0.0.0.0:3000")?
.next()
.unwrap();
println!("Listening at {http_server_addr}");
// A long lived encryption key which is used to encode and decode the
// cookies stored by the client. Typically this would be persisted across
// program invocations.
let persistent_secret = cookie::Key::generate();
// Setup our config. `AuthConfig` is `#[non_exhaustive]`, so we build it with
// `AuthConfig::new` and then set the fields we want rather than with a struct
// literal.
let token_config = TokenConfig::new("token");
let mut cfg = AuthConfig::new(persistent_secret);
cfg.token_config = Some(token_config.clone());
// Precompute the address(es) we will advertise.
let advertised_addrs: Vec<std::net::SocketAddr> = expand_unspecified(http_server_addr.ip())?
.into_iter()
.map(|ip| std::net::SocketAddr::new(ip, http_server_addr.port()))
.collect();
// Periodically mint a fresh self-expiring token and print it out-of-band.
//
// Because tokens are stateless signed values validated against
// `persistent_secret`, the running middleware needs no update to rotate
// them: it accepts any unexpired token and old ones expire on their own. We
// keep a clone of the secret solely to mint tokens via the free-standing
// `generate_token` (no need to hold on to the whole `AuthConfig`, which is
// consumed by `into_layer` below). (This rotation is impossible in 0.2.x,
// where the single accepted token is frozen into the layer at construction
// time.)
//
// Each token is signed with `persistent_secret` and carries its own expiry.
// Upon successful authentication with one, the middleware sets a signed
// cookie enabling subsequent authentications without any token.
let token_secret = cfg.persistent_secret.clone();
let token_name = token_config.name.clone();
tokio::spawn(async move {
// Tokens are valid for 5 minutes but rotated every 2, so a freshly
// printed token always overlaps the previous one (no gap with no valid
// token).
const TTL: std::time::Duration = std::time::Duration::from_secs(5 * 60);
const ROTATE_EVERY: std::time::Duration = std::time::Duration::from_secs(2 * 60);
loop {
let token = axum_token_auth::generate_token(&token_secret, TTL);
println!("\nNew access token (valid 5 min):");
for addr in &advertised_addrs {
println!(" * http://{addr}/?{token_name}={token}");
}
tokio::time::sleep(ROTATE_EVERY).await;
}
});
// Setup auth layer.
let auth_layer = cfg.into_layer();
// Build our application with a single route.
let app = Router::new()
.route(
"/",
get(|| async { Html("Hello, World! <a href=\"/user\">user</a>") }),
)
.route("/user", get(user_handler))
.layer(
tower::ServiceBuilder::new()
// `auth_layer` will produce an error if the request cannot be authorized
// so we must handle that.
.layer(HandleErrorLayer::new(handle_auth_error))
.layer(auth_layer),
);
// run our app
let listener = tokio::net::TcpListener::bind(http_server_addr).await?;
axum::serve(listener, app).await?;
Ok(())
}