From 6566c56bb930d1d813ae10e2e275c7b05dc9d7c0 Mon Sep 17 00:00:00 2001
From: Dominik George <dominik.george@teckids.org>
Date: Wed, 19 May 2021 12:37:02 +0200
Subject: [PATCH] [PAM] Implement token persistence in /run, and configurable
 persistence

---
 etc/nss_pam_webapi.example.toml |  9 ++++
 src/cache.rs                    | 85 ++++++++++++++++++++++++++++-----
 src/config.rs                   |  2 +
 src/pam.rs                      |  6 ++-
 4 files changed, 88 insertions(+), 14 deletions(-)

diff --git a/etc/nss_pam_webapi.example.toml b/etc/nss_pam_webapi.example.toml
index ad8f0e5..d2467b1 100644
--- a/etc/nss_pam_webapi.example.toml
+++ b/etc/nss_pam_webapi.example.toml
@@ -19,6 +19,15 @@ token_url = "https://ticdesk-dev.teckids.org/oauth/token/"
 client_id = "Df1cpPEBsbG64oZ1Q1L8NetH1UKNBUyA5qhxg1Zh"
 client_secret = ""
 
+# Persist the user token in the filesystem
+# Possible values:
+#  run - Only persist to runtime directory (probably /run)
+#  home - Also persist to home directory
+# If NSS is in use, the token MUST be persisted to at least /run, or user-only
+# mode will not work.
+# Defaults to only /run
+persist_token = { run = true, home = true }
+
 [nss]
 # Client ID and secret for acquiring OAuth tokens
 # You might want to put these into a separate file nss_pam_webapi.secret.toml!
diff --git a/src/cache.rs b/src/cache.rs
index 5052284..e9159cf 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -283,14 +283,25 @@ impl UserInfo {
         }
     }
 
