a (hacky, wip) multi-tenant oidc-terminating reverse proxy, written in anger on top of pingora
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>;