use super::{OAuthProviderPlugin, OAuthTokenIssueParams};
use alibi_core::endpoint::EndpointError;
use alibi_core::session::SessionRequest;
use alibi_core::{AuthContext, AuthError, AuthSchema};
use serde_json::{Value, json};
pub(super) fn proof_header(req: &impl SessionRequest) -> Option<&str> {
req.session_headers()
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case("dpop"))
.map(|(_, value)| value.trim_matches([' ', '\t', '\r', '\n']))
.filter(|value| !value.is_empty())
}
pub(super) fn endpoint_url<S: AuthSchema>(
ctx: &AuthContext<S>,
req: &impl SessionRequest,
path: &str,
) -> String {
req.physical_request()
.and_then(|value| value.url())
.map_or_else(
|| {
format!(
"{}{}{path}",
ctx.config.base_url.trim_end_matches('/'),
ctx.config.base_path
)
},
ToString::to_string,
)
}
impl OAuthProviderPlugin {
pub(super) async fn resolve_dpop_binding<S: AuthSchema>(
&self,
ctx: &AuthContext<S>,
req: &impl SessionRequest,
params: &OAuthTokenIssueParams,
policy: &super::resource_policy::ResourceAdmission,
) -> Result<Option<Value>, EndpointError> {
let metadata = params
.client
.metadata
.get("metadata")
.and_then(|value| match value {
Value::String(text) => serde_json::from_str::<Value>(text).ok(),
value => Some(value.clone()),
});
let client_required = params.client.metadata.get("dpop_bound_access_tokens")
== Some(&Value::Bool(true))
|| params.client.metadata.get("dpopBoundAccessTokens") == Some(&Value::Bool(true))
|| metadata
.as_ref()
.and_then(|v| v.get("dpop_bound_access_tokens"))
== Some(&Value::Bool(true));
let code_jkt = params
.verification_value
.as_ref()
.and_then(|v| v.get("query"))
.and_then(|v| v.get("dpop_jkt"))
.and_then(Value::as_str);
let refresh_jkt = params
.refresh_token
.as_ref()
.and_then(|v| v.metadata.get("confirmation"))
.and_then(crate::oauth2::get_confirmation_jkt);
let proof = proof_header(req);
let Some(proof) = proof else {
if client_required
|| policy.dpop_required
|| refresh_jkt.is_some()
|| code_jkt.is_some_and(|v| !v.is_empty())
{
return Err(super::resource_policy::failure(
"invalid_dpop_proof",
"DPoP proof header is required",
));
}
return Ok(None);
};
let url = endpoint_url(ctx, req, "/oauth2/token");
let replay = crate::oauth2::create_dpop_replay_store(ctx);
let mut options = crate::oauth2::VerifyDpopProofOptions::new(proof, "POST", &url);
options.expected_jkt = refresh_jkt.or(code_jkt);
options.replay_store = Some(&replay);
options.proof_max_age_seconds = self.config.dpop_proof_max_age_seconds;
options.signing_algorithms = self.config.dpop_signing_algorithms.clone();
match crate::oauth2::verify_dpop_proof(&options).await {
Ok(proof) => Ok(Some(json!({"jkt":proof.jkt}))),
Err(error) if crate::oauth2::is_dpop_proof_error(&error) => Err(
super::resource_policy::failure("invalid_dpop_proof", &error.message),
),
Err(error) => Err(AuthError::internal(error.message).into()),
}
}
}