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}}},
);
}