Repository: shuttle-hq/shuttle (Rust)

Issue:
[Feature]: allow resetting a users API-key
### Describe the feature

A user should be able to reset their api-key, this is important functionality to have in case users leak their key. This should generate a new api-key and persist it in the users table.

### Questions

- A JWT will be cached with the api-key or a cookie sid as the key for 10 minutes, should we also clear this? Can we?
- Should this only be possible through the console?

### Tasks

- [x] Add a new reset endpoint to auth
- [x] Update gateway to forward reset requests to auth
- [x] Clean the cache in gateway
- [ ] Add a subcommand to the cli

### Suggestion or Example of how the feature would be used

_No response_

### Duplicate declaration

- [X] I have searched the issues and this feature has not been requested before.


Discussion: Shuttle Batch 2.0 project https://shuttle-hq.notion.site/535743328783490baacd6fbb7befdd5d?v=f0988ae59d8f47e788bed373047a4255&p=916eeba051f74a07a27f20efb8711bc8&pm=s. 

Picked by @jonaro00 and @hseeberger.
Looking at the code it seems to me that there is only one API key per user. Is that correct?
Yes, that is correct. :+1: 
I saw a suggestion to allow users to have more than one key, but we can wait with that until later if deemed appropriate.



Fix (patch):
diff --git a/Cargo.toml b/Cargo.toml
index e5e3811496..c6409c80c2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,6 +36,7 @@ shuttle-service = { path = "service", version = "0.16.0" }
 anyhow = "1.0.66"
 async-trait = "0.1.58"
 axum = { version = "0.6.0", default-features = false }
+axum-sessions = "0.4.1"
 base64 = "0.13.1"
 bollard = "0.14.0"
 bytes = "1.3.0"
diff --git a/auth/Cargo.toml b/auth/Cargo.toml
index 8b3d7171ad..d9a5715ded 100644
--- a/auth/Cargo.toml
+++ b/auth/Cargo.toml
@@ -9,7 +9,7 @@ repository.workspace = true
 anyhow = { workspace = true }
 async-trait = { workspace = true }
 axum = { workspace = true, features = ["headers"] }
-axum-sessions = "0.4.1"
+axum-sessions = { workspace = true }
 clap = { workspace = true }
 http = { workspace = true }
 jsonwebtoken = { workspace = true }
diff --git a/auth/src/api/builder.rs b/auth/src/api/builder.rs
index d460628465..fb6c7aed6f 100644
--- a/auth/src/api/builder.rs
+++ b/auth/src/api/builder.rs
@@ -3,7 +3,7 @@ use std::{net::SocketAddr, sync::Arc};
 use axum::{
     extract::FromRef,
     middleware::from_extractor,
-    routing::{get, post},
+    routing::{get, post, put},
     Router, Server,
 };
 use axum_sessions::{async_session::MemoryStore, SessionLayer};
@@ -22,7 +22,8 @@ use crate::{
 };
 
 use super::handlers::{
-    convert_cookie, convert_key, get_public_key, get_user, login, logout, post_user, refresh_token,
+    convert_cookie, convert_key, get_public_key, get_user, login, logout, post_user,
+    put_user_reset_key, refresh_token,
 };
 
 pub type UserManagerState = Arc<Box<dyn UserManagement>>;
