+59
Cargo.lock
+59
Cargo.lock
···
1697
1697
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
1698
1698
dependencies = [
1699
1699
"cfg-if",
1700
+
"js-sys",
1700
1701
"libc",
1701
1702
"wasi 0.11.0+wasi-snapshot-preview1",
1703
+
"wasm-bindgen",
1702
1704
]
1703
1705
1704
1706
[[package]]
···
2455
2457
]
2456
2458
2457
2459
[[package]]
2460
+
name = "jsonwebtoken"
2461
+
version = "9.3.1"
2462
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2463
+
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
2464
+
dependencies = [
2465
+
"base64 0.22.1",
2466
+
"js-sys",
2467
+
"pem",
2468
+
"ring",
2469
+
"serde",
2470
+
"serde_json",
2471
+
"simple_asn1",
2472
+
]
2473
+
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
+
name = "num-bigint"
2976
+
version = "0.4.6"
2977
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2978
+
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
2979
+
dependencies = [
2980
+
"num-integer",
2981
+
"num-traits",
2982
+
]
2983
+
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
+
name = "num-integer"
3002
+
version = "0.1.46"
3003
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3004
+
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
3005
+
dependencies = [
3006
+
"num-traits",
3007
+
]
3008
+
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
+
name = "pem"
3199
+
version = "3.0.5"
3200
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3201
+
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
3202
+
dependencies = [
3203
+
"base64 0.22.1",
3204
+
"serde",
3205
+
]
3206
+
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
+
]
4134
+
4135
+
[[package]]
4136
+
name = "simple_asn1"
4137
+
version = "0.6.3"
4138
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4139
+
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
4140
+
dependencies = [
4141
+
"num-bigint",
4142
+
"num-traits",
4143
+
"thiserror 2.0.12",
4144
+
"time",
4087
4145
]
4088
4146
4089
4147
[[package]]
···
5087
5145
"dashmap",
5088
5146
"handlebars",
5089
5147
"hickory-resolver",
5148
+
"jsonwebtoken",
5090
5149
"metrics",
5091
5150
"metrics-exporter-prometheus 0.17.2",
5092
5151
"rand 0.9.1",
+1
who-am-i/.gitignore
+1
who-am-i/.gitignore
···
1
+
jwt-key.pem
+1
who-am-i/Cargo.toml
+1
who-am-i/Cargo.toml
+2
who-am-i/demo/index.html
+2
who-am-i/demo/index.html
···
12
12
13
13
<body>
14
14
<h1>hey <span id="who"></span></h1>
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
+
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
+55
who-am-i/src/jwt.rs
···
1
+
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError};
2
+
use serde::Serialize;
3
+
use std::fs;
4
+
use std::io::Error as IOError;
5
+
use std::path::Path;
6
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
+
use thiserror::Error;
8
+
9
+
#[derive(Debug, Error)]
10
+
pub enum TokensSetupError {
11
+
#[error(transparent)]
12
+
Io(#[from] IOError),
13
+
#[error("failed to retrieve ec key: {0}")]
14
+
FromEc(JWTError),
15
+
}
16
+
17
+
#[derive(Debug, Error)]
18
+
pub enum TokenMintingError {
19
+
#[error("failed to mint: {0}")]
20
+
FromEc(#[from] JWTError),
21
+
}
22
+
23
+
pub struct Tokens {
24
+
encoding_key: EncodingKey,
25
+
}
26
+
27
+
impl Tokens {
28
+
pub fn from_file(f: impl AsRef<Path>) -> Result<Self, TokensSetupError> {
29
+
let data: Vec<u8> = fs::read(f)?;
30
+
let encoding_key = EncodingKey::from_ec_pem(&data).map_err(TokensSetupError::FromEc)?;
31
+
Ok(Self { encoding_key })
32
+
}
33
+
34
+
pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> {
35
+
let sub = t.to_string();
36
+
37
+
let dt_now = SystemTime::now()
38
+
.duration_since(UNIX_EPOCH)
39
+
.expect("unix epoch is in the past");
40
+
let dt_exp = dt_now + Duration::from_secs(30 * 86_400);
41
+
let exp = dt_exp.as_secs();
42
+
43
+
Ok(encode(
44
+
&Header::new(Algorithm::ES256),
45
+
&Claims { sub, exp },
46
+
&self.encoding_key,
47
+
)?)
48
+
}
49
+
}
50
+
51
+
#[derive(Debug, Serialize)]
52
+
struct Claims {
53
+
sub: String,
54
+
exp: u64,
55
+
}
+2
who-am-i/src/lib.rs
+2
who-am-i/src/lib.rs
+22
-3
who-am-i/src/main.rs
+22
-3
who-am-i/src/main.rs
···
1
1
use clap::{ArgAction, Parser};
2
-
use metrics_exporter_prometheus::{PrometheusBuilder, BuildError as PromBuildError};
2
+
use metrics_exporter_prometheus::{BuildError as PromBuildError, PrometheusBuilder};
3
+
use std::path::PathBuf;
3
4
use tokio_util::sync::CancellationToken;
4
-
use who_am_i::serve;
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
+
/// path to jwt key (PEM format)
19
+
///
20
+
/// generate with:
21
+
/// ```bash
22
+
/// openssl ecparam -genkey -noout -name prime256v1 \
23
+
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-JWT-KEY>.pem
24
+
/// ```
25
+
#[arg(long)]
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
+
let tokens = Tokens::from_file(args.jwt_key).unwrap();
58
+
47
59
if let Err(e) = install_metrics_server() {
48
60
eprintln!("failed to install metrics server: {e:?}");
49
61
};
50
62
51
-
serve(shutdown, args.app_secret, args.allowed_hosts, args.dev).await;
63
+
serve(
64
+
shutdown,
65
+
args.app_secret,
66
+
tokens,
67
+
args.allowed_hosts,
68
+
args.dev,
69
+
)
70
+
.await;
52
71
}
53
72
54
73
fn install_metrics_server() -> Result<(), PromBuildError> {
+30
-1
who-am-i/src/server.rs
+30
-1
who-am-i/src/server.rs
···
22
22
use tokio_util::sync::CancellationToken;
23
23
use url::Url;
24
24
25
-
use crate::{ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError};
25
+
use crate::{
26
+
ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError, Tokens,
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
+
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
+
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
+
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
+
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
+
223
+
let token = match tokens.mint(&*did) {
224
+
Ok(t) => t,
225
+
Err(e) => {
226
+
eprintln!("failed to create JWT: {e:?}");
227
+
return err("failed to create JWT", false);
228
+
}
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
+
"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
+
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
+
let token = match tokens.mint(&*did) {
406
+
Ok(t) => t,
407
+
Err(e) => {
408
+
eprintln!("failed to create JWT: {e:?}");
409
+
return err(
410
+
StatusCode::INTERNAL_SERVER_ERROR,
411
+
"fail",
412
+
"failed to create JWT",
413
+
);
414
+
}
415
+
};
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
+
"token": token,
401
430
"fetch_key": fetch_key,
402
431
});
403
432
(jar, RenderHtml("authorized", engine, info)).into_response()
+4
-4
who-am-i/templates/prompt.hbs
+4
-4
who-am-i/templates/prompt.hbs
···
55
55
56
56
loaderEl.classList.add('hidden');
57
57
handleViewEl.textContent = `@${handle}`;
58
-
allowEl.addEventListener('click', () => shareAllow(handle));
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
-
shareAllow(handle);
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
-
const shareAllow = handle => {
128
+
const shareAllow = (handle, token) => {
129
129
top.postMessage(
130
-
{ action: "allow", handle },
130
+
{ action: "allow", handle, token },
131
131
{{{json parent_origin}}},
132
132
);
133
133
}