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