Redeem website login keycards over SASL
All checks were successful
CI / check (push) Successful in 3m52s
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:
parent
bad17a184e
commit
f2fd80694d
9 changed files with 359 additions and 4 deletions
278
Cargo.lock
generated
278
Cargo.lock
generated
|
|
@ -255,6 +255,17 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "displaydoc"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dunce"
|
name = "dunce"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
|
@ -273,6 +284,7 @@ dependencies = [
|
||||||
"echo-botserv",
|
"echo-botserv",
|
||||||
"echo-chanfix",
|
"echo-chanfix",
|
||||||
"echo-chanserv",
|
"echo-chanserv",
|
||||||
|
"echo-debugserv",
|
||||||
"echo-diceserv",
|
"echo-diceserv",
|
||||||
"echo-example",
|
"echo-example",
|
||||||
"echo-groupserv",
|
"echo-groupserv",
|
||||||
|
|
@ -304,6 +316,7 @@ dependencies = [
|
||||||
"tonic-build",
|
"tonic-build",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"ureq",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -331,6 +344,13 @@ dependencies = [
|
||||||
"echo-api",
|
"echo-api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "echo-debugserv"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"echo-api",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "echo-diceserv"
|
name = "echo-diceserv"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|
@ -704,6 +724,109 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_collections"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"potential_utf",
|
||||||
|
"utf8_iter",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_locale_core"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"litemap",
|
||||||
|
"tinystr",
|
||||||
|
"writeable",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_normalizer"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||||
|
dependencies = [
|
||||||
|
"icu_collections",
|
||||||
|
"icu_normalizer_data",
|
||||||
|
"icu_properties",
|
||||||
|
"icu_provider",
|
||||||
|
"smallvec",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_normalizer_data"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_properties"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||||
|
dependencies = [
|
||||||
|
"icu_collections",
|
||||||
|
"icu_locale_core",
|
||||||
|
"icu_properties_data",
|
||||||
|
"icu_provider",
|
||||||
|
"zerotrie",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_properties_data"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_provider"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"icu_locale_core",
|
||||||
|
"writeable",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerotrie",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||||
|
dependencies = [
|
||||||
|
"idna_adapter",
|
||||||
|
"smallvec",
|
||||||
|
"utf8_iter",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna_adapter"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
|
||||||
|
dependencies = [
|
||||||
|
"icu_normalizer",
|
||||||
|
"icu_properties",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
|
|
@ -767,6 +890,12 @@ version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litemap"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.33"
|
version = "0.4.33"
|
||||||
|
|
@ -890,6 +1019,15 @@ version = "0.3.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "potential_utf"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||||
|
dependencies = [
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
|
|
@ -1336,6 +1474,12 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stable_deref_trait"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|
@ -1359,6 +1503,17 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "synstructure"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
|
|
@ -1381,6 +1536,16 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinystr"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.3"
|
version = "1.52.3"
|
||||||
|
|
@ -1663,6 +1828,36 @@ version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "2.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url"
|
||||||
|
version = "2.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"idna",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8_iter"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
@ -1787,6 +1982,35 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "writeable"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
||||||
|
dependencies = [
|
||||||
|
"stable_deref_trait",
|
||||||
|
"yoke-derive",
|
||||||
|
"zerofrom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke-derive"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.54"
|
version = "0.8.54"
|
||||||
|
|
@ -1807,12 +2031,66 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerofrom"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
||||||
|
dependencies = [
|
||||||
|
"zerofrom-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerofrom-derive"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerotrie"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerovec"
|
||||||
|
version = "0.11.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||||
|
dependencies = [
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerovec-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerovec-derive"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ axum = "0.7"
|
||||||
axum-server = { version = "0.7", features = ["tls-rustls"] }
|
axum-server = { version = "0.7", features = ["tls-rustls"] }
|
||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
ureq = { version = "2", default-features = false }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-build = "0.12"
|
tonic-build = "0.12"
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,11 @@ pub enum NetAction {
|
||||||
// fetches `verifier` cheaply, runs verify_plain off-thread, then calls
|
// fetches `verifier` cheaply, runs verify_plain off-thread, then calls
|
||||||
// Engine::complete_authenticate with the boolean result and `then`.
|
// Engine::complete_authenticate with the boolean result and `then`.
|
||||||
DeferAuthenticate { verifier: String, password: String, then: AuthThen },
|
DeferAuthenticate { verifier: String, password: String, then: AuthThen },
|
||||||
|
// Internal only: redeem a website login keycard (`kc_…`) off-thread. The
|
||||||
|
// credential isn't a password, so it can't be SCRAM-verified; the link layer
|
||||||
|
// redeems `token` for `account` against Django's localhost login-token
|
||||||
|
// endpoint, then calls Engine::complete_authenticate with the result + `then`.
|
||||||
|
DeferKeycard { token: String, account: String, then: AuthThen },
|
||||||
// Internal only: send an email (plaintext + optional HTML). The link layer
|
// Internal only: send an email (plaintext + optional HTML). The link layer
|
||||||
// pipes it to the configured mail command off-thread; never serialized.
|
// pipes it to the configured mail command off-thread; never serialized.
|
||||||
SendEmail { to: String, subject: String, text: String, html: Option<String> },
|
SendEmail { to: String, subject: String, text: String, html: Option<String> },
|
||||||
|
|
|
||||||
|
|
@ -396,7 +396,7 @@ impl Protocol for InspIrcd {
|
||||||
NetAction::Squit { target, reason } => vec![self.sourced(format!("SQUIT {} :{}", target, reason))],
|
NetAction::Squit { target, reason } => vec![self.sourced(format!("SQUIT {} :{}", target, reason))],
|
||||||
NetAction::Raw(s) => vec![s.clone()],
|
NetAction::Raw(s) => vec![s.clone()],
|
||||||
// Internal: the link layer handles these before serialization.
|
// Internal: the link layer handles these before serialization.
|
||||||
NetAction::DeferRegister { .. } | NetAction::DeferPassword { .. } | NetAction::DeferAuthenticate { .. } | NetAction::SendEmail { .. } | NetAction::Shutdown { .. } => vec![],
|
NetAction::DeferRegister { .. } | NetAction::DeferPassword { .. } | NetAction::DeferAuthenticate { .. } | NetAction::DeferKeycard { .. } | NetAction::SendEmail { .. } | NetAction::Shutdown { .. } => vec![],
|
||||||
};
|
};
|
||||||
// A trailing parameter can carry free-form text (message bodies, kick
|
// A trailing parameter can carry free-form text (message bodies, kick
|
||||||
// reasons, topics, metadata). Strip CR/LF/NUL at this single choke-point
|
// reasons, topics, metadata). Strip CR/LF/NUL at this single choke-point
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,20 @@ pub struct Config {
|
||||||
// and pushes accounts in; IRC can only authenticate.
|
// and pushes accounts in; IRC can only authenticate.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub auth: Option<Auth>,
|
pub auth: Option<Auth>,
|
||||||
|
// Website single-use keycards (passwordless web login). Absent = keycard
|
||||||
|
// credentials (`kc_…` over SASL) are not honoured.
|
||||||
|
#[serde(default)]
|
||||||
|
pub keycard: Option<Keycard>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redeeming a website login keycard: a member already authenticated on the
|
||||||
|
// website connects with a one-time `kc_…` token instead of their password. We
|
||||||
|
// hand it to Django's localhost login-token endpoint, which redeems it and
|
||||||
|
// confirms the account. `url` is that endpoint; `api_key` matches its X-API-Key.
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct Keycard {
|
||||||
|
pub url: String,
|
||||||
|
pub api_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Account-authority configuration.
|
// Account-authority configuration.
|
||||||
|
|
|
||||||
|
|
@ -59,13 +59,30 @@ impl Engine {
|
||||||
// Decode, then defer the verify off the lock (the login
|
// Decode, then defer the verify off the lock (the login
|
||||||
// finish lands in Engine::complete_authenticate).
|
// finish lands in Engine::complete_authenticate).
|
||||||
match decode_plain(&response) {
|
match decode_plain(&response) {
|
||||||
|
// A website login keycard (`kc_…`), not a password: it can't be
|
||||||
|
// SCRAM-verified, so redeem it off-lock (link.rs) against Django.
|
||||||
|
Some((authcid, passwd)) if passwd.starts_with("kc_") => {
|
||||||
|
let account = match self.db.resolve_account(&authcid) {
|
||||||
|
Some(a) => a.to_string(),
|
||||||
|
None => authcid,
|
||||||
|
};
|
||||||
|
vec![NetAction::DeferKeycard {
|
||||||
|
token: passwd,
|
||||||
|
account: account.clone(),
|
||||||
|
then: AuthThen::Sasl { agent: agent.clone(), client: client.clone(), account },
|
||||||
|
}]
|
||||||
|
}
|
||||||
Some((authcid, passwd)) => match self.scram_verifier(&authcid) {
|
Some((authcid, passwd)) => match self.scram_verifier(&authcid) {
|
||||||
Some((account, verifier)) => vec![NetAction::DeferAuthenticate {
|
Some((account, verifier)) => vec![NetAction::DeferAuthenticate {
|
||||||
verifier,
|
verifier,
|
||||||
password: passwd,
|
password: passwd,
|
||||||
then: AuthThen::Sasl { agent: agent.clone(), client: client.clone(), account },
|
then: AuthThen::Sasl { agent: agent.clone(), client: client.clone(), account },
|
||||||
}],
|
}],
|
||||||
None => mk("D", vec!["F".to_string()]),
|
None => {
|
||||||
|
let mut out = mk("D", vec!["F".to_string()]);
|
||||||
|
out.extend(self.feed("AUTH", format!("\x0304✗\x03 {} — SASL PLAIN failed for \x02{authcid}\x02 (unknown account)", self.who(&client))));
|
||||||
|
out
|
||||||
|
}
|
||||||
},
|
},
|
||||||
None => mk("D", vec!["F".to_string()]),
|
None => mk("D", vec!["F".to_string()]),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
src/keycard.rs
Normal file
27
src/keycard.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
//! Redeeming website login keycards. A member already authenticated on the
|
||||||
|
//! website (a Django session) connects with a single-use `kc_…` token instead of
|
||||||
|
//! replaying their password. It isn't the account password, so it can't be
|
||||||
|
//! checked against a SCRAM verifier — we hand it to Django's localhost
|
||||||
|
//! login-token endpoint, which redeems the keycard (exactly once) and confirms
|
||||||
|
//! the bound account.
|
||||||
|
//!
|
||||||
|
//! Runs on a blocking thread (see link.rs): a synchronous localhost round-trip,
|
||||||
|
//! never on the reactor or under the engine lock.
|
||||||
|
use crate::config::Keycard;
|
||||||
|
|
||||||
|
/// Redeem `token` for `account` against the configured endpoint. Returns true
|
||||||
|
/// only on a confirmed success (HTTP 2xx). A missing config, transport error, or
|
||||||
|
/// any non-2xx (bad / expired / already-used / account-mismatched keycard) is a
|
||||||
|
/// clean `false` — the caller then fails the SASL exchange.
|
||||||
|
pub fn redeem(cfg: Option<&Keycard>, account: &str, token: &str) -> bool {
|
||||||
|
let Some(cfg) = cfg else { return false };
|
||||||
|
let agent = ureq::AgentBuilder::new()
|
||||||
|
.timeout_connect(std::time::Duration::from_secs(3))
|
||||||
|
.timeout_read(std::time::Duration::from_secs(4))
|
||||||
|
.build();
|
||||||
|
agent
|
||||||
|
.post(&cfg.url)
|
||||||
|
.set("X-API-Key", &cfg.api_key)
|
||||||
|
.send_form(&[("username", account), ("password", token)])
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
14
src/link.rs
14
src/link.rs
|
|
@ -71,7 +71,7 @@ fn redact(line: &str) -> Cow<'_, str> {
|
||||||
// One uplink session: connect, handshake + burst, then translate lines forever.
|
// 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
|
// The engine is shared with the gossip layer, so it is locked per operation and
|
||||||
// never held across the registration key-stretching await.
|
// 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 stream = TcpStream::connect(addr).await?;
|
||||||
let (read, mut write) = stream.into_split();
|
let (read, mut write) = stream.into_split();
|
||||||
let mut lines = BufReader::new(read).lines();
|
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?;
|
.await?;
|
||||||
engine.lock().await.complete_authenticate(ok, then)
|
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],
|
action => vec![action],
|
||||||
};
|
};
|
||||||
for act in outs {
|
for act in outs {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ mod engine;
|
||||||
mod gossip;
|
mod gossip;
|
||||||
mod grpc;
|
mod grpc;
|
||||||
mod jsonrpc;
|
mod jsonrpc;
|
||||||
|
mod keycard;
|
||||||
mod link;
|
mod link;
|
||||||
mod migrate;
|
mod migrate;
|
||||||
mod proto;
|
mod proto;
|
||||||
|
|
@ -262,7 +263,7 @@ async fn main() -> Result<()> {
|
||||||
// just lets systemd stop us without waiting out the kill timeout.
|
// just lets systemd stop us without waiting out the kill timeout.
|
||||||
let shutdown_engine = engine.clone();
|
let shutdown_engine = engine.clone();
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = link::run(proto, engine, &addr, irc_rx, cfg.email.clone()) => res,
|
res = link::run(proto, engine, &addr, irc_rx, cfg.email.clone(), cfg.keycard.clone()) => res,
|
||||||
_ = shutdown_signal() => {
|
_ = shutdown_signal() => {
|
||||||
// Flush stat counters so a clean stop/restart keeps StatServ history.
|
// Flush stat counters so a clean stop/restart keeps StatServ history.
|
||||||
shutdown_engine.lock().await.persist_stats();
|
shutdown_engine.lock().await.persist_stats();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue