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
serve jwks for token validation
author
phil
date
11 months ago
(Jul 3, 2025, 3:48 PM -0400)
commit
bf9a8fe5
bf9a8fe53eb74c443477ab3ff6a982aec6da9e7d
parent
65d7a109
65d7a109a01b6243d3bcb5dc3d108af731273609
+58
-17
4 changed files
Expand all
Collapse all
Unified
Split
who-am-i
.gitignore
src
jwt.rs
main.rs
server.rs
+2
-1
who-am-i/.gitignore
Reviewed
···
1
1
-
jwt-key.pem
1
1
+
*.pem
2
2
+
jwks.json
+28
-9
who-am-i/src/jwt.rs
Reviewed
···
3
3
use std::fs;
4
4
use std::io::Error as IOError;
5
5
use std::path::Path;
6
6
+
use std::string::FromUtf8Error;
6
7
use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8
use thiserror::Error;
8
9
9
10
#[derive(Debug, Error)]
10
11
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),
12
12
+
#[error("failed to read private key")]
13
13
+
ReadPrivateKey(IOError),
14
14
+
#[error("failed to retrieve private key: {0}")]
15
15
+
PrivateKey(JWTError),
16
16
+
#[error("failed to read private key")]
17
17
+
ReadJwks(IOError),
18
18
+
#[error("failed to retrieve jwks: {0}")]
19
19
+
DecodeJwks(FromUtf8Error),
15
20
}
16
21
17
22
#[derive(Debug, Error)]
18
23
pub enum TokenMintingError {
19
24
#[error("failed to mint: {0}")]
20
20
-
FromEc(#[from] JWTError),
25
25
+
EncodingError(#[from] JWTError),
21
26
}
22
27
23
28
pub struct Tokens {
24
29
encoding_key: EncodingKey,
30
30
+
jwks: String,
25
31
}
26
32
27
33
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 })
34
34
+
pub fn from_files(
35
35
+
priv_f: impl AsRef<Path>,
36
36
+
jwks_f: impl AsRef<Path>,
37
37
+
) -> Result<Self, TokensSetupError> {
38
38
+
let private_key_data: Vec<u8> =
39
39
+
fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?;
40
40
+
let encoding_key =
41
41
+
EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?;
42
42
+
43
43
+
let jwks_data: Vec<u8> = fs::read(jwks_f).map_err(TokensSetupError::ReadJwks)?;
44
44
+
let jwks = String::from_utf8(jwks_data).map_err(TokensSetupError::DecodeJwks)?;
45
45
+
46
46
+
Ok(Self { encoding_key, jwks })
32
47
}
33
48
34
49
pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> {
···
45
60
&Claims { sub, exp },
46
61
&self.encoding_key,
47
62
)?)
63
63
+
}
64
64
+
65
65
+
pub fn jwks(&self) -> String {
66
66
+
self.jwks.clone()
48
67
}
49
68
}
50
69
+19
-7
who-am-i/src/main.rs
Reviewed
···
15
15
/// eg: `cat /dev/urandom | head -c 64 | base64`
16
16
#[arg(long, env)]
17
17
app_secret: String,
18
18
-
/// path to jwt key (PEM format)
18
18
+
/// path to jwt private key (PEM pk8 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
-
/// ```
21
21
+
///
22
22
+
/// openssl ecparam -genkey -noout -name prime256v1 \
23
23
+
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem
25
24
#[arg(long)]
26
26
-
jwt_key: PathBuf,
25
25
+
jwt_private_key: PathBuf,
26
26
+
/// path to pubkeys file (jwks format)
27
27
+
///
28
28
+
/// get pem of pubkey from private key with:
29
29
+
///
30
30
+
/// openssl ec -in <PATH-TO-PRIV-KEY>.pem -pubout
31
31
+
///
32
32
+
/// then convert to a jwk, probably with something less sketchy than an [online tool](https://jwkset.com/generate)
33
33
+
///
34
34
+
/// wrap the jwk in an array, then in an object under "keys":
35
35
+
///
36
36
+
/// { "keys": [<JWK obj>] }
37
37
+
#[arg(long)]
38
38
+
jwks: PathBuf,
27
39
/// Enable dev mode
28
40
///
29
41
/// enables automatic template reloading
···
54
66
println!(" - {host}");
55
67
}
56
68
57
57
-
let tokens = Tokens::from_file(args.jwt_key).unwrap();
69
69
+
let tokens = Tokens::from_files(args.jwt_private_key, args.jwks).unwrap();
58
70
59
71
if let Err(e) = install_metrics_server() {
60
72
eprintln!("failed to install metrics server: {e:?}");
+9
who-am-i/src/server.rs
Reviewed
···
91
91
.route("/auth", get(start_oauth))
92
92
.route("/authorized", get(complete_oauth))
93
93
.route("/disconnect", post(disconnect))
94
94
+
.route("/.well-known/jwks.json", get(jwks))
94
95
.with_state(state);
95
96
96
97
let listener = TcpListener::bind("0.0.0.0:9997")
···
437
438
let jar = jar.remove(DID_COOKIE_KEY);
438
439
(jar, Json(json!({ "ok": true })))
439
440
}
441
441
+
442
442
+
async fn jwks(State(AppState { tokens, .. }): State<AppState>) -> impl IntoResponse {
443
443
+
let headers = [
444
444
+
(CONTENT_TYPE, "application/json"),
445
445
+
// (CACHE_CONTROL, "") // TODO
446
446
+
];
447
447
+
(headers, tokens.jwks())
448
448
+
}