alpha
Login
or
Join now
microcosm.blue
/
microcosm-rs
Star
0
Fork
3
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm
Star
0
Fork
3
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
make jwts with the did in em
author
phil
date
11 months ago
(Jul 3, 2025, 3:12 PM -0400)
commit
65d7a109
65d7a109a01b6243d3bcb5dc3d108af731273609
parent
6d65c3f4
6d65c3f4620cc447ff9d40ceb75d3aeb878d2984
+177
-8
10 changed files
Expand all
Collapse all
Unified
Split
Cargo.lock
who-am-i
.gitignore
Cargo.toml
demo
index.html
src
jwt.rs
lib.rs
main.rs
server.rs
templates
authorized.hbs
prompt.hbs
+59
Cargo.lock
Reviewed
···
1697
1697
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
1698
1698
dependencies = [
1699
1699
"cfg-if",
1700
1700
+
"js-sys",
1700
1701
"libc",
1701
1702
"wasi 0.11.0+wasi-snapshot-preview1",
1703
1703
+
"wasm-bindgen",
1702
1704
]
1703
1705
1704
1706
[[package]]
···
2455
2457
]
2456
2458
2457
2459
[[package]]
2460
2460
+
name = "jsonwebtoken"
2461
2461
+
version = "9.3.1"
2462
2462
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2463
2463
+
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
2464
2464
+
dependencies = [
2465
2465
+
"base64 0.22.1",
2466
2466
+
"js-sys",
2467
2467
+
"pem",
2468
2468
+
"ring",
2469
2469
+
"serde",
2470
2470
+
"serde_json",
2471
2471
+
"simple_asn1",
2472
2472
+
]
2473
2473
+
2474
2474
+
[[package]]
2458
2475
name = "langtag"
2459
2476
version = "0.3.4"
2460
2477
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2955
2972
]
2956
2973
2957
2974
[[package]]
2975
2975
+
name = "num-bigint"
2976
2976
+
version = "0.4.6"
2977
2977
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2978
2978
+
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
2979
2979
+
dependencies = [
2980
2980
+
"num-integer",
2981
2981
+
"num-traits",
2982
2982
+
]
2983
2983
+
2984
2984
+
[[package]]
2958
2985
name = "num-conv"
2959
2986
version = "0.1.0"
2960
2987
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2971
2998
]
2972
2999
2973
3000
[[package]]
3001
3001
+
name = "num-integer"
3002
3002
+
version = "0.1.46"
3003
3003
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3004
3004
+
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
3005
3005
+
dependencies = [
3006
3006
+
"num-traits",
3007
3007
+
]
3008
3008
+
3009
3009
+
[[package]]
2974
3010
name = "num-modular"
2975
3011
version = "0.6.1"
2976
3012
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3159
3195
]
3160
3196
3161
3197
[[package]]
3198
3198
+
name = "pem"
3199
3199
+
version = "3.0.5"
3200
3200
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3201
3201
+
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
3202
3202
+
dependencies = [
3203
3203
+
"base64 0.22.1",
3204
3204
+
"serde",
3205
3205
+
]
3206
3206
+
3207
3207
+
[[package]]
3162
3208
name = "percent-encoding"
3163
3209
version = "2.3.1"
3164
3210
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4084
4130
dependencies = [
4085
4131
"digest",
4086
4132
"rand_core 0.6.4",
4133
4133
+
]
4134
4134
+
4135
4135
+
[[package]]
4136
4136
+
name = "simple_asn1"
4137
4137
+
version = "0.6.3"
4138
4138
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4139
4139
+
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
4140
4140
+
dependencies = [
4141
4141
+
"num-bigint",
4142
4142
+
"num-traits",
4143
4143
+
"thiserror 2.0.12",
4144
4144
+
"time",
4087
4145
]
4088
4146
4089
4147
[[package]]
···
5087
5145
"dashmap",
5088
5146
"handlebars",
5089
5147
"hickory-resolver",
5148
5148
+
"jsonwebtoken",
5090
5149
"metrics",
5091
5150
"metrics-exporter-prometheus 0.17.2",
5092
5151
"rand 0.9.1",
+1
who-am-i/.gitignore
Reviewed
···
1
1
+
jwt-key.pem
+1
who-am-i/Cargo.toml
Reviewed
···
16
16
dashmap = "6.1.0"
17
17
handlebars = { version = "6.3.2", features = ["dir_source"] }
18
18
hickory-resolver = "0.25.2"
19
19
+
jsonwebtoken = "9.3.1"
19
20
metrics = "0.24.2"
20
21
rand = "0.9.1"
21
22
reqwest = { version = "0.12.22", features = ["native-tls-vendored"] }
+2
who-am-i/demo/index.html
Reviewed
···
12
12
13
13
<body>
14
14
<h1>hey <span id="who"></span></h1>
15
15
+
<p><code id="jwt"></code></p>
15
16
16
17
<iframe src="http://127.0.0.1:9997/prompt" id="whoami" style="border: none" height="160" width="320"></iframe>
17
18
···
27
28
window.removeEventListener('message', handleMessage);
28
29
29
30
document.getElementById('who').textContent = ev.data.handle;
31
31
+
document.getElementById('jwt').textContent = ev.data.token;
30
32
}
31
33
window.addEventListener('message', handleMessage);
32
34
})(document.getElementById('whoami'));
+55
who-am-i/src/jwt.rs
Reviewed
···
1
1
+
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError};
2
2
+
use serde::Serialize;
3
3
+
use std::fs;
4
4
+
use std::io::Error as IOError;
5
5
+
use std::path::Path;
6
6
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
7
+
use thiserror::Error;
8
8
+
9
9
+
#[derive(Debug, Error)]
10
10
+
pub enum TokensSetupError {
11
11
+
#[error(transparent)]
12
12
+
Io(#[from] IOError),
13
13
+
#[error("failed to retrieve ec key: {0}")]
14
14
+
FromEc(JWTError),
15
15
+
}
16
16
+
17
17
+
#[derive(Debug, Error)]
18
18
+
pub enum TokenMintingError {
19
19
+
#[error("failed to mint: {0}")]
20
20
+
FromEc(#[from] JWTError),
21
21
+
}
22
22
+
23
23
+
pub struct Tokens {
24
24
+
encoding_key: EncodingKey,
25
25
+
}
26
26
+
27
27
+
impl Tokens {
28
28
+
pub fn from_file(f: impl AsRef<Path>) -> Result<Self, TokensSetupError> {
29
29
+
let data: Vec<u8> = fs::read(f)?;
30
30
+
let encoding_key = EncodingKey::from_ec_pem(&data).map_err(TokensSetupError::FromEc)?;
31
31
+
Ok(Self { encoding_key })
32
32
+
}
33
33
+
34
34
+
pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> {
35
35
+
let sub = t.to_string();
36
36
+
37
37
+
let dt_now = SystemTime::now()
38
38
+
.duration_since(UNIX_EPOCH)
39
39
+
.expect("unix epoch is in the past");
40
40
+
let dt_exp = dt_now + Duration::from_secs(30 * 86_400);
41
41
+
let exp = dt_exp.as_secs();
42
42
+
43
43
+
Ok(encode(
44
44
+
&Header::new(Algorithm::ES256),
45
45
+
&Claims { sub, exp },
46
46
+
&self.encoding_key,
47
47
+
)?)
48
48
+
}
49
49
+
}
50
50
+
51
51
+
#[derive(Debug, Serialize)]
52
52
+
struct Claims {
53
53
+
sub: String,
54
54
+
exp: u64,
55
55
+
}
+2
who-am-i/src/lib.rs
Reviewed
···
1
1
mod expiring_task_map;
2
2
+
mod jwt;
2
3
mod oauth;
3
4
mod server;
4
5
5
6
pub use expiring_task_map::ExpiringTaskMap;
7
7
+
pub use jwt::Tokens;
6
8
pub use oauth::{OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError};
7
9
pub use server::serve;
+22
-3
who-am-i/src/main.rs
Reviewed
···
1
1
use clap::{ArgAction, Parser};
2
2
-
use metrics_exporter_prometheus::{PrometheusBuilder, BuildError as PromBuildError};
2
2
+
use metrics_exporter_prometheus::{BuildError as PromBuildError, PrometheusBuilder};
3
3
+
use std::path::PathBuf;
3
4
use tokio_util::sync::CancellationToken;
4
4
-
use who_am_i::serve;
5
5
+
use who_am_i::{Tokens, serve};
5
6
6
7
/// Aggregate links in the at-mosphere
7
8
#[derive(Parser, Debug, Clone)]
···
14
15
/// eg: `cat /dev/urandom | head -c 64 | base64`
15
16
#[arg(long, env)]
16
17
app_secret: String,
18
18
+
/// path to jwt key (PEM format)
19
19
+
///
20
20
+
/// generate with:
21
21
+
/// ```bash
22
22
+
/// openssl ecparam -genkey -noout -name prime256v1 \
23
23
+
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-JWT-KEY>.pem
24
24
+
/// ```
25
25
+
#[arg(long)]
26
26
+
jwt_key: PathBuf,
17
27
/// Enable dev mode
18
28
///
19
29
/// enables automatic template reloading
···
44
54
println!(" - {host}");
45
55
}
46
56
57
57
+
let tokens = Tokens::from_file(args.jwt_key).unwrap();
58
58
+
47
59
if let Err(e) = install_metrics_server() {
48
60
eprintln!("failed to install metrics server: {e:?}");
49
61
};
50
62
51
51
-
serve(shutdown, args.app_secret, args.allowed_hosts, args.dev).await;
63
63
+
serve(
64
64
+
shutdown,
65
65
+
args.app_secret,
66
66
+
tokens,
67
67
+
args.allowed_hosts,
68
68
+
args.dev,
69
69
+
)
70
70
+
.await;
52
71
}
53
72
54
73
fn install_metrics_server() -> Result<(), PromBuildError> {
+30
-1
who-am-i/src/server.rs
Reviewed
···
22
22
use tokio_util::sync::CancellationToken;
23
23
use url::Url;
24
24
25
25
-
use crate::{ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError};
25
25
+
use crate::{
26
26
+
ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError, Tokens,
27
27
+
};
26
28
27
29
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
28
30
const STYLE_CSS: &str = include_str!("../static/style.css");
···
41
43
pub oauth: Arc<OAuth>,
42
44
pub resolve_handles: ExpiringTaskMap<Result<String, ResolveHandleError>>,
43
45
pub shutdown: CancellationToken,
46
46
+
pub tokens: Arc<Tokens>,
44
47
}
45
48
46
49
impl FromRef<AppState> for Key {
···
52
55
pub async fn serve(
53
56
shutdown: CancellationToken,
54
57
app_secret: String,
58
58
+
tokens: Tokens,
55
59
allowed_hosts: Vec<String>,
56
60
dev: bool,
57
61
) {
···
75
79
oauth: Arc::new(oauth),
76
80
resolve_handles: ExpiringTaskMap::new(task_pickup_expiration),
77
81
shutdown: shutdown.clone(),
82
82
+
tokens: Arc::new(tokens),
78
83
};
79
84
80
85
let app = Router::new()
···
166
171
oauth,
167
172
resolve_handles,
168
173
shutdown,
174
174
+
tokens,
169
175
..
170
176
}): State<AppState>,
171
177
jar: SignedCookieJar,
···
213
219
214
220
// push cookie expiry
215
221
let jar = jar.add(cookie(&did));
222
222
+
223
223
+
let token = match tokens.mint(&*did) {
224
224
+
Ok(t) => t,
225
225
+
Err(e) => {
226
226
+
eprintln!("failed to create JWT: {e:?}");
227
227
+
return err("failed to create JWT", false);
228
228
+
}
229
229
+
};
216
230
217
231
let fetch_key = resolve_handles.dispatch(
218
232
{
···
226
240
metrics::counter!("whoami_auth_prompt", "ok" => "true", "known" => "true").increment(1);
227
241
let info = json!({
228
242
"did": did,
243
243
+
"token": token,
229
244
"fetch_key": fetch_key,
230
245
"parent_host": parent_host,
231
246
"parent_origin": parent_origin,
···
340
355
resolve_handles,
341
356
oauth,
342
357
shutdown,
358
358
+
tokens,
343
359
..
344
360
}): State<AppState>,
345
361
Query(params): Query<OAuthCallbackParams>,
···
386
402
387
403
let jar = jar.add(cookie(&did));
388
404
405
405
+
let token = match tokens.mint(&*did) {
406
406
+
Ok(t) => t,
407
407
+
Err(e) => {
408
408
+
eprintln!("failed to create JWT: {e:?}");
409
409
+
return err(
410
410
+
StatusCode::INTERNAL_SERVER_ERROR,
411
411
+
"fail",
412
412
+
"failed to create JWT",
413
413
+
);
414
414
+
}
415
415
+
};
416
416
+
389
417
let fetch_key = resolve_handles.dispatch(
390
418
{
391
419
let oauth = oauth.clone();
···
398
426
metrics::counter!("whoami_auth_complete", "ok" => "true").increment(1);
399
427
let info = json!({
400
428
"did": did,
429
429
+
"token": token,
401
430
"fetch_key": fetch_key,
402
431
});
403
432
(jar, RenderHtml("authorized", engine, info)).into_response()
+1
who-am-i/templates/authorized.hbs
Reviewed
···
8
8
localStorage.setItem("who-am-i", JSON.stringify({
9
9
result: "success",
10
10
did: {{{json did}}},
11
11
+
token: {{{json token}}},
11
12
fetch_key: {{{json fetch_key}}},
12
13
}));
13
14
window.close();
+4
-4
who-am-i/templates/prompt.hbs
Reviewed
···
55
55
56
56
loaderEl.classList.add('hidden');
57
57
handleViewEl.textContent = `@${handle}`;
58
58
-
allowEl.addEventListener('click', () => shareAllow(handle));
58
58
+
allowEl.addEventListener('click', () => shareAllow(handle, {{{json token}}}));
59
59
})();
60
60
61
61
// anon user
···
108
108
109
109
const handle = await lookUp(parsed.fetch_key);
110
110
111
111
-
shareAllow(handle);
111
111
+
shareAllow(handle, token);
112
112
});
113
113
114
114
async function lookUp(fetch_key) {
···
125
125
return info.handle;
126
126
}
127
127
128
128
-
const shareAllow = handle => {
128
128
+
const shareAllow = (handle, token) => {
129
129
top.postMessage(
130
130
-
{ action: "allow", handle },
130
130
+
{ action: "allow", handle, token },
131
131
{{{json parent_origin}}},
132
132
);
133
133
}