+28
-9
who-am-i/src/jwt.rs
+28
-9
who-am-i/src/jwt.rs
···
3
3
use std::fs;
4
4
use std::io::Error as IOError;
5
5
use std::path::Path;
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
-
#[error(transparent)]
12
-
Io(#[from] IOError),
13
-
#[error("failed to retrieve ec key: {0}")]
14
-
FromEc(JWTError),
12
+
#[error("failed to read private key")]
13
+
ReadPrivateKey(IOError),
14
+
#[error("failed to retrieve private key: {0}")]
15
+
PrivateKey(JWTError),
16
+
#[error("failed to read private key")]
17
+
ReadJwks(IOError),
18
+
#[error("failed to retrieve jwks: {0}")]
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
-
FromEc(#[from] JWTError),
25
+
EncodingError(#[from] JWTError),
21
26
}
22
27
23
28
pub struct Tokens {
24
29
encoding_key: EncodingKey,
30
+
jwks: String,
25
31
}
26
32
27
33
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 })
34
+
pub fn from_files(
35
+
priv_f: impl AsRef<Path>,
36
+
jwks_f: impl AsRef<Path>,
37
+
) -> Result<Self, TokensSetupError> {
38
+
let private_key_data: Vec<u8> =
39
+
fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?;
40
+
let encoding_key =
41
+
EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?;
42
+
43
+
let jwks_data: Vec<u8> = fs::read(jwks_f).map_err(TokensSetupError::ReadJwks)?;
44
+
let jwks = String::from_utf8(jwks_data).map_err(TokensSetupError::DecodeJwks)?;
45
+
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
+
}
64
+
65
+
pub fn jwks(&self) -> String {
66
+
self.jwks.clone()
48
67
}
49
68
}
50
69
+19
-7
who-am-i/src/main.rs
+19
-7
who-am-i/src/main.rs
···
15
15
/// eg: `cat /dev/urandom | head -c 64 | base64`
16
16
#[arg(long, env)]
17
17
app_secret: String,
18
-
/// path to jwt key (PEM format)
18
+
/// path to jwt private key (PEM pk8 format)
19
19
///
20
20
/// generate with:
21
-
/// ```bash
22
-
/// openssl ecparam -genkey -noout -name prime256v1 \
23
-
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-JWT-KEY>.pem
24
-
/// ```
21
+
///
22
+
/// openssl ecparam -genkey -noout -name prime256v1 \
23
+
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem
25
24
#[arg(long)]
26
-
jwt_key: PathBuf,
25
+
jwt_private_key: PathBuf,
26
+
/// path to pubkeys file (jwks format)
27
+
///
28
+
/// get pem of pubkey from private key with:
29
+
///
30
+
/// openssl ec -in <PATH-TO-PRIV-KEY>.pem -pubout
31
+
///
32
+
/// then convert to a jwk, probably with something less sketchy than an [online tool](https://jwkset.com/generate)
33
+
///
34
+
/// wrap the jwk in an array, then in an object under "keys":
35
+
///
36
+
/// { "keys": [<JWK obj>] }
37
+
#[arg(long)]
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
-
let tokens = Tokens::from_file(args.jwt_key).unwrap();
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
+9
who-am-i/src/server.rs
···
91
91
.route("/auth", get(start_oauth))
92
92
.route("/authorized", get(complete_oauth))
93
93
.route("/disconnect", post(disconnect))
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
+
442
+
async fn jwks(State(AppState { tokens, .. }): State<AppState>) -> impl IntoResponse {
443
+
let headers = [
444
+
(CONTENT_TYPE, "application/json"),
445
+
// (CACHE_CONTROL, "") // TODO
446
+
];
447
+
(headers, tokens.jwks())
448
+
}