Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions crates/rattler_networking/src/authentication_middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,22 @@ use reqwest_middleware::{Middleware, Next};
use url::Url;

use crate::{
Authentication, AuthenticationStorage, authentication_storage::AuthenticationStorageError,
oauth_refresh,
Authentication, AuthenticationStorage,
authentication_storage::AuthenticationStorageError,
oauth_refresh::{self, OAuthRefreshFailure},
};

/// Error returned when a request to `host` failed authentication while the
/// stored OAuth credential for that host was expired and could not be
/// refreshed. Surfaced so callers can tell the user to authenticate again
/// (e.g. by re-running their login command).
#[derive(Debug, Clone, thiserror::Error)]
#[error("authentication for '{host}' has expired; please re-authenticate")]
pub struct AuthenticationExpired {
/// The host that requires re-authentication.
pub host: String,
}

/// `reqwest` middleware to authenticate requests
#[derive(Clone)]
pub struct AuthenticationMiddleware {
Expand Down Expand Up @@ -43,22 +55,41 @@ impl Middleware for AuthenticationMiddleware {
}
Ok((url, auth_with_key)) => {
// If this is an OAuth token, attempt refresh if expired
let auth = match auth_with_key {
let (auth, reauth_host) = match auth_with_key {
Some((matched_key, auth)) => {
let refresh_result = oauth_refresh::maybe_refresh_oauth(
&self.auth_storage,
auth,
&matched_key,
)
.await;
// A failure that means the stored login itself is no
// longer usable (vs. a transient refresh problem).
let needs_reauth = matches!(
refresh_result.failure(),
Some(
OAuthRefreshFailure::MissingRefreshToken
| OAuthRefreshFailure::ReauthenticationRequired { .. }
)
);
if let Some(failure) = refresh_result.failure() {
tracing::warn!(
"OAuth refresh for '{matched_key}' did not produce fresh credentials: {failure}"
);
}
refresh_result.into_authentication()
let auth = refresh_result.into_authentication();
// Only when the dead credential was actually dropped does
// the request go out unauthenticated; remember the host
// so we can prompt for re-authentication if it still
// fails.
let reauth_host = if needs_reauth && auth.is_none() {
url.host_str().map(str::to_string)
} else {
None
};
(auth, reauth_host)
}
None => None,
None => (None, None),
};

let url = Self::authenticate_url(url, &auth);
Expand All @@ -67,7 +98,20 @@ impl Middleware for AuthenticationMiddleware {
*req.url_mut() = url;

let req = Self::authenticate_request(req, &auth).await?;
next.run(req, extensions).await
let response = next.run(req, extensions).await?;

// The stored login expired, could not be refreshed, and the
// request still came back unauthorized: surface a typed error so
// callers can tell the user to re-authenticate.
if let Some(host) = reauth_host
&& matches!(response.status().as_u16(), 401 | 403)
{
return Err(reqwest_middleware::Error::middleware(
AuthenticationExpired { host },
));
}

Ok(response)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rattler_networking/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![deny(missing_docs)]

//! Networking utilities for Rattler, specifically authenticating requests
pub use authentication_middleware::AuthenticationMiddleware;
pub use authentication_middleware::{AuthenticationExpired, AuthenticationMiddleware};
pub use authentication_storage::{authentication::Authentication, storage::AuthenticationStorage};
pub use challenge_middleware::{
AuthChallengeMiddleware, AuthFlow, AuthFlowError, BearerToken, Challenge,
Expand Down
Loading