a (hacky, wip) multi-tenant oidc-terminating reverse proxy, written in anger on top of pingora
at main 85 lines 3.3 kB view raw
1//! # Cookie handling 2//! 3//! cookies are stored as postcard-encoded ed25519-signed tokens, [Protocol Buffer Tokens], which 4//! this is inspired by. They contain session ids, _not_ the actual access token, which are stored 5//! in an in-memory session store. 6//! 7//! the signing key is generated on server start this does mean server restarts invalidate active 8//! sessions. this is not a big deal for my current usecase, and we could probably do secret 9//! handover or have configurable secrets should the need arise. 10//! 11//! [Protocol Buffer Tokens]: https://fly.io/blog/api-tokens-a-tedious-survey/ 12 13use std::marker::PhantomData; 14 15use base64::Engine as _; 16use base64::prelude::BASE64_STANDARD; 17use color_eyre::Result; 18use serde::de::DeserializeOwned; 19use serde::{Deserialize, Serialize}; 20 21#[derive(Serialize, Deserialize)] 22pub struct Signed<MSG> { 23 signature: ed25519_dalek::Signature, 24 message: Vec<u8>, 25 _kind: PhantomData<MSG>, 26} 27impl<MSG: DeserializeOwned + Versioned> Signed<MSG> { 28 pub fn contents(raw: &str, key: &ed25519_dalek::SigningKey) -> Result<MSG, ()> { 29 let raw = BASE64_STANDARD.decode(raw).map_err(drop)?; 30 // timing threats: technically, someone could try to discover the appropriate shape of our 31 // tokens by seeing if we early-return from the postcard::from_bytes message. 32 // this doesn't seem like a huge threat, since we're not encrypting our cookies, so the 33 // structure is already pretty obvious 34 35 let envelope: Self = postcard::from_bytes(&raw).map_err(drop)?; 36 key.verify(&envelope.message, &envelope.signature) 37 .map_err(drop)?; 38 let (version_num, msg_raw): (u64, _) = 39 postcard::take_from_bytes(&envelope.message).map_err(drop)?; 40 // we're past the signature part, so we know this is ours. 41 // worst we're getting here is an attempt to check if old tokens still work, 42 // so we don't need to be as stressed about timing sensitivity 43 if version_num != MSG::VERSION { 44 return Err(()); 45 } 46 postcard::from_bytes(msg_raw).map_err(drop) 47 } 48} 49 50impl<MSG: Serialize + Versioned> Signed<MSG> { 51 pub fn sign(msg: MSG, key: &ed25519_dalek::SigningKey) -> Result<String, ()> { 52 use ed25519_dalek::ed25519::signature::Signer as _; 53 54 let raw = { 55 let raw = Vec::new(); 56 let raw = postcard::to_extend(&MSG::VERSION, raw).map_err(drop)?; 57 postcard::to_extend(&msg, raw).map_err(drop)? 58 }; 59 let signature = key.sign(&raw); 60 let envelope = Self { 61 signature, 62 message: raw, 63 _kind: Default::default(), 64 }; 65 let raw = postcard::to_extend(&envelope, Vec::new()).map_err(drop)?; 66 Ok(BASE64_STANDARD.encode(raw)) 67 } 68} 69 70pub trait Versioned { 71 const VERSION: u64; 72} 73 74#[derive(Serialize, Deserialize)] 75pub struct CookieMessage { 76 // NB: per rfc:draft-ietf-oauth-v2-1#7.1.3.4, since we're signing our cookies and not encrypting 77 // them, we can't store the access token directly. instead we'll store the session id, which 78 // we'll use to look up the token in a session store. 79 pub session_id: u64, 80} 81impl Versioned for CookieMessage { 82 const VERSION: u64 = 1; 83} 84 85pub type CookieContents = Signed<CookieMessage>;