+    /// Get the full path to a cache file under our prefix in this user's XDG runtime directory
+    fn place_user_runtime_file(&mut self, filename: String) -> Result<PathBuf, io::Error> {
+        match self.get_user_xdg_base_directories() {
+            Ok(b) => b.place_runtime_file(filename),
+            Err(e) => {
+                error!("Error placing runtime file {}: {}", filename, e);
+                Err(e)
+            }
+        }
+    }
+
     /// Get a known access token for this user
     ///
     /// This will use the in-memory token from the `access_token` slot if it is filled,
     /// or attempt to load a token from disk if not
     pub fn get_access_token(&mut self) -> Option<BasicTokenResponse> {
-        // Try to load our acess token if none is known
+        // Try to load our acess token from home directory if none is known
         if self.access_token.is_none() {
-            debug!("No token in memory, trying to load from file");
+            debug!("No token in memory, trying to load from cache file");
             self.drop_privileges().ok();
             // Trying to read even after failed privilege dropping is safe
             self.access_token = match self.place_user_cache_file(USER_TOKEN_FILENAME.to_string()) {
@@ -303,6 +314,21 @@ impl UserInfo {
             restore_privileges();
         }
 
+        // Try to load our acess token from runtime directory if none is known
+        if self.access_token.is_none() {
+            debug!("No token in memory, trying to load from runtime file");
+            self.drop_privileges().ok();
+            // Trying to read even after failed privilege dropping is safe
+            self.access_token = match self.place_user_runtime_file(USER_TOKEN_FILENAME.to_string()) {
+                Ok(path) => match load_json(path) {
+                    Ok(read_token) => Some(read_token),
+                    Err(_) => None
+                },
+                Err(_) => None
+            };
+            restore_privileges();
+        }
+
         match &self.access_token {
             Some(t) => Some(t.clone()),
             None => None
@@ -312,20 +338,53 @@ impl UserInfo {
     /// Set the known access token for this user
     ///
     /// This will store the token in memory in the `access_token` slot, and attempt to
-    /// write the token to disk afterwards
-    pub fn set_access_token(&mut self, token: BasicTokenResponse, persist: bool) -> Result<(), io::Error> {
+    /// write the token to disk afterwards if requested.
+    ///
+    /// Arguments
+    /// ---------
+    ///
+    /// * `token` - the OAuth token to store
+    /// * `persist_run` - whether to store token in XDG_RUNTIME_DIR (probably /run/<uid>)
+    /// * `persist_home` - whether to store token in XDG_CACHE_DIR (probably ~/.cache)
+    pub fn set_access_token(&mut self, token: BasicTokenResponse, persist_run: bool, persist_home: bool) -> Result<(), io::Error> {
         self.access_token = Some(token.clone());
         debug!("Saved token in memory");
 
-        if persist {
-            // Try to write user's token cache file
+        if persist_run {
+            // Try to write user's token cache file to XDG_RUNTIME_DIR
+            // We need to ensure privileges were dropped successfully to avoid symlink attacks
+            // cf. https://capec.mitre.org/data/definitions/132.html
+            let res = match self.drop_privileges() {
+                Ok(_) => match self.place_user_runtime_file(USER_TOKEN_FILENAME.to_string()) {
+                    Ok(path) => {
+                        debug!("Storing token in runtime file");
+                        save_json(path, &token)
+                    },
+                    Err(e) => {
+                        error!("Error getting cache path in runtime directory: {}", e);
+                        Err(e)
+                    }
+                },
+                Err(e) => {
+                    error!("Error dropping privileges to store token in runtime directory: {}", e);
+                    Err(e)
+                }
+            };
+            restore_privileges();
+            if res.is_err() {
+                return res;
+            }
+        }
+
+        if persist_home {
+            // Try to write user's token cache file to XDG_CACHE_DIR
             // We need to ensure privileges were dropped successfully to avoid symlink attacks
             // cf. https://capec.mitre.org/data/definitions/132.html
             let res = match self.drop_privileges() {
                 Ok(_) => match self.place_user_cache_file(USER_TOKEN_FILENAME.to_string()) {
                     Ok(path) => {
-                        debug!("Storing token for in cache file");
-                        save_json(path, token)
+                        debug!("Storing token in cache file");
+                        save_json(path, &token)
                     },
                     Err(e) => {
                         error!("Error getting cache path in user home: {}", e);
@@ -338,11 +397,13 @@ impl UserInfo {
                 }
             };
             restore_privileges();
-
-            res
-        } else {
-            Ok(())
+            if res.is_err() {
+                return res;
+            }
         }
+
+        // If we got here, no error was ever thrown
+        Ok(())
     }
 }
 
diff --git a/src/config.rs b/src/config.rs
index 30c265a..29a42f1 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -25,6 +25,8 @@ pub fn get_config(conf_args: Option<config::Config>) -> config::Config {
     // Preset default configuration
     let mut conf = config::Config::default();
     conf.set("pam.flow", "password").ok();
+    conf.set("pam.persist_token.run", true).ok();
+    conf.set("pam.persist_token.home", false).ok();
 
     // Unwrap passed arguments or use empty fallback
     let conf_args = conf_args.unwrap_or_default();
diff --git a/src/pam.rs b/src/pam.rs
index f679bbc..2ef3a2b 100644
--- a/src/pam.rs
+++ b/src/pam.rs
@@ -105,14 +105,16 @@ impl PamServiceModule for PamOidc {
                     // 1. ...mark getpwnam unsafe (prevent cache code from calling it)
                     set_is_getpwnam_safe(false);
                     // 2. ...store the access token in memory
-                    get_context_user().set_access_token(t.clone(), false).ok();
+                    get_context_user().set_access_token(t.clone(), false, false).ok();
                     // 3. ...call getpwnam ourselves without having the cache object locked
                     let passwd = getpwnam_safe(username.to_string());
                     if passwd.is_ok() {
                         // 4. ...if getpwnam was successful, store the token again (this time,
                         //    modulo other errors, it will go through to $HOME)
                         get_context_user().set_passwd(passwd.unwrap());
-                        get_context_user().set_access_token(t.clone(), true).ok();
+                        let persist_run = conf.get_bool("pam.persist_token.run").unwrap();
+                        let persist_home = conf.get_bool("pam.persist_token.home").unwrap();
+                        get_context_user().set_access_token(t.clone(), persist_run, persist_home).ok();
                     }
                     // 5. ...unlock getpwnam again (somewhat unnecessary)
                     set_is_getpwnam_safe(true);
-- 
GitLab