build(web): share reqwest 0.13 with the OIDC client

Disable the reqwest 0.12 client bundled with openidconnect/oauth2 and
use the official oauth2-reqwest adapter around the workspace reqwest
0.13 client. Pin the pre-release adapter version until its API
stabilizes.

Keep the existing redirect policy and 30-second timeout. Initialize the
ring provider when constructing OIDC configuration, including outside
the web application entry point.

Exercise discovery, JWKS fetching, token success and OAuth error
responses against a local mock provider. Verify redirects are not
followed. Refresh Cargo.lock to remove reqwest 0.12 without unrelated
upgrades.

Validation: Docker OIDC tests (2) and webhook tests (13) pass. Clippy
with warnings denied for native/core/proto/web all targets and full
features passes, as does workspace formatting. The default core/web
dependency graph has one reqwest version and 45 multi-version names
instead of 46; the TLS runtime still selects ring only.
This commit is contained in:
KKRainbow committed 2026-10-08 23:41:51 +08:00
1 parent 16d688aaf5
commit 59b185b919
3 files changed
+143 -43

No files matched your search

Generated
+14 -36
View File
@@ -2850,7 +2850,7 @@ dependencies = [
"prost-types 0.14.4",
"prost-wkt-types",
"quote",
"reqwest 0.13.5",
"reqwest",
"serde",
"serde_json",
"sha2 0.10.9",
@@ -2915,11 +2915,12 @@ dependencies = [
"imageproc",
"maxminddb",
"mimalloc",
"oauth2-reqwest",
"openidconnect",
"password-auth",
"prost 0.14.4",
"rand 0.8.8",
"reqwest 0.13.5",
"reqwest",
"rust-embed",
"rust-i18n",
"sea-orm",
@@ -6030,7 +6031,6 @@ dependencies = [
"getrandom 0.2.17",
"http",
"rand 0.8.8",
"reqwest 0.12.28",
"serde",
"serde_json",
"serde_path_to_error",
@@ -6039,6 +6039,16 @@ dependencies = [
"url",
]
[[package]]
name = "oauth2-reqwest"
version = "0.1.0-alpha.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234fb5c965bbce983ee5de636a7a51d6a3223da8067ea02f9ab2d2d78ac08be2"
dependencies = [
"oauth2",
"reqwest",
]
[[package]]
name = "objc2"
version = "0.6.4"
@@ -7692,38 +7702,6 @@ version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "reqwest"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-http 0.6.11",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "reqwest"
version = "0.13.5"
@@ -9579,7 +9557,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest 0.13.5",
"reqwest",
"serde",
"serde_json",
"serde_repr",
+4 -1
View File
@@ -74,7 +74,10 @@ uuid = { workspace = true, features = [
] }
chrono = { workspace = true, features = ["serde"] }
openidconnect = { version = "4.0", default-features = false, features = ["accept-rfc3339-timestamps", "reqwest"] }
# Keep the bundled reqwest 0.12 client disabled; use the official 0.13 adapter.
openidconnect = { version = "4.0", default-features = false, features = ["accept-rfc3339-timestamps"] }
# Pin the adapter until its API stabilizes.
oauth2-reqwest = "=0.1.0-alpha.3"
# Reuse EasyTier's ring provider. reqwest 0.13's rustls feature adds aws-lc,
# making rustls backend selection ambiguous when core/web are built together.
# Initialize the process provider before constructing a reqwest client.
+125 -6
View File
@@ -1,4 +1,4 @@
use openidconnect::reqwest;
use oauth2_reqwest::ReqwestClient;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
@@ -180,7 +180,7 @@ pub struct OidcConfig {
pub scopes: Vec<String>,
pub pkce_enabled: bool,
pub frontend_base_url: Option<String>,
pub http_client: Option<reqwest::Client>,
pub http_client: Option<ReqwestClient>,
cached_client: Option<Arc<ConfiguredAppClient>>,
}
@@ -224,10 +224,14 @@ impl OidcConfig {
if oidc_username_claim.trim().is_empty() {
return Err(anyhow::anyhow!("--oidc-username-claim cannot be empty"));
}
let http_client = reqwest::ClientBuilder::new()
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(30))
.build()?;
// OIDC configuration can also be created outside the web entry point.
easytier::utils::init_crypto_provider();
let http_client = ReqwestClient::from(
reqwest::ClientBuilder::new()
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(30))
.build()?,
);
let issuer_url = oidc_issuer_url.ok_or_else(|| {
anyhow::anyhow!("--oidc-issuer-url is required when using OIDC authentication")
@@ -697,6 +701,121 @@ mod route {
mod tests {
use super::*;
#[tokio::test]
async fn reqwest_adapter_supports_discovery_and_token_exchange() {
use axum::{
Form, Json,
http::{HeaderMap, StatusCode},
response::{IntoResponse, Redirect},
routing::post,
};
use openidconnect::{AuthorizationCode, OAuth2TokenResponse, RequestTokenError};
use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio_util::task::AbortOnDropHandle;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let issuer = format!("http://{}", listener.local_addr().unwrap());
let metadata = json!({
"issuer": issuer,
"authorization_endpoint": format!("{issuer}/authorize"),
"token_endpoint": format!("{issuer}/token"),
"jwks_uri": format!("{issuer}/jwks"),
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
});
let redirect_hits = Arc::new(AtomicUsize::new(0));
let hits = redirect_hits.clone();
let app = Router::new()
.route(
"/.well-known/openid-configuration",
get(move || async move { Json(metadata) }),
)
.route("/jwks", get(|| async { Json(json!({ "keys": [] })) }))
.route(
"/token",
post(
|headers: HeaderMap, Form(form): Form<HashMap<String, String>>| async move {
assert_eq!(
headers["authorization"],
"Basic dGVzdC1jbGllbnQ6dGVzdC1zZWNyZXQ="
);
assert_eq!(form["grant_type"], "authorization_code");
assert_eq!(form["redirect_uri"], "http://localhost/callback");
match form["code"].as_str() {
"valid" => Json(json!({
"access_token": "test-access-token",
"token_type": "Bearer",
"expires_in": 3600,
}))
.into_response(),
"redirect" => Redirect::temporary("/unexpected").into_response(),
_ => (
StatusCode::BAD_REQUEST,
Json(json!({ "error": "invalid_grant" })),
)
.into_response(),
}
},
),
)
.route(
"/unexpected",
post(move || async move {
hits.fetch_add(1, Ordering::SeqCst);
Json(json!({ "access_token": "unexpected", "token_type": "Bearer" }))
}),
);
let _server = AbortOnDropHandle::new(tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
}));
let config = OidcConfig::from_params(OidcOptions {
oidc_issuer_url: Some(issuer),
oidc_client_id: Some("test-client".to_owned()),
oidc_client_secret: Some("test-secret".to_owned()),
oidc_username_claim: "preferred_username".to_owned(),
oidc_scopes: vec!["openid".to_owned()],
oidc_redirect_url: Some("http://localhost/callback".to_owned()),
oidc_disable_pkce: false,
oidc_frontend_base_url: None,
})
.await
.unwrap();
let client = config.client().unwrap();
let http_client = config.http_client.as_ref().unwrap();
let token = client
.exchange_code(AuthorizationCode::new("valid".to_owned()))
.unwrap()
.request_async(http_client)
.await
.unwrap();
assert_eq!(token.access_token().secret(), "test-access-token");
assert_eq!(token.expires_in(), Some(Duration::from_secs(3600)));
let error = client
.exchange_code(AuthorizationCode::new("invalid".to_owned()))
.unwrap()
.request_async(http_client)
.await
.unwrap_err();
assert!(
matches!(error, RequestTokenError::ServerResponse(ref response)
if response.error() == &CoreErrorResponseType::InvalidGrant)
);
assert!(
client
.exchange_code(AuthorizationCode::new("redirect".to_owned()))
.unwrap()
.request_async(http_client)
.await
.is_err()
);
assert_eq!(redirect_hits.load(Ordering::SeqCst), 0);
}
#[test]
fn test_dot_path_to_json_pointer() {
use serde_json::json;