Redeem website login keycards over SASL
All checks were successful
CI / check (push) Successful in 3m52s

A member already authenticated on tchatou.fr connects with a single-use kc_…
keycard instead of their password (the m_apiauth module that used to validate it
was retired in the Anope->echo cutover, so these logins had started failing as
"wrong password"). The SASL PLAIN handler now recognises a kc_-prefixed
credential and emits NetAction::DeferKeycard; the link layer redeems it off the
engine lock — a localhost round-trip to Django's login-token endpoint via
keycard::redeem — and completes the login exactly like a password auth. Config:
[keycard] url + api_key; absent, kc_ credentials are simply not honoured.
This commit is contained in:
Jean Chevronnet 2026-07-16 22:27:59 +00:00
parent bad17a184e
commit f2fd80694d
No known key found for this signature in database
9 changed files with 359 additions and 4 deletions

View file

@ -71,7 +71,7 @@ fn redact(line: &str) -> Cow<'_, str> {
// One uplink session: connect, handshake + burst, then translate lines forever.
// The engine is shared with the gossip layer, so it is locked per operation and
// never held across the registration key-stretching await.
pub async fn run(mut proto: Box<dyn Protocol>, engine: Arc<Mutex<Engine>>, addr: &str, mut irc_rx: mpsc::UnboundedReceiver<NetAction>, email: Option<crate::config::Email>) -> Result<()> {
pub async fn run(mut proto: Box<dyn Protocol>, engine: Arc<Mutex<Engine>>, addr: &str, mut irc_rx: mpsc::UnboundedReceiver<NetAction>, email: Option<crate::config::Email>, keycard: Option<crate::config::Keycard>) -> Result<()> {
let stream = TcpStream::connect(addr).await?;
let (read, mut write) = stream.into_split();
let mut lines = BufReader::new(read).lines();
@ -131,6 +131,18 @@ pub async fn run(mut proto: Box<dyn Protocol>, engine: Arc<Mutex<Engine>>, addr:
.await?;
engine.lock().await.complete_authenticate(ok, then)
}
NetAction::DeferKeycard { token, account, then } => {
// Redeem a website login keycard off the reactor (a localhost
// HTTP round-trip), then finish the login under the lock — the
// completion is identical to a password auth once we know the
// outcome.
let kc = keycard.clone();
let ok = tokio::task::spawn_blocking(move || {
crate::keycard::redeem(kc.as_ref(), &account, &token)
})
.await?;
engine.lock().await.complete_authenticate(ok, then)
}
action => vec![action],
};
for act in outs {