(feat): improvements to the slow

This commit is contained in:
Alois 2026-07-03 00:36:15 +02:00
commit c51c0f2e83
7 changed files with 76 additions and 21 deletions

1
Cargo.lock generated
View file

@ -1888,6 +1888,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"url", "url",

View file

@ -2,7 +2,7 @@
name = "tauth-sdk" name = "tauth-sdk"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "UNLICENSED" license = "MIT"
[lib] [lib]
name = "tauth_sdk" name = "tauth_sdk"
@ -14,6 +14,7 @@ mtp = { git = "https://git.methanium.net/methanium/mtp.git", features = ["client
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
sha2 = "0.10"
thiserror = "2" thiserror = "2"
tokio = { version = "1", features = ["rt", "time"] } tokio = { version = "1", features = ["rt", "time"] }
url = "2" url = "2"

View file

@ -48,7 +48,7 @@ let client = TAuthClient::from_config(
"https://my.cool.app/tauth/callback", "https://my.cool.app/tauth/callback",
std::env::var("TAUTH_PRIVATE_KEY")?, std::env::var("TAUTH_PRIVATE_KEY")?,
) )
.frontend_url("https://tauth.tensamin.net/login"), .frontend_url("https://app.tensamin.net"),
)?; )?;
``` ```

View file

@ -30,6 +30,8 @@ Set `TAUTH_PRIVATE_KEY` in `backend/.env`. Generate keys with:
pnpm --filter @tauth-example/backend exec cargo run --bin generate_keys pnpm --filter @tauth-example/backend exec cargo run --bin generate_keys
``` ```
Publish the generated `TAUTH_PUBLIC_KEY_SHA256` value in the `tauth.<identifier>` TXT record. TAuth receives the full public key during authorization and verifies it against that hash.
For local callback testing, `TAUTH_REDIRECT_URL` must be reachable by TAuth. Use a tunnel if needed. For local callback testing, `TAUTH_REDIRECT_URL` must be reachable by TAuth. Use a tunnel if needed.
## Run ## Run

View file

@ -1990,6 +1990,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"url", "url",

View file

@ -3,4 +3,5 @@ fn main() {
println!("TAUTH_PRIVATE_KEY={}", keys.private); println!("TAUTH_PRIVATE_KEY={}", keys.private);
println!("TAUTH_PUBLIC_KEY={}", keys.public); println!("TAUTH_PUBLIC_KEY={}", keys.public);
println!("TAUTH_PUBLIC_KEY_SHA256={}", keys.public_hash);
} }

View file

