fix: add missing DCR fields, PKCE verifier validation, and Cargo.lock sync

- Add policy_uri, tos_uri, software_id, software_version to DCR per RFC 7591
- Add code_verifier length (43-128) and charset validation per RFC 7636 §4.1
- Warn at startup if OIDC server enabled without identity providers
- Include Cargo.lock update for ring dependency
This commit is contained in:
Daniel Flanagan
2026-03-12 14:44:42 -05:00
parent ce8abf6bf1
commit 489ff6f2a2
4 changed files with 26 additions and 4 deletions

1
Cargo.lock generated
View File

@@ -5519,6 +5519,7 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
"regex", "regex",
"reqwest 0.13.1", "reqwest 0.13.1",
"ring",
"ruma", "ruma",
"rustls", "rustls",
"rustyline-async", "rustyline-async",

View File

@@ -58,7 +58,7 @@ pub(crate) async fn registration_route(State(services): State<crate::State>, Jso
let reg = oidc.register_client(body)?; let reg = oidc.register_client(body)?;
info!("OIDC client registered: {} ({})", reg.client_id, reg.client_name.as_deref().unwrap_or("unnamed")); info!("OIDC client registered: {} ({})", reg.client_id, reg.client_name.as_deref().unwrap_or("unnamed"));
Ok((StatusCode::CREATED, Json(serde_json::json!({"client_id": reg.client_id, "client_id_issued_at": reg.registered_at, "redirect_uris": reg.redirect_uris, "client_name": reg.client_name, "client_uri": reg.client_uri, "logo_uri": reg.logo_uri, "contacts": reg.contacts, "token_endpoint_auth_method": reg.token_endpoint_auth_method, "grant_types": reg.grant_types, "response_types": reg.response_types, "application_type": reg.application_type})))) Ok((StatusCode::CREATED, Json(serde_json::json!({"client_id": reg.client_id, "client_id_issued_at": reg.registered_at, "redirect_uris": reg.redirect_uris, "client_name": reg.client_name, "client_uri": reg.client_uri, "logo_uri": reg.logo_uri, "contacts": reg.contacts, "token_endpoint_auth_method": reg.token_endpoint_auth_method, "grant_types": reg.grant_types, "response_types": reg.response_types, "application_type": reg.application_type, "policy_uri": reg.policy_uri, "tos_uri": reg.tos_uri, "software_id": reg.software_id, "software_version": reg.software_version}))))
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]

View File

