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

serve jwks for token validation

Changed files
+58 -17
who-am-i
+2 -1
who-am-i/.gitignore
···
-
jwt-key.pem
+
*.pem
+
jwks.json
+28 -9
who-am-i/src/jwt.rs
···
use std::fs;
use std::io::Error as IOError;
use std::path::Path;
+
use std::string::FromUtf8Error;
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),
+
#[error("failed to read private key")]
+
ReadPrivateKey(IOError),
+
#[error("failed to retrieve private key: {0}")]
+
PrivateKey(JWTError),
+
#[error("failed to read private key")]
+
ReadJwks(IOError),
+
#[error("failed to retrieve jwks: {0}")]
+
DecodeJwks(FromUtf8Error),
}
#[derive(Debug, Error)]
pub enum TokenMintingError {
#[error("failed to mint: {0}")]
-
FromEc(#[from] JWTError),
+
EncodingError(#[from] JWTError),
}
pub struct Tokens {
encoding_key: EncodingKey,
+
jwks: String,
}
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 from_files(
+
priv_f: impl AsRef<Path>,
+
jwks_f: impl AsRef<Path>,
+
) -> Result<Self, TokensSetupError> {
+
let private_key_data: Vec<u8> =
+
fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?;
+
let encoding_key =
+
EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?;
+
+
let jwks_data: Vec<u8> = fs::read(jwks_f).map_err(TokensSetupError::ReadJwks)?;
+
let jwks = String::from_utf8(jwks_data).map_err(TokensSetupError::DecodeJwks)?;
+
+
Ok(Self { encoding_key, jwks })
}
pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> {
···
&Claims { sub, exp },
&self.encoding_key,
)?)
+
}
+
+
pub fn jwks(&self) -> String {
+
self.jwks.clone()
}
}
+19 -7
who-am-i/src/main.rs
···
/// eg: `cat /dev/urandom | head -c 64 | base64`
#[arg(long, env)]
app_secret: String,
-
/// path to jwt key (PEM format)
+
/// path to jwt private key (PEM pk8 format)
///
/// generate with:
-
/// ```bash
-
/// openssl ecparam -genkey -noout -name prime256v1 \
-
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-JWT-KEY>.pem
-
/// ```
+
///
+
/// openssl ecparam -genkey -noout -name prime256v1 \
+
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem
#[arg(long)]
-
jwt_key: PathBuf,
+
jwt_private_key: PathBuf,
+
/// path to pubkeys file (jwks format)
+
///
+
/// get pem of pubkey from private key with:
+
///
+
/// openssl ec -in <PATH-TO-PRIV-KEY>.pem -pubout
+
///
+
/// then convert to a jwk, probably with something less sketchy than an [online tool](https://jwkset.com/generate)
+
///
+
/// wrap the jwk in an array, then in an object under "keys":
+
///
+
/// { "keys": [<JWK obj>] }
+
#[arg(long)]
+
jwks: PathBuf,
/// Enable dev mode
///
/// enables automatic template reloading
···
println!(" - {host}");
}
-
let tokens = Tokens::from_file(args.jwt_key).unwrap();
+
let tokens = Tokens::from_files(args.jwt_private_key, args.jwks).unwrap();
if let Err(e) = install_metrics_server() {
eprintln!("failed to install metrics server: {e:?}");
+9
who-am-i/src/server.rs
···
.route("/auth", get(start_oauth))
.route("/authorized", get(complete_oauth))
.route("/disconnect", post(disconnect))
+
.route("/.well-known/jwks.json", get(jwks))
.with_state(state);
let listener = TcpListener::bind("0.0.0.0:9997")
···
let jar = jar.remove(DID_COOKIE_KEY);
(jar, Json(json!({ "ok": true })))
}
+
+
async fn jwks(State(AppState { tokens, .. }): State<AppState>) -> impl IntoResponse {
+
let headers = [
+
(CONTENT_TYPE, "application/json"),
+
// (CACHE_CONTROL, "") // TODO
+
];
+
(headers, tokens.jwks())
+
}