@@ -71,6 +72,7 @@ impl ApiBuilder {
             .route("/public-key", get(get_public_key))
             .route("/users/:account_name", get(get_user))
             .route("/users/:account_name/:account_tier", post(post_user))
+            .route("/users/reset-api-key", put(put_user_reset_key))
             .route_layer(from_extractor::<Metrics>())
             .layer(
                 TraceLayer::new(|request| {
diff --git a/auth/src/api/handlers.rs b/auth/src/api/handlers.rs
index e96325ed5c..9c3e5eed80 100644
--- a/auth/src/api/handlers.rs
+++ b/auth/src/api/handlers.rs
@@ -39,6 +39,23 @@ pub(crate) async fn post_user(
     Ok(Json(user.into()))
 }
 
+pub(crate) async fn put_user_reset_key(
+    session: ReadableSession,
+    State(user_manager): State<UserManagerState>,
+    key: Option<Key>,
+) -> Result<(), Error> {
+    let account_name = match session.get::<String>("account_name") {
+        Some(account_name) => account_name.into(),
+
+        None => match key {
+            Some(key) => user_manager.get_user_by_key(key.into()).await?.name,
+            None => return Err(Error::Unauthorized),
+        },
+    };
+
+    user_manager.reset_key(account_name).await
+}
+
 pub(crate) async fn login(
     mut session: WritableSession,
     State(user_manager): State<UserManagerState>,
@@ -64,12 +81,12 @@ pub(crate) async fn convert_cookie(
     session: ReadableSession,
     State(key_manager): State<KeyManagerState>,
 ) -> Result<Json<shuttle_common::backends::auth::ConvertResponse>, StatusCode> {
-    let account_name: String = session
-        .get("account_name")
+    let account_name = session
+        .get::<String>("account_name")
         .ok_or(StatusCode::UNAUTHORIZED)?;
 
-    let account_tier: AccountTier = session
-        .get("account_tier")
+    let account_tier = session
+        .get::<AccountTier>("account_tier")
         .ok_or(StatusCode::UNAUTHORIZED)?;
 
     let claim = Claim::new(account_name, account_tier.into());
@@ -92,7 +109,7 @@ pub(crate) async fn convert_key(
     let User {
         name, account_tier, ..
     } = user_manager
-        .get_user_by_key(key.as_ref().clone())
+        .get_user_by_key(key.into())
         .await
         .map_err(|_| StatusCode::UNAUTHORIZED)?;
 
diff --git a/auth/src/user.rs b/auth/src/user.rs
index 7666927f02..d4d89f6a7e 100644
--- a/auth/src/user.rs
+++ b/auth/src/user.rs
@@ -22,6 +22,7 @@ pub trait UserManagement: Send + Sync {
     async fn create_user(&self, name: AccountName, tier: AccountTier) -> Result<User, Error>;
     async fn get_user(&self, name: AccountName) -> Result<User, Error>;
     async fn get_user_by_key(&self, key: ApiKey) -> Result<User, Error>;
+    async fn reset_key(&self, name: AccountName) -> Result<(), Error>;
 }
 
 #[derive(Clone)]
@@ -69,6 +70,23 @@ impl UserManagement for UserManager {
             })
             .ok_or(Error::UserNotFound)
     }
+
+    async fn reset_key(&self, name: AccountName) -> Result<(), Error> {
+        let key = ApiKey::generate();
+
+        let rows_affected = query("UPDATE users SET key = ?1 WHERE account_name = ?2")
+            .bind(&key)
+            .bind(&name)
+            .execute(&self.pool)
+            .await?
+            .rows_affected();
+
+        if rows_affected > 0 {
+            Ok(())
+        } else {
+            Err(Error::UserNotFound)
+        }
+    }
 }
 
 #[derive(Clone, Deserialize, PartialEq, Eq, Serialize, Debug)]
@@ -106,7 +124,7 @@ where
         let user_manager: UserManagerState = UserManagerState::from_ref(state);
 
         let user = user_manager
-            .get_user_by_key(key.as_ref().clone())
+            .get_user_by_key(key.into())
             .await
             // Absorb any error into `Unauthorized`
             .map_err(|_| Error::Unauthorized)?;
@@ -128,13 +146,12 @@ impl From<User> for shuttle_common::models::user::Response {
     }
 }
 
-/// A wrapper around [ApiKey] so we can implement [FromRequestParts]
-/// for it.
+/// A wrapper around [ApiKey] so we can implement [FromRequestParts] for it.
 pub struct Key(ApiKey);
 
-impl AsRef<ApiKey> for Key {
-    fn as_ref(&self) -> &ApiKey {
-        &self.0
+impl From<Key> for ApiKey {
+    fn from(key: Key) -> Self {
+        key.0
     }
 }
 
@@ -192,11 +209,17 @@ impl From<AccountTier> for Vec<Scope> {
 #[sqlx(transparent)]
 pub struct AccountName(String);
 
+impl From<String> for AccountName {
+    fn from(s: String) -> Self {
+        Self(s)
+    }
+}
+
 impl FromStr for AccountName {
     type Err = Error;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(Self(s.to_string()))
+        Ok(s.to_string().into())
     }
 }
 
diff --git a/cargo-shuttle/src/args.rs b/cargo-shuttle/src/args.rs
index 9cae82aa78..1cf9844c5b 100644
--- a/cargo-shuttle/src/args.rs
+++ b/cargo-shuttle/src/args.rs
@@ -124,7 +124,7 @@ pub enum Command {
     /// Login to the shuttle platform
     Login(LoginArgs),
     /// Log out of the shuttle platform
-    Logout,
+    Logout(LogoutArgs),
     /// Generate shell completions
     Generate {
         /// Which shell
@@ -188,6 +188,12 @@ pub struct LoginArgs {
     pub api_key: Option<String>,
 }
 
+#[derive(Parser, Clone, Debug)]
+pub struct LogoutArgs {
+    /// Reset the API key before logging out
+    #[arg(long)]
+    pub reset_api_key: bool,
+}
 #[derive(Parser)]
 pub struct DeployArgs {
     /// Allow deployment with uncommited files
diff --git a/cargo-shuttle/src/client.rs b/cargo-shuttle/src/client.rs
index ca72a1cda5..37625f4330 100644
--- a/cargo-shuttle/src/client.rs
+++ b/cargo-shuttle/src/client.rs
@@ -16,6 +16,7 @@ use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream};
 use tracing::error;
 use uuid::Uuid;
 
+#[derive(Clone)]
 pub struct Client {
     api_url: ApiUrl,
     api_key: Option<ApiKey>,
@@ -201,6 +202,11 @@ impl Client {
         self.get(path).await
     }
 
+    pub async fn reset_api_key(&self) -> Result<Response> {
+        self.put("/users/reset-api-key".into(), Option::<()>::None)
+            .await
+    }
+
     async fn ws_get(&self, path: String) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
         let ws_scheme = self.api_url.clone().replace("http", "ws");
         let url = format!("{ws_scheme}{path}");
@@ -253,6 +259,22 @@ impl Client {
         Ok(builder.send().await?)
     }
 
+    async fn put<T: Serialize>(&self, path: String, body: Option<T>) -> Result<Response> {
+        let url = format!("{}{}", self.api_url, path);
+
+        let mut builder = Self::get_retry_client().put(url);
+
+        builder = self.set_builder_auth(builder);
+
+        if let Some(body) = body {
+            let body = serde_json::to_string(&body)?;
+            builder = builder.body(body);
+            builder = builder.header("Content-Type", "application/json");
+        }
+
+        Ok(builder.send().await?)
+    }
+
     async fn delete<M>(&self, path: String) -> Result<M>
     where
         M: for<'de> Deserialize<'de>,
diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs
index 49ee556dae..da11acca68 100644
--- a/cargo-shuttle/src/lib.rs
+++ b/cargo-shuttle/src/lib.rs
@@ -4,6 +4,7 @@ pub mod config;
 mod init;
 mod provisioner_server;
 
+use args::LogoutArgs;
 use indicatif::ProgressBar;
 use shuttle_common::claims::{ClaimService, InjectPropagation};
 use shuttle_common::models::deployment::get_deployments_table;
@@ -29,7 +30,7 @@ use std::path::{Path, PathBuf};
 use std::process::exit;
 use std::str::FromStr;
 
-use anyhow::{bail, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 pub use args::{Args, Command, DeployArgs, InitArgs, LoginArgs, ProjectArgs, RunArgs};
 use cargo_metadata::Message;
 use clap::CommandFactory;
@@ -57,6 +58,8 @@ use crate::provisioner_server::LocalProvisioner;
 
 const VERSION: &str = env!("CARGO_PKG_VERSION");
 const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");
+const SHUTTLE_LOGIN_URL: &str = "https://shuttle.rs/login";
+const SHUTTLE_GH_ISSUE_URL: &str = "https://github.com/shuttle-hq/shuttle/issues/new";
 
 pub struct Shuttle {
     ctx: RequestContext,
@@ -70,6 +73,8 @@ impl Shuttle {
 
     pub async fn run(mut self, mut args: Args) -> Result<CommandOutcome> {
         trace!("running local client");
+
+        // All commands that need to know which project is being handled
         if matches!(
             args.cmd,
             Command::Deploy(..)
@@ -98,11 +103,11 @@ impl Shuttle {
             Command::Init(init_args) => self.init(init_args, args.project_args).await,
             Command::Generate { shell, output } => self.complete(shell, output).await,
             Command::Login(login_args) => self.login(login_args).await,
-            Command::Logout => self.logout().await,
+            Command::Logout(logout_args) => self.logout(logout_args).await,
             Command::Feedback => self.feedback().await,
             Command::Run(run_args) => self.local_run(run_args).await,
             Command::Deploy(deploy_args) => {
-                return self.deploy(deploy_args, &self.client()?).await;
+                return self.deploy(&self.client()?, deploy_args).await;
             }
             Command::Status => self.status(&self.client()?).await,
             Command::Logs { id, latest, follow } => {
@@ -249,10 +254,9 @@ impl Shuttle {
 
     /// Provide feedback on GitHub.
     async fn feedback(&self) -> Result<()> {
-        let url = "https://github.com/shuttle-hq/shuttle/issues/new";
-        let _ = webbrowser::open(url);
+        let _ = webbrowser::open(SHUTTLE_GH_ISSUE_URL);
+        println!("If your browser did not open automatically, go to {SHUTTLE_GH_ISSUE_URL}");
 
-        println!("\nIf your browser did not open automatically, go to {url}");
         Ok(())
     }
 
@@ -261,10 +265,8 @@ impl Shuttle {
         let api_key_str = match login_args.api_key {
             Some(api_key) => api_key,
             None => {
-                let url = "https://shuttle.rs/login";
-                let _ = webbrowser::open(url);
-
-                println!("If your browser did not automatically open, go to {url}");
+                let _ = webbrowser::open(SHUTTLE_LOGIN_URL);
+                println!("If your browser did not automatically open, go to {SHUTTLE_LOGIN_URL}");
 
                 Password::with_theme(&ColorfulTheme::default())
                     .with_prompt("API key")
@@ -280,13 +282,28 @@ impl Shuttle {
         Ok(())
     }
 
-    async fn logout(&mut self) -> Result<()> {
+    async fn logout(&mut self, logout_args: LogoutArgs) -> Result<()> {
+        if logout_args.reset_api_key {
+            self.reset_api_key(&self.client()?).await?;
+            println!("Successfully reset the API key.");
+            println!(" -> Go to {SHUTTLE_LOGIN_URL} to get a new one.\n");
+        }
         self.ctx.clear_api_key()?;
-
         println!("Successfully logged out of shuttle.");
+
         Ok(())
     }
 
+    async fn reset_api_key(&self, client: &Client) -> Result<()> {
+        client.reset_api_key().await.and_then(|res| {
+            if res.status().is_success() {
+                Ok(())
+            } else {
+                Err(anyhow!("Resetting API key failed."))
+            }
+        })
+    }
+
     async fn stop(&self, client: &Client) -> Result<()> {
         let proj_name = self.ctx.project_name();
         let mut service = client.stop_service(proj_name).await?;
@@ -867,7 +884,7 @@ impl Shuttle {
         Ok(())
     }
 
-    async fn deploy(&self, args: DeployArgs, client: &Client) -> Result<CommandOutcome> {
+    async fn deploy(&self, client: &Client, args: DeployArgs) -> Result<CommandOutcome> {
         if !args.allow_dirty {
             self.is_dirty()?;
         }
diff --git a/gateway/src/api/auth_layer.rs b/gateway/src/api/auth_layer.rs
index b96b3a1a37..1a59abf40d 100644
--- a/gateway/src/api/auth_layer.rs
+++ b/gateway/src/api/auth_layer.rs
@@ -1,4 +1,4 @@
-use std::{convert::Infallible, net::Ipv4Addr, sync::Arc, time::Duration};
+use std::{convert::Infallible, fmt::Debug, net::Ipv4Addr, sync::Arc, time::Duration};
 
 use axum::{
     body::{boxed, HttpBody},
@@ -73,6 +73,7 @@ pub struct ShuttleAuthService<S> {
 impl<S> Service<Request<Body>> for ShuttleAuthService<S>
 where
     S: Service<Request<Body>, Response = Response> + Send + Clone + 'static,
+    S::Error: Debug,
     S::Future: Send + 'static,
 {
     type Response = S::Response;
@@ -114,13 +115,18 @@ where
             other => other.starts_with("/users"),
         };
 
+        // If /users/reset-api-key is called, invalidate the cached JWT.
+        if req.uri().path() == "/users/reset-api-key" {
+            if let Some((cache_key, _)) = cache_key_and_token_req(&req) {
+                self.cache_manager.invalidate(&cache_key);
+            };
+        }
+
         // If logout is called, invalidate the cached JWT for the callers cookie.
         if req.uri().path() == "/logout" {
-            let cache_manager = self.cache_manager.clone();
-
             if let Ok(Some(cookie)) = req.headers().typed_try_get::<Cookie>() {
-                if let Some(key) = cookie.get("shuttle.sid").map(|id| id.to_string()) {
-                    cache_manager.invalidate(&key);
+                if let Some(cache_key) = cookie.get("shuttle.sid").map(|id| id.to_string()) {
+                    self.cache_manager.invalidate(&cache_key);
                 }
             };
         }
@@ -165,109 +171,91 @@ where
             let mut this = self.clone();
 
             Box::pin(async move {
-                let mut auth_details = None;
-                let mut cache_key = None;
-
-                if let Some(bearer) = req.headers().typed_get::<Authorization<Bearer>>() {
-                    cache_key = Some(bearer.token().trim().to_string());
-                    auth_details = Some(make_token_request("/auth/key", bearer));
-                }
-
-                if let Some(cookie) = req.headers().typed_get::<Cookie>() {
-                    if let Some(id) = cookie.get("shuttle.sid") {
-                        cache_key = Some(id.to_string());
-                        auth_details = Some(make_token_request("/auth/session", cookie));
-                    };
-                }
-
                 // Only if there is something to upgrade
-                if let Some(token_request) = auth_details {
+                if let Some((cache_key, token_request)) = cache_key_and_token_req(&req) {
                     let target_url = this.auth_uri.to_string();
 
-                    if let Some(key) = cache_key {
-                        // Check if the token is cached.
-                        if let Some(token) = this.cache_manager.get(&key) {
-                            trace!("JWT cache hit, setting token from cache on request");
-
-                            // Token is cached and not expired, return it in the response.
-                            req.headers_mut()
-                                .typed_insert(Authorization::bearer(&token).unwrap());
-                        } else {
-                            trace!("JWT cache missed, sending convert token request");
-
-                            // Token is not in the cache, send a convert request.
-                            let token_response = match PROXY_CLIENT
-                                .call(Ipv4Addr::LOCALHOST.into(), &target_url, token_request)
-                                .await
-                            {
-                                Ok(res) => res,
-                                Err(error) => {
-                                    error!(?error, "failed to call authentication service");
-
-                                    return Ok(Response::builder()
-                                        .status(StatusCode::SERVICE_UNAVAILABLE)
-                                        .body(boxed(Body::empty()))
-                                        .unwrap());
-                                }
-                            };
-
-                            // Bubble up auth errors
-                            if token_response.status() != StatusCode::OK {
-                                let (parts, body) = token_response.into_parts();
-                                let body = <Body as HttpBody>::map_err(body, axum::Error::new)
-                                    .boxed_unsync();
-
-                                return Ok(Response::from_parts(parts, body));
+                    // Check if the token is cached.
+                    if let Some(token) = this.cache_manager.get(&cache_key) {
+                        trace!("JWT cache hit, setting token from cache on request");
+
+                        // Token is cached and not expired, return it in the response.
+                        req.headers_mut()
+                            .typed_insert(Authorization::bearer(&token).unwrap());
+                    } else {
+                        trace!("JWT cache missed, sending convert token request");
+
+                        // Token is not in the cache, send a convert request.
+                        let token_response = match PROXY_CLIENT
+                            .call(Ipv4Addr::LOCALHOST.into(), &target_url, token_request)
+                            .await
+                        {
+                            Ok(res) => res,
+                            Err(error) => {
+                                error!(?error, "failed to call authentication service");
+
+                                return Ok(Response::builder()
+                                    .status(StatusCode::SERVICE_UNAVAILABLE)
+                                    .body(boxed(Body::empty()))
+                                    .unwrap());
                             }
+                        };
+
+                        // Bubble up auth errors
+                        if token_response.status() != StatusCode::OK {
+                            let (parts, body) = token_response.into_parts();
+                            let body = body.map_err(axum::Error::new).boxed_unsync();
 
-                            let body = match hyper::body::to_bytes(token_response.into_body()).await
-                            {
-                                Ok(body) => body,
-                                Err(error) => {
-                                    error!(
-                                        error = &error as &dyn std::error::Error,
-                                        "failed to get response body"
-                                    );
-
-                                    return Ok(Response::builder()
-                                        .status(StatusCode::INTERNAL_SERVER_ERROR)
-                                        .body(boxed(Body::empty()))
-                                        .unwrap());
-                                }
-                            };
-
-                            let response: ConvertResponse = match serde_json::from_slice(&body) {
-                                Ok(response) => response,
-                                Err(error) => {
-                                    error!(
-                                        error = &error as &dyn std::error::Error,
-                                        "failed to convert body to ConvertResponse"
-                                    );
-
-                                    return Ok(Response::builder()
-                                        .status(StatusCode::INTERNAL_SERVER_ERROR)
-                                        .body(boxed(Body::empty()))
-                                        .unwrap());
-                                }
-                            };
-
-                            this.cache_manager.insert(
-                                key.as_str(),
-                                response.token.clone(),
-                                Duration::from_secs(CACHE_MINUTES * 60),
-                            );
-
-                            trace!("token inserted in cache, request proceeding");
-                            req.headers_mut()
-                                .typed_insert(Authorization::bearer(&response.token).unwrap());
+                            return Ok(Response::from_parts(parts, body));
                         }
+
+                        let body = match hyper::body::to_bytes(token_response.into_body()).await {
+                            Ok(body) => body,
+                            Err(error) => {
+                                error!(
+                                    error = &error as &dyn std::error::Error,
+                                    "failed to get response body"
+                                );
+
+                                return Ok(Response::builder()
+                                    .status(StatusCode::INTERNAL_SERVER_ERROR)
+                                    .body(boxed(Body::empty()))
+                                    .unwrap());
+                            }
+                        };
+
+                        let response = match serde_json::from_slice::<ConvertResponse>(&body) {
+                            Ok(response) => response,
+                            Err(error) => {
+                                error!(
+                                    error = &error as &dyn std::error::Error,
+                                    "failed to convert body to ConvertResponse"
+                                );
+
+                                return Ok(Response::builder()
+                                    .status(StatusCode::INTERNAL_SERVER_ERROR)
+                                    .body(boxed(Body::empty()))
+                                    .unwrap());
+                            }
+                        };
+
+                        let bearer = Authorization::bearer(&response.token).expect("bearer token");
+
+                        this.cache_manager.insert(
+                            cache_key.as_str(),
+                            response.token,
+                            Duration::from_secs(CACHE_MINUTES * 60),
+                        );
+
+                        trace!("token inserted in cache, request proceeding");
+                        req.headers_mut().typed_insert(bearer);
                     };
                 }
 
                 match this.inner.call(req).await {
                     Ok(response) => Ok(response),
-                    Err(_) => {
-                        error!("unexpected internal error from gateway");
+                    Err(error) => {
+                        error!(?error, "unexpected internal error from gateway");
 
                         Ok(Response::builder()
                             .status(StatusCode::SERVICE_UNAVAILABLE)
@@ -280,6 +268,25 @@ where
     }
 }
 
+fn cache_key_and_token_req(req: &Request<Body>) -> Option<(String, Request<Body>)> {
+    req.headers()
+        .typed_get::<Authorization<Bearer>>()
+        .map(|bearer| {
+            let cache_key = bearer.token().trim().to_string();
+            let token_request = make_token_request("/auth/key", bearer);
+            (cache_key, token_request)
+        })
+        .or_else(|| {
+            req.headers().typed_get::<Cookie>().and_then(|cookie| {
+                cookie.get("shuttle.sid").map(|id| {
+                    let cache_key = id.to_string();
+                    let token_request = make_token_request("/auth/session", cookie.clone());
+                    (cache_key, token_request)
+                })
+            })
+        })
+}
+
 fn make_token_request(uri: &str, header: impl Header) -> Request<Body> {
     let mut token_request = Request::builder().uri(uri);
     token_request