Skip to main content

wasm_oidc_plugin/
session.rs

1// aes_gcm
2use aes_gcm::{
3    aead::{Aead, Generate, Nonce},
4    Aes256Gcm,
5};
6
7// base64
8use base64::{engine::general_purpose::STANDARD_NO_PAD as base64engine, Engine as _};
9
10// log
11use log::debug;
12
13// std
14use std::convert::TryInto;
15use std::fmt::Debug;
16
17// serde
18use serde::{Deserialize, Serialize};
19
20// crate
21use crate::error::PluginError;
22
23/// Struct parse the cookie from the request into a struct in order to access the fields and
24/// also to save the cookie on the client side
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct AuthorizationState {
27    /// Access token to be used for requests to the API
28    pub access_token: String,
29    /// Type of the access token
30    pub token_type: String,
31    /// Time in seconds until the access token expires
32    pub expires_in: u32,
33    /// Refresh token to be used to refresh the access token
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub refresh_token: Option<String>,
36    /// ID token in JWT format
37    pub id_token: String,
38}
39
40/// Struct that holds all information about the current session including the authorization state,
41/// the original path, the PKCE code verifier and the state
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Session {
44    /// Issuer of the OpenID Connect Provider
45    pub issuer: String,
46    /// Authorization state
47    pub authorization_state: Option<AuthorizationState>,
48    /// Original Path to which the user should be redirected after login
49    pub original_path: String,
50    /// PKCE Code Verifier used to generate the PKCE Code Challenge
51    pub code_verifier: String,
52    /// State used to prevent CSRF attacks
53    pub state: String,
54}
55
56impl Session {
57    /// Create a new session, encrypt it and encode it by using the given cipher
58    /// * `cipher` - Cipher used to encrypt the cookie
59    ///
60    /// Returns:
61    /// * the base64 encoded encrypted session data
62    /// * the base64 encoded nonce needed to decrypt it
63    pub fn encrypt_and_encode(&self, cipher: Aes256Gcm) -> Result<(String, String), PluginError> {
64        // Generate nonce and encode it
65        // We generate the nonce here to make sure we never encrypt with the same nonce twice
66        let nonce = Nonce::<Aes256Gcm>::generate();
67        let encoded_nonce = base64engine.encode(nonce.as_slice());
68
69        // Encrypt and encode cookie
70        let encrypted_cookie = cipher.encrypt(&nonce, serde_json::to_vec(&self)?.as_slice())?;
71        let encoded_cookie = base64engine.encode(encrypted_cookie.as_slice());
72
73        debug!("encrypted with nonce: {}", encoded_nonce);
74
75        Ok((encoded_cookie, encoded_nonce))
76    }
77
78    /// Make the cookie values from the encoded cookie by splitting it into chunks of 4000 bytes and
79    /// then building the values to be set in the Set-Cookie headers
80    /// * `encoded_cookie` - Encoded cookie to be split into chunks of 4000 bytes
81    /// * `encoded_nonce` - Base64 encoded nonce needed to decrypt the cookie
82    /// * `cookie_name` - Name of the cookie
83    /// * `cookie_duration_in_s` - Duration of the cookie in seconds
84    pub fn make_cookie_values(
85        encoded_cookie: &str,
86        encoded_nonce: &str,
87        cookie_name: &str,
88        cookie_duration_in_s: u64,
89    ) -> Vec<String> {
90        // Split every 4000 bytes
91        let cookie_parts = encoded_cookie
92            .as_bytes()
93            .chunks(4000)
94            .map(|chunk| std::str::from_utf8(chunk)
95            .expect("auth_cookie is base64 encoded, which means ASCII, which means one character = one byte, so this is valid"));
96
97        let mut cookie_values = vec![];
98
99        // Build the cookie values
100        for (i, cookie_part) in cookie_parts.enumerate() {
101            let cookie_value = format!(
102                "{cookie_name}-{i}={cookie_part}; Path=/; Secure; HttpOnly; Max-Age={cookie_duration_in_s}; SameSite=Lax",
103            );
104            cookie_values.push(cookie_value);
105        }
106
107        let num_parts = cookie_values.len();
108        let num_parts_cookie_value = format!(
109            "{cookie_name}-parts={num_parts}; Path=/; Secure; HttpOnly; Max-Age={cookie_duration_in_s}; SameSite=Lax"
110        );
111        cookie_values.push(num_parts_cookie_value);
112
113        // Build nonce cookie value
114        let nonce_cookie_value = format!(
115            "{cookie_name}-nonce={encoded_nonce}; Path=/; Secure; HttpOnly; Max-Age={cookie_duration_in_s}; SameSite=Lax",
116        );
117        cookie_values.push(nonce_cookie_value);
118
119        cookie_values
120    }
121
122    /// Build Set-Cookie values that expire all session cookie parts immediately.
123    ///
124    /// `num_parts` should be the current `{cookie_name}-parts` value from the request
125    /// (default to at least 1 so `{cookie_name}-0` is cleared even if `-parts` is missing).
126    pub fn clear_cookie_values(cookie_name: &str, num_parts: u8) -> Vec<String> {
127        let num_parts = num_parts.max(1);
128        let mut cookie_values = Vec::with_capacity(num_parts as usize + 2);
129
130        for i in 0..num_parts {
131            cookie_values.push(format!(
132                "{cookie_name}-{i}=; Path=/; Secure; HttpOnly; Max-Age=0; SameSite=Lax"
133            ));
134        }
135        cookie_values.push(format!(
136            "{cookie_name}-parts=; Path=/; Secure; HttpOnly; Max-Age=0; SameSite=Lax"
137        ));
138        cookie_values.push(format!(
139            "{cookie_name}-nonce=; Path=/; Secure; HttpOnly; Max-Age=0; SameSite=Lax"
140        ));
141
142        cookie_values
143    }
144
145    /// Make the Set-Cookie headers from the cookie values
146    /// * `cookie_values` - Cookie values to be set in the Set-Cookie headers
147    pub fn make_set_cookie_headers(cookie_values: &[String]) -> Vec<(&'static str, &str)> {
148        // Build the cookie headers
149        let set_cookie_headers: Vec<(&str, &str)> = cookie_values
150            .iter()
151            .map(|v| ("Set-Cookie", v.as_str()))
152            .collect();
153
154        // Return the cookie headers
155        set_cookie_headers
156    }
157
158    /// Decode cookie, parse into a struct in order to access the fields
159    /// * `encoded_cookie` - Encoded cookie to be decoded and parsed into a struct
160    /// * `cipher` - Cipher used to decrypt the cookie
161    /// * `encoded_nonce` - Nonce used to decrypt the cookie
162    pub fn decode_and_decrypt(
163        encoded_cookie: String,
164        cipher: Aes256Gcm,
165        encoded_nonce: String,
166    ) -> Result<Session, PluginError> {
167        // Decode nonce using base64
168        debug!("decrypting with nonce: {}", encoded_nonce);
169        let decoded_nonce = base64engine.decode(encoded_nonce.as_bytes())?;
170        let nonce: Nonce<Aes256Gcm> = decoded_nonce
171            .as_slice()
172            .try_into()
173            .map_err(|_| PluginError::CookieValidationError("invalid nonce length".to_string()))?;
174
175        // Decode cookie using base64
176        let decoded_cookie = base64engine.decode(encoded_cookie.as_bytes())?;
177
178        // Decrypt with cipher
179        let decrypted_cookie = cipher.decrypt(&nonce, decoded_cookie.as_slice())?;
180
181        // Parse cookie into a struct
182        let state = serde_json::from_slice::<Session>(&decrypted_cookie)?;
183        debug!("state: {state:?}");
184        Ok(state)
185    }
186}