Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

make jwts with the did in em

+59
Cargo.lock
···
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
+
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
+
"wasm-bindgen",
[[package]]
···
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
dependencies = [
"digest",
"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]]
···
"dashmap",
"handlebars",
"hickory-resolver",
+
"jsonwebtoken",
"metrics",
"metrics-exporter-prometheus 0.17.2",
"rand 0.9.1",
+1
who-am-i/.gitignore
···
+
jwt-key.pem
+1
who-am-i/Cargo.toml
···
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"] }
+2
who-am-i/demo/index.html
···
<body>
<h1>hey <span id="who"></span></h1>
+
<p><code id="jwt"></code></p>
<iframe src="http://127.0.0.1:9997/prompt" id="whoami" style="border: none" height="160" width="320"></iframe>
···
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'));
+55
who-am-i/src/jwt.rs
···
+
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<Path>) -> Result<Self, TokensSetupError> {
+
let data: Vec<u8> = 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<String, TokenMintingError> {
+
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,
+
}
+2
who-am-i/src/lib.rs
···
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;
+22 -3
who-am-i/src/main.rs
···
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)]
···
/// 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 <PATH-TO-JWT-KEY>.pem
+
/// ```
+
#[arg(long)]
+
jwt_key: PathBuf,
/// Enable dev mode
///
/// enables automatic template reloading
···
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> {
+30 -1
who-am-i/src/server.rs
···
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");
···
pub oauth: Arc<OAuth>,
pub resolve_handles: ExpiringTaskMap<Result<String, ResolveHandleError>>,
pub shutdown: CancellationToken,
+
pub tokens: Arc<Tokens>,
}
impl FromRef<AppState> for Key {
···
pub async fn serve(
shutdown: CancellationToken,
app_secret: String,
+
tokens: Tokens,
allowed_hosts: Vec<String>,
dev: bool,
) {
···
oauth: Arc::new(oauth),
resolve_handles: ExpiringTaskMap::new(task_pickup_expiration),
shutdown: shutdown.clone(),
+
tokens: Arc::new(tokens),
};
let app = Router::new()
···
oauth,
resolve_handles,
shutdown,
+
tokens,
..
}): State<AppState>,
jar: SignedCookieJar,
···
// 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(
{
···
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,
···
resolve_handles,
oauth,
shutdown,
+
tokens,
..
}): State<AppState>,
Query(params): Query<OAuthCallbackParams>,
···
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();
···
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()
+1
who-am-i/templates/authorized.hbs
···
localStorage.setItem("who-am-i", JSON.stringify({
result: "success",
did: {{{json did}}},
+
token: {{{json token}}},
fetch_key: {{{json fetch_key}}},
}));
window.close();
+4 -4
who-am-i/templates/prompt.hbs
···
loaderEl.classList.add('hidden');
handleViewEl.textContent = `@${handle}`;
-
allowEl.addEventListener('click', () => shareAllow(handle));
+
allowEl.addEventListener('click', () => shareAllow(handle, {{{json token}}}));
})();
// anon user
···
const handle = await lookUp(parsed.fetch_key);
-
shareAllow(handle);
+
shareAllow(handle, token);
});
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}}},
);
}