From 65d7a109a01b6243d3bcb5dc3d108af731273609 Mon Sep 17 00:00:00 2001 From: phil Date: Thu, 3 Jul 2025 15:12:38 -0400 Subject: [PATCH] make jwts with the did in em --- Cargo.lock | 59 +++++++++++++++++++++++++++++++ who-am-i/.gitignore | 1 + who-am-i/Cargo.toml | 1 + who-am-i/demo/index.html | 2 ++ who-am-i/src/jwt.rs | 55 ++++++++++++++++++++++++++++ who-am-i/src/lib.rs | 2 ++ who-am-i/src/main.rs | 25 +++++++++++-- who-am-i/src/server.rs | 31 +++++++++++++++- who-am-i/templates/authorized.hbs | 1 + who-am-i/templates/prompt.hbs | 8 ++--- 10 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 who-am-i/.gitignore create mode 100644 who-am-i/src/jwt.rs diff --git a/Cargo.lock b/Cargo.lock index a40c951..428ea70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1697,8 +1697,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -2454,6 +2456,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "langtag" version = "0.3.4" @@ -2954,6 +2971,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2970,6 +2997,15 @@ dependencies = [ "itoa", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-modular" version = "0.6.1" @@ -3158,6 +3194,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4086,6 +4132,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.12", + "time", +] + [[package]] name = "sketches-ddsketch" version = "0.3.0" @@ -5087,6 +5145,7 @@ dependencies = [ "dashmap", "handlebars", "hickory-resolver", + "jsonwebtoken", "metrics", "metrics-exporter-prometheus 0.17.2", "rand 0.9.1", diff --git a/who-am-i/.gitignore b/who-am-i/.gitignore new file mode 100644 index 0000000..9ff601a --- /dev/null +++ b/who-am-i/.gitignore @@ -0,0 +1 @@ +jwt-key.pem diff --git a/who-am-i/Cargo.toml b/who-am-i/Cargo.toml index ee26be6..e194c3a 100644 --- a/who-am-i/Cargo.toml +++ b/who-am-i/Cargo.toml @@ -16,6 +16,7 @@ ctrlc = "3.4.7" dashmap = "6.1.0" handlebars = { version = "6.3.2", features = ["dir_source"] } hickory-resolver = "0.25.2" +jsonwebtoken = "9.3.1" metrics = "0.24.2" rand = "0.9.1" reqwest = { version = "0.12.22", features = ["native-tls-vendored"] } diff --git a/who-am-i/demo/index.html b/who-am-i/demo/index.html index 06fe404..801a226 100644 --- a/who-am-i/demo/index.html +++ b/who-am-i/demo/index.html @@ -12,6 +12,7 @@

hey

+

@@ -27,6 +28,7 @@ window.removeEventListener('message', handleMessage); document.getElementById('who').textContent = ev.data.handle; + document.getElementById('jwt').textContent = ev.data.token; } window.addEventListener('message', handleMessage); })(document.getElementById('whoami')); diff --git a/who-am-i/src/jwt.rs b/who-am-i/src/jwt.rs new file mode 100644 index 0000000..f56a040 --- /dev/null +++ b/who-am-i/src/jwt.rs @@ -0,0 +1,55 @@ +use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError}; +use serde::Serialize; +use std::fs; +use std::io::Error as IOError; +use std::path::Path; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum TokensSetupError { + #[error(transparent)] + Io(#[from] IOError), + #[error("failed to retrieve ec key: {0}")] + FromEc(JWTError), +} + +#[derive(Debug, Error)] +pub enum TokenMintingError { + #[error("failed to mint: {0}")] + FromEc(#[from] JWTError), +} + +pub struct Tokens { + encoding_key: EncodingKey, +} + +impl Tokens { + pub fn from_file(f: impl AsRef) -> Result { + let data: Vec = fs::read(f)?; + let encoding_key = EncodingKey::from_ec_pem(&data).map_err(TokensSetupError::FromEc)?; + Ok(Self { encoding_key }) + } + + pub fn mint(&self, t: impl ToString) -> Result { + let sub = t.to_string(); + + let dt_now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("unix epoch is in the past"); + let dt_exp = dt_now + Duration::from_secs(30 * 86_400); + let exp = dt_exp.as_secs(); + + Ok(encode( + &Header::new(Algorithm::ES256), + &Claims { sub, exp }, + &self.encoding_key, + )?) + } +} + +#[derive(Debug, Serialize)] +struct Claims { + sub: String, + exp: u64, +} diff --git a/who-am-i/src/lib.rs b/who-am-i/src/lib.rs index 76673ad..975fb4b 100644 --- a/who-am-i/src/lib.rs +++ b/who-am-i/src/lib.rs @@ -1,7 +1,9 @@ mod expiring_task_map; +mod jwt; mod oauth; mod server; pub use expiring_task_map::ExpiringTaskMap; +pub use jwt::Tokens; pub use oauth::{OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError}; pub use server::serve; diff --git a/who-am-i/src/main.rs b/who-am-i/src/main.rs index 75d3bcf..7f9f866 100644 --- a/who-am-i/src/main.rs +++ b/who-am-i/src/main.rs @@ -1,7 +1,8 @@ use clap::{ArgAction, Parser}; -use metrics_exporter_prometheus::{PrometheusBuilder, BuildError as PromBuildError}; +use metrics_exporter_prometheus::{BuildError as PromBuildError, PrometheusBuilder}; +use std::path::PathBuf; use tokio_util::sync::CancellationToken; -use who_am_i::serve; +use who_am_i::{Tokens, serve}; /// Aggregate links in the at-mosphere #[derive(Parser, Debug, Clone)] @@ -14,6 +15,15 @@ struct Args { /// eg: `cat /dev/urandom | head -c 64 | base64` #[arg(long, env)] app_secret: String, + /// path to jwt key (PEM format) + /// + /// generate with: + /// ```bash + /// openssl ecparam -genkey -noout -name prime256v1 \ + /// | openssl pkcs8 -topk8 -nocrypt -out .pem + /// ``` + #[arg(long)] + jwt_key: PathBuf, /// Enable dev mode /// /// enables automatic template reloading @@ -44,11 +54,20 @@ async fn main() { println!(" - {host}"); } + let tokens = Tokens::from_file(args.jwt_key).unwrap(); + if let Err(e) = install_metrics_server() { eprintln!("failed to install metrics server: {e:?}"); }; - serve(shutdown, args.app_secret, args.allowed_hosts, args.dev).await; + serve( + shutdown, + args.app_secret, + tokens, + args.allowed_hosts, + args.dev, + ) + .await; } fn install_metrics_server() -> Result<(), PromBuildError> { diff --git a/who-am-i/src/server.rs b/who-am-i/src/server.rs index 9e15638..51d088b 100644 --- a/who-am-i/src/server.rs +++ b/who-am-i/src/server.rs @@ -22,7 +22,9 @@ use tokio::net::TcpListener; use tokio_util::sync::CancellationToken; use url::Url; -use crate::{ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError}; +use crate::{ + ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError, Tokens, +}; const FAVICON: &[u8] = include_bytes!("../static/favicon.ico"); const STYLE_CSS: &str = include_str!("../static/style.css"); @@ -41,6 +43,7 @@ struct AppState { pub oauth: Arc, pub resolve_handles: ExpiringTaskMap>, pub shutdown: CancellationToken, + pub tokens: Arc, } impl FromRef for Key { @@ -52,6 +55,7 @@ impl FromRef for Key { pub async fn serve( shutdown: CancellationToken, app_secret: String, + tokens: Tokens, allowed_hosts: Vec, dev: bool, ) { @@ -75,6 +79,7 @@ pub async fn serve( oauth: Arc::new(oauth), resolve_handles: ExpiringTaskMap::new(task_pickup_expiration), shutdown: shutdown.clone(), + tokens: Arc::new(tokens), }; let app = Router::new() @@ -166,6 +171,7 @@ async fn prompt( oauth, resolve_handles, shutdown, + tokens, .. }): State, jar: SignedCookieJar, @@ -214,6 +220,14 @@ async fn prompt( // push cookie expiry let jar = jar.add(cookie(&did)); + let token = match tokens.mint(&*did) { + Ok(t) => t, + Err(e) => { + eprintln!("failed to create JWT: {e:?}"); + return err("failed to create JWT", false); + } + }; + let fetch_key = resolve_handles.dispatch( { let oauth = oauth.clone(); @@ -226,6 +240,7 @@ async fn prompt( metrics::counter!("whoami_auth_prompt", "ok" => "true", "known" => "true").increment(1); let info = json!({ "did": did, + "token": token, "fetch_key": fetch_key, "parent_host": parent_host, "parent_origin": parent_origin, @@ -340,6 +355,7 @@ async fn complete_oauth( resolve_handles, oauth, shutdown, + tokens, .. }): State, Query(params): Query, @@ -386,6 +402,18 @@ async fn complete_oauth( let jar = jar.add(cookie(&did)); + let token = match tokens.mint(&*did) { + Ok(t) => t, + Err(e) => { + eprintln!("failed to create JWT: {e:?}"); + return err( + StatusCode::INTERNAL_SERVER_ERROR, + "fail", + "failed to create JWT", + ); + } + }; + let fetch_key = resolve_handles.dispatch( { let oauth = oauth.clone(); @@ -398,6 +426,7 @@ async fn complete_oauth( metrics::counter!("whoami_auth_complete", "ok" => "true").increment(1); let info = json!({ "did": did, + "token": token, "fetch_key": fetch_key, }); (jar, RenderHtml("authorized", engine, info)).into_response() diff --git a/who-am-i/templates/authorized.hbs b/who-am-i/templates/authorized.hbs index 38f945c..7edacbc 100644 --- a/who-am-i/templates/authorized.hbs +++ b/who-am-i/templates/authorized.hbs @@ -8,6 +8,7 @@ localStorage.setItem("who-am-i", JSON.stringify({ result: "success", did: {{{json did}}}, + token: {{{json token}}}, fetch_key: {{{json fetch_key}}}, })); window.close(); diff --git a/who-am-i/templates/prompt.hbs b/who-am-i/templates/prompt.hbs index 5e87be9..05b6f59 100644 --- a/who-am-i/templates/prompt.hbs +++ b/who-am-i/templates/prompt.hbs @@ -55,7 +55,7 @@ function err(e, msg) { loaderEl.classList.add('hidden'); handleViewEl.textContent = `@${handle}`; - allowEl.addEventListener('click', () => shareAllow(handle)); + allowEl.addEventListener('click', () => shareAllow(handle, {{{json token}}})); })(); // anon user @@ -108,7 +108,7 @@ window.addEventListener('storage', async e => { const handle = await lookUp(parsed.fetch_key); - shareAllow(handle); + shareAllow(handle, token); }); async function lookUp(fetch_key) { @@ -125,9 +125,9 @@ async function lookUp(fetch_key) { return info.handle; } -const shareAllow = handle => { +const shareAllow = (handle, token) => { top.postMessage( - { action: "allow", handle }, + { action: "allow", handle, token }, {{{json parent_origin}}}, ); } -- 2.51.2