diff --git a/src/nss.rs b/src/nss.rs
index 118285cb8f93b6a22b6fe3355b123f84bb8dca3e..e35a37ec9707c491b8c5ab25f036fb25c9427c8e 100644
--- a/src/nss.rs
+++ b/src/nss.rs
@@ -24,7 +24,7 @@ use crate::cache::get_context_user;
 
 use crate::logging::setup_log;
 
-use crate::oauth::{get_access_token_client, get_data_jq};
+use crate::oauth::{get_data_jq, get_usable_token};
 use serde::{Serialize, Deserialize};
 
 use libnss::interop::Response;
@@ -69,20 +69,11 @@ impl PasswdHooks for OidcPasswd {
         let conf = nss_hook_prepare();
         info!("[NSS] passwd.get_all_entries called");
 
-        let user_token_res = get_context_user().get_access_token();
-        // FIXME Implement caching of system token
-        let system_token_res = get_access_token_client(&conf, "nss", "", "");
-        let token = match user_token_res {
+        let token = match get_usable_token(&conf, "nss", &mut get_context_user()) {
             Some(t) => t,
             None => {
-                debug!("Could not find a user token to request NSS data; trying client credentials");
-                match system_token_res {
-                    Ok(ct) => ct,
-                    Err(e) => {
-                        error!("Failed to get access token with client credentials: {}", e);
-                        return Response::Unavail;
-                    }
-                }
+                error!("Failed to get usable access token");
+                return Response::Unavail;
             }
         };
 
@@ -100,20 +91,11 @@ impl PasswdHooks for OidcPasswd {
         let conf = nss_hook_prepare();
         info!("[NSS] passwd.get_entry_by_uid called for {}", uid);
 
-        let user_token_res = get_context_user().get_access_token();
-        // FIXME Implement caching of system token
-        let system_token_res = get_access_token_client(&conf, "nss", "", "");
-        let token = match user_token_res {
+        let token = match get_usable_token(&conf, "nss", &mut get_context_user()) {
             Some(t) => t,
             None => {
-                debug!("Could not find a user token to request NSS data; trying client credentials");
-                match system_token_res {
-                    Ok(ct) => ct,
-                    Err(e) => {
-                        error!("Failed to get access token with client credentials: {}", e);
-                        return Response::Unavail;
-                    }
-                }
+                error!("Failed to get usable access token");
+                return Response::Unavail;
             }
         };
 
@@ -131,20 +113,11 @@ impl PasswdHooks for OidcPasswd {
         let conf = nss_hook_prepare();
         info!("[NSS] passwd.get_entry_by_name called for {}", name);
 
-        let user_token_res = get_context_user().get_access_token();
-        // FIXME Implement caching of system token
-        let system_token_res = get_access_token_client(&conf, "nss", "", "");
-        let token = match user_token_res {
+        let token = match get_usable_token(&conf, "nss", &mut get_context_user()) {
             Some(t) => t,
             None => {
-                debug!("Could not find a user token to request NSS data; trying client credentials");
-                match system_token_res {
-                    Ok(ct) => ct,
-                    Err(e) => {
-                        error!("Failed to get access token with client credentials: {}", e);
-                        return Response::Unavail;
-                    }
-                }
+                error!("Failed to get usable access token");
+                return Response::Unavail;
             }
         };
 
diff --git a/src/oauth.rs b/src/oauth.rs
index 8d7d26cbbb39730b3edecd6da2bf376cc860fa7d..6e17ed1966637ee2b0c0a9333fb731a48aadb120 100644
--- a/src/oauth.rs
+++ b/src/oauth.rs
@@ -24,6 +24,8 @@ use crate::config::{
 
 use config::Config;
 
+use crate::cache::UserInfo;
+
 use oauth2::{
     AuthUrl,
     ClientId,
@@ -240,6 +242,37 @@ fn get_jq_prog(conf: &Config, prefix: &str, endpoint: &str, multi: bool, rev: bo
     jq_rs::compile(&jq_code)
 }
 
+/// Get a usable access token for the passed user and prefix
+///
+/// Will return the user's personal token, or a system token, or None,
+/// in that order.
+///
+/// Arguments
+/// ---------
+///
+/// `conf` - reference to the config object
+/// `user` - reference to a user info object to get a token for
+/// `prefix` - current prefix (probably `nss` or `pam`)
+pub fn get_usable_token(conf: &Config, prefix: &str, user: &mut UserInfo) -> Option<BasicTokenResponse> {
+    let user_token_res = user.get_access_token();
+    // FIXME Implement caching of system token
+    let system_token_res = get_access_token_client(&conf, prefix, "", "");
+
+    match user_token_res {
+        Some(t) => Some(t),
+        None => {
+            debug!("Could not find a user token; trying client credentials");
+            match system_token_res {
+                Ok(ct) => Some(ct),
+                Err(e) => {
+                    error!("Failed to get access token with client credentials: {}", e);
+                    None
+                }
+            }
+        }
+    }
+}
+
 /// Retrieve JSON data from a configured endpoint, transforming using configured jq programs
 ///
 /// This function takes a prefix (probably `nss` or `pam`, and an endpoint name to lookup
diff --git a/src/pam.rs b/src/pam.rs
index 364c98d787de3cc4118904326d3d488a8973f648..9088c60cf37808452fb71c22771e3cf56ef3b4f7 100644
--- a/src/pam.rs
+++ b/src/pam.rs
@@ -22,7 +22,7 @@ use crate::config::{
 };
 use config::Config;
 
-use crate::oauth::{get_access_token_password, get_data_jq};
+use crate::oauth::{get_access_token_password, get_data_jq, get_usable_token};
 
 use crate::logging::setup_log;
 
@@ -143,13 +143,14 @@ impl PamServiceModule for PamOidc {
 
         // Retrieve access token (as acquired in the auth stage, probably)
         set_is_getpwnam_safe(false);
-        // FIXME implement fallback to system token
-        let res = get_context_user().get_access_token();
-        set_is_getpwnam_safe(true);
-        let token = match res {
+        let token = match get_usable_token(&conf, "pam", &mut get_context_user()) {
             Some(t) => t,
-            None => return PamError::CRED_UNAVAIL
+            None => {
+                error!("Failed to get usable access token");
+                return PamError::CRED_UNAVAIL;
+            }
         };
+        set_is_getpwnam_safe(true);
 
         // Get and transform data from API
         let data: bool = match get_data_jq(&conf, "pam", "authz", "".to_string(), &token, true) {