diff --git a/Cargo.lock b/Cargo.lock index 82f4bfb..9564c3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,7 +285,9 @@ dependencies = [ "dotenv", "env_logger", "futures", + "getrandom", "http 1.0.0", + "jsonwebtoken", "log", "macros", "maud", @@ -549,8 +551,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -810,6 +814,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.5", + "js-sys", + "ring", + "serde", + "serde_json", +] + [[package]] name = "libc" version = "0.2.151" @@ -1211,6 +1228,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1377,6 +1409,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "strsim" version = "0.10.0" @@ -1689,6 +1727,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 4bf1d9e..22ae6ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,8 @@ percent-encoding = "2.3.1" pkce = { path = "./vendor/pkce" } tower-service = "0.3.2" async-trait = "0.1.80" +jsonwebtoken = { version = "9.3.0", default-features = false } +getrandom = { version = "0.2.11", default-features = false } [features] dev = [] diff --git a/src/server/auth.rs b/src/server/auth.rs index 0fe7c2e..0416c4a 100644 --- a/src/server/auth.rs +++ b/src/server/auth.rs @@ -1,4 +1,7 @@ +use jsonwebtoken::{Algorithm, DecodingKey}; +use serde::Deserialize; use std::borrow::Cow; +use std::env; use time::Duration; use tower_cookies::cookie::SameSite; use tower_cookies::Cookie; @@ -19,3 +22,38 @@ pub fn cookie<'a>( cookie.into() } + +#[derive(Clone)] +pub struct JwtDecoder { + pub secret: jsonwebtoken::DecodingKey, + pub validation: jsonwebtoken::Validation, +} + +impl Default for JwtDecoder { + fn default() -> Self { + let mut validation = jsonwebtoken::Validation::new(Algorithm::HS256); + validation.set_audience(&["authenticated"]); + Self { + validation, + secret: DecodingKey::from_secret(env::var("SUPABASE_JWT_SECRET").unwrap().as_ref()), + } + } +} + +impl JwtDecoder { + pub fn decode(&self, jwt: &str) -> anyhow::Result { + let token = jsonwebtoken::decode::(&jwt, &self.secret, &self.validation)?; + Ok(token.claims) + } +} +type Claims = serde_json::Value; + +#[derive(Deserialize)] +pub struct Claims_ { + aud: String, // Optional. Audience + exp: usize, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp) + //iat: usize, // Optional. Issued at (as UTC timestamp) + iss: String, // Optional. Issuer + //nbf: usize, // Optional. Not Before (as UTC timestamp) + sub: String, +} diff --git a/src/server/state.rs b/src/server/state.rs index 1eee921..5021837 100644 --- a/src/server/state.rs +++ b/src/server/state.rs @@ -1,8 +1,10 @@ +use super::auth::JwtDecoder; use super::ws::WebSockets; use crate::server::supabase::SupabaseAnonClient; use crate::strings::UserName; use crate::strings::{AccessToken, RefreshToken, SessionId, UserId}; use crate::{game::Game, lobby::Lobby}; +use anyhow::anyhow; use serde::Deserialize; use std::sync::{Arc, Mutex}; use tower_cookies::Cookies; @@ -11,23 +13,25 @@ fn new_arc_mutex(item: T) -> Arc> { Arc::new(std::sync::Mutex::new(item)) } -struct SessionInfo { - pub user_id: UserId, - pub access_token: AccessToken, - pub refresh_token: RefreshToken, - pub expires_in: u64, -} - #[derive(Default, Clone)] pub struct AppState { - pub lobby: Arc>, - pub game: Arc>>, + // TODO: remove these + pub lobby: Arc>, + pub game: Arc>>, + // inherently stateless + pub jwt_decoder: JwtDecoder, pub supabase: SupabaseAnonClient, + // stateful, but transient pub ws_connections: Arc>, } impl AppState { pub async fn user_id(&self, cookies: Cookies) -> anyhow::Result { + let cookie = cookies + .get("access_token") + .ok_or(anyhow!("no jwt cookie"))?; + let decoded = self.jwt_decoder.decode(cookie.value()); + anyhow::bail!("TODO: app.user_id()") } pub async fn logout(&self, cookies: Cookies) -> anyhow::Result<()> {