mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-11 05:26:14 -08:00
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:
1 parent
16d688aaf5
commit
59b185b919
3 files changed
+143
-43
No files matched your search
Generated
+14
-36
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user