@ -5,10 +5,11 @@ use mtp::crypto::{Keyring, PublicKeyBundle};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use sha2::{Digest, Sha256};
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
const DEFAULT_FRONTEND_URL: &str = "https://tauth.tensamin.net/login"; const DEFAULT_FRONTEND_URL: &str = "https://app.tensamin.net";
const OMEGA_API: &str = "https://omega.tensamin.net/api"; const OMEGA_API: &str = "https://omega.tensamin.net/api";
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -39,6 +40,7 @@ pub enum TAuthError {
pub struct TAuthKeyPair { pub struct TAuthKeyPair {
pub private: String, pub private: String,
pub public: String, pub public: String,
pub public_hash: String,
} }
pub type TAuthKeys = TAuthKeyPair; pub type TAuthKeys = TAuthKeyPair;
@ -168,7 +170,11 @@ impl TAuthClient {
redirect_url: impl AsRef<str>, redirect_url: impl AsRef<str>,
private_key: impl AsRef<str>, private_key: impl AsRef<str>,
) -> Result<Self, TAuthError> { ) -> Result<Self, TAuthError> {
Self::from_config(TAuthConfig::new(identifier, redirect_url.as_ref(), private_key.as_ref())) Self::from_config(TAuthConfig::new(
identifier,
redirect_url.as_ref(),
private_key.as_ref(),
))
} }
pub fn with_frontend_url( pub fn with_frontend_url(
@ -205,6 +211,10 @@ impl TAuthClient {
encode_bytes(&self.public_key_bundle.as_bytes()) encode_bytes(&self.public_key_bundle.as_bytes())
} }
pub fn public_key_hash(&self) -> String {
sha256_hex(self.public_key().as_bytes())
}
pub fn redirect_url(&self) -> &Url { pub fn redirect_url(&self) -> &Url {
&self.redirect_url &self.redirect_url
} }
@ -217,6 +227,7 @@ impl TAuthClient {
TAuthKeyPair { TAuthKeyPair {
private: encode_bytes(&self.keyring.to_bytes()), private: encode_bytes(&self.keyring.to_bytes()),
public: self.public_key(), public: self.public_key(),
public_hash: self.public_key_hash(),
} }
} }
@ -238,7 +249,10 @@ impl TAuthClient {
link.to_string() link.to_string()
} }
pub fn parse_callback(&self, callback_url: impl AsRef<str>) -> Result<AuthCallback, TAuthError> { pub fn parse_callback(
&self,
callback_url: impl AsRef<str>,
) -> Result<AuthCallback, TAuthError> {
parse_callback(callback_url) parse_callback(callback_url)
} }
@ -314,7 +328,12 @@ impl TAuthClient {
host_public_key: impl AsRef<str>, host_public_key: impl AsRef<str>,
) -> Result<MTPConnection, TAuthError> { ) -> Result<MTPConnection, TAuthError> {
let omikron = fetch_omikron(user_id).await?; let omikron = fetch_omikron(user_id).await?;
self.connect_or_register_to_omikron(user_id, session_id, omikron.ip_address, host_public_key) self.connect_or_register_to_omikron(
user_id,
session_id,
omikron.ip_address,
host_public_key,
)
.await .await
} }
@ -329,13 +348,15 @@ impl TAuthClient {
let config = ClientConfig::new(omikron_url.into()) let config = ClientConfig::new(omikron_url.into())
.with_description(format!("tauth:{}:{user_id}", self.identifier)) .with_description(format!("tauth:{}:{user_id}", self.identifier))
.with_auth_timeout(self.auth_timeout); .with_auth_timeout(self.auth_timeout);
Ok(MTPClient::auth_connect_or_register( Ok(
MTPClient::auth_connect_or_register(
config, config,
session_id, session_id,
&self.keyring, &self.keyring,
&host_public_key, &host_public_key,
) )
.await?) .await?,
)
} }
pub async fn connect_session( pub async fn connect_session(
@ -359,9 +380,11 @@ impl TAuthClient {
pub fn generate_key_pair() -> TAuthKeyPair { pub fn generate_key_pair() -> TAuthKeyPair {
let keyring = Keyring::generate(); let keyring = Keyring::generate();
let public = encode_bytes(&keyring.public_key_bundle().as_bytes());
TAuthKeyPair { TAuthKeyPair {
private: encode_bytes(&keyring.to_bytes()), private: encode_bytes(&keyring.to_bytes()),
public: encode_bytes(&keyring.public_key_bundle().as_bytes()), public_hash: sha256_hex(public.as_bytes()),
public,
} }
} }
@ -440,6 +463,13 @@ fn decode_public_key_bundle(value: &str) -> Result<PublicKeyBundle, TAuthError>
.map_err(|error| TAuthError::Key(error.to_string())) .map_err(|error| TAuthError::Key(error.to_string()))
} }
fn sha256_hex(bytes: &[u8]) -> String {
Sha256::digest(bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
async fn fetch_omikron(user_id: u64) -> Result<OmikronResponse, TAuthError> { async fn fetch_omikron(user_id: u64) -> Result<OmikronResponse, TAuthError> {
let url = format!("{OMEGA_API}/get/omikron/{user_id}"); let url = format!("{OMEGA_API}/get/omikron/{user_id}");
Ok(reqwest::get(url).await?.error_for_status()?.json().await?) Ok(reqwest::get(url).await?.error_for_status()?.json().await?)
@ -456,6 +486,7 @@ mod tests {
let public = decode_public_key_bundle(&keys.public).unwrap(); let public = decode_public_key_bundle(&keys.public).unwrap();
assert_eq!(public.as_bytes(), keyring.public_key_bundle().as_bytes()); assert_eq!(public.as_bytes(), keyring.public_key_bundle().as_bytes());
assert_eq!(keys.public_hash, sha256_hex(keys.public.as_bytes()));
} }
#[test] #[test]
@ -470,9 +501,28 @@ mod tests {
.unwrap(); .unwrap();
let link = Url::parse(&client.generate_link(Some("abc123"))).unwrap(); let link = Url::parse(&client.generate_link(Some("abc123"))).unwrap();
assert_eq!(link.query_pairs().find(|(key, _)| key == "identifier").unwrap().1, "test.app"); assert_eq!(
assert_eq!(link.query_pairs().find(|(key, _)| key == "challenge").unwrap().1, "abc123"); link.query_pairs()
assert!(link.query_pairs().any(|(key, value)| key == "public_key" && !value.is_empty())); .find(|(key, _)| key == "identifier")
.unwrap()
.1,
"test.app"
);
assert_eq!(
link.query_pairs()
.find(|(key, _)| key == "challenge")
.unwrap()
.1,
"abc123"
);
assert!(
link.query_pairs()
.any(|(key, value)| key == "public_key" && !value.is_empty())
);
assert_eq!(
client.public_key_hash(),
sha256_hex(client.public_key().as_bytes())
);
} }
#[test] #[test]
@ -491,9 +541,8 @@ mod tests {
#[test] #[test]
fn parses_snake_case_host_key_callback_url() { fn parses_snake_case_host_key_callback_url() {
let callback = parse_callback( let callback =
"https://app.example/callback?userId=42&sessionId=7&host_public_key=c", parse_callback("https://app.example/callback?userId=42&sessionId=7&host_public_key=c")
)
.unwrap(); .unwrap();
assert_eq!(callback.host_public_key.as_deref(), Some("c")); assert_eq!(callback.host_public_key.as_deref(), Some("c"));