Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
4.4 kB · 144 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};use k256::ecdsa::{Signature, SigningKey, signature::Signer};use serde::Deserialize;use serde_json::json;use std::time::{SystemTime, UNIX_EPOCH};
use crate::{Error, now, relay};use axum::http::StatusCode;
#[derive(Deserialize)]struct Committers { committers: Vec<String>,}
/// Registration policy gating delegated account creation on Tranquil.////// Build with [`Gate::builder`], then call [`Gate::check`] for each request.pub struct Gate { http: reqwest::Client, gate_did: String, signing_key: SigningKey, deliberi_url: String, deliberi_did: String, require_verified_email: bool,}
pub struct GateBuilder { http: reqwest::Client, gate_did: String, signing_key: SigningKey, deliberi_url: String, deliberi_did: String, require_verified_email: bool,}
impl Gate { pub fn builder( http: reqwest::Client, gate_did: impl Into<String>, signing_key: SigningKey, deliberi_url: impl Into<String>, deliberi_did: impl Into<String>, ) -> GateBuilder { GateBuilder { http, gate_did: gate_did.into(), signing_key, deliberi_url: deliberi_url.into(), deliberi_did: deliberi_did.into(), require_verified_email: true, } }
/// Tranquil enforces the per-controller delegate cap itself, keyed on the /// authenticated controller, so the gate only checks what Tranquil cannot. pub async fn check(&self, did: &str) -> Result<(), Error> { if self.require_verified_email { self.check_verified_email(did).await?; } Ok(()) }
async fn check_verified_email(&self, did: &str) -> Result<(), Error> { let response = self .http .post(format!( "{}/xrpc/sh.tangled.identity.resolveCommitters", self.deliberi_url )) .bearer_auth(self.resolver_token()) .json(&json!({"actor": did})) .send() .await?; let verified: Committers = relay(response).await?.json().await?; if !verified.committers.iter().any(|value| is_email(value)) { return Err(Error::code(StatusCode::FORBIDDEN, "VerifiedEmailRequired")); } Ok(()) }
fn resolver_token(&self) -> String { let timestamp = now(); let claims = json!({"iss": self.gate_did, "aud": self.deliberi_did, "iat": timestamp, "exp": timestamp + 60, "lxm": "sh.tangled.identity.resolveCommitters", "jti": format!("{}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos())}); let input = format!( "{}.{}", URL_SAFE_NO_PAD.encode(br#"{"alg":"ES256K","typ":"JWT"}"#), URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims).unwrap()) ); let signature: Signature = self.signing_key.sign(input.as_bytes()); format!("{input}.{}", URL_SAFE_NO_PAD.encode(signature.to_bytes())) }}
impl GateBuilder { pub fn require_verified_email(mut self, require: bool) -> Self { self.require_verified_email = require; self }
pub fn build(self) -> Gate { Gate { http: self.http, gate_did: self.gate_did, signing_key: self.signing_key, deliberi_url: self.deliberi_url, deliberi_did: self.deliberi_did, require_verified_email: self.require_verified_email, } }}
fn is_email(value: &str) -> bool { let value = value.trim(); !value.starts_with("did:") && value.len() <= 320 && !value.chars().any(char::is_whitespace) && !value.chars().any(char::is_control) && value.split_once('@').is_some_and(|(local, domain)| { !local.is_empty() && !domain.is_empty() && !domain.contains('@') })}
#[cfg(test)]mod tests { use super::*;
#[test] fn email_filter() { for value in [ "did:plc:abcdefghijklmnopqrstuvwx", "", "@host", "user@", "a@@b", "a\0@b", "a b@host", ] { assert!(!is_email(value)); } assert!(is_email("person@example.com")); }}