diff --git a/etc/nss_pam_webapi.example.toml b/etc/nss_pam_webapi.example.toml index ad8f0e5b5a01b2d43e6f150a2d6d16c3b071d0c4..d2467b1d4092b13a19a8f8fe2e72be026d93d97a 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 5052284f59c01391b63cac0e698879f0c2f0bddb..e9159cfe05a13b7cb1ea8c2ec200b0ad381efd17 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 30c265a2a0c41102e29ed8db28cb1283f40ef60e..29a42f18b3767416f5523a42d96b51b4280bb31d 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 f679bbcd2fcfad585186e3b28a9bfa6b1e2b119d..2ef3a2b8cdd09b45091af12512805b28d6619fca 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);