@@ -15,7 +15,7 @@ use ruma::UserId;
use serde::Serialize; use serde::Serialize;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use tuwunel_core::{ use tuwunel_core::{
Err, Result, err, implement, info, Err, Result, err, implement, info, warn,
utils::{hash::sha256, result::LogErr, stream::ReadyExt}, utils::{hash::sha256, result::LogErr, stream::ReadyExt},
}; };
use url::Url; use url::Url;
@@ -46,6 +46,9 @@ impl crate::Service for Service {
let oidc_server = if !args.server.config.identity_provider.is_empty() let oidc_server = if !args.server.config.identity_provider.is_empty()
|| args.server.config.well_known.client.is_some() || args.server.config.well_known.client.is_some()
{ {
if args.server.config.identity_provider.is_empty() {
warn!("OIDC server enabled (well_known.client is set) but no identity_provider configured; authorization flow will not work");
}
info!("Initializing OIDC server for next-gen auth (MSC2965)"); info!("Initializing OIDC server for next-gen auth (MSC2965)");
Some(Arc::new(OidcServer::build(args)?)) Some(Arc::new(OidcServer::build(args)?))
} else { } else {

View File

@@ -23,6 +23,8 @@ pub struct DcrRequest {
pub logo_uri: Option<String>, #[serde(default)] pub contacts: Vec<String>, pub logo_uri: Option<String>, #[serde(default)] pub contacts: Vec<String>,
pub token_endpoint_auth_method: Option<String>, pub grant_types: Option<Vec<String>>, pub token_endpoint_auth_method: Option<String>, pub grant_types: Option<Vec<String>>,
pub response_types: Option<Vec<String>>, pub application_type: Option<String>, pub response_types: Option<Vec<String>>, pub application_type: Option<String>,
pub policy_uri: Option<String>, pub tos_uri: Option<String>,
pub software_id: Option<String>, pub software_version: Option<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@@ -30,7 +32,8 @@ pub struct OidcClientRegistration {
pub client_id: String, pub redirect_uris: Vec<String>, pub client_name: Option<String>, pub client_id: String, pub redirect_uris: Vec<String>, pub client_name: Option<String>,
pub client_uri: Option<String>, pub logo_uri: Option<String>, pub contacts: Vec<String>, pub client_uri: Option<String>, pub logo_uri: Option<String>, pub contacts: Vec<String>,
pub token_endpoint_auth_method: String, pub grant_types: Vec<String>, pub response_types: Vec<String>, pub token_endpoint_auth_method: String, pub grant_types: Vec<String>, pub response_types: Vec<String>,
pub application_type: Option<String>, pub registered_at: u64, pub application_type: Option<String>, pub policy_uri: Option<String>, pub tos_uri: Option<String>,
pub software_id: Option<String>, pub software_version: Option<String>, pub registered_at: u64,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@@ -124,7 +127,8 @@ impl OidcServer {
token_endpoint_auth_method: auth_method, token_endpoint_auth_method: auth_method,
grant_types: request.grant_types.unwrap_or_else(|| vec!["authorization_code".to_owned(), "refresh_token".to_owned()]), grant_types: request.grant_types.unwrap_or_else(|| vec!["authorization_code".to_owned(), "refresh_token".to_owned()]),
response_types: request.response_types.unwrap_or_else(|| vec!["code".to_owned()]), response_types: request.response_types.unwrap_or_else(|| vec!["code".to_owned()]),
application_type: request.application_type, application_type: request.application_type, policy_uri: request.policy_uri, tos_uri: request.tos_uri,
software_id: request.software_id, software_version: request.software_version,
registered_at: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(), registered_at: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(),
}; };
self.db.oidcclientid_registration.raw_put(&*client_id, Cbor(&registration)); self.db.oidcclientid_registration.raw_put(&*client_id, Cbor(&registration));
@@ -172,6 +176,7 @@ impl OidcServer {
if let Some(challenge) = &session.code_challenge { if let Some(challenge) = &session.code_challenge {
let Some(verifier) = code_verifier else { return Err!(Request(Forbidden("code_verifier required for PKCE"))); }; let Some(verifier) = code_verifier else { return Err!(Request(Forbidden("code_verifier required for PKCE"))); };
Self::validate_code_verifier(verifier)?;
let method = session.code_challenge_method.as_deref().unwrap_or("S256"); let method = session.code_challenge_method.as_deref().unwrap_or("S256");
let computed = match method { let computed = match method {
| "S256" => { let hash = utils::hash::sha256::hash(verifier.as_bytes()); b64.encode(hash) }, | "S256" => { let hash = utils::hash::sha256::hash(verifier.as_bytes()); b64.encode(hash) },
@@ -184,6 +189,19 @@ impl OidcServer {
Ok(session) Ok(session)
} }
/// Validate code_verifier per RFC 7636 Section 4.1: must be 43-128
/// characters using only unreserved characters [A-Z] / [a-z] / [0-9] /
/// "-" / "." / "_" / "~".
fn validate_code_verifier(verifier: &str) -> Result {
if !(43..=128).contains(&verifier.len()) {
return Err!(Request(InvalidParam("code_verifier must be 43-128 characters")));
}
if !verifier.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.' || b == b'_' || b == b'~') {
return Err!(Request(InvalidParam("code_verifier contains invalid characters")));
}
Ok(())
}
pub fn sign_id_token(&self, claims: &IdTokenClaims) -> Result<String> { pub fn sign_id_token(&self, claims: &IdTokenClaims) -> Result<String> {
let mut header = jwt::Header::new(jwt::Algorithm::ES256); let mut header = jwt::Header::new(jwt::Algorithm::ES256);
header.kid = Some(self.key_id.clone()); header.kid = Some(self.key_id.clone());