| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- //! Revocation request validation: unauthenticated `401`, missing token `400`.
- use axum::{
- Router,
- body::Body,
- http::{Request, StatusCode, header},
- response::IntoResponse,
- routing::post,
- };
- use http_body_util::BodyExt as _;
- use serde_json::Value;
- use tower::ServiceExt as _;
- use super::{
- TestError, TestResult, handler_state, lazy_pool, lazy_redis, no_credentials, service, store,
- test_keys,
- };
- use crate::{
- error::OAuthErrorCode,
- protocol::revoke::{RevokeContext, RevokeRequest, RevokeStores, revoke_handler, revoke_tokens},
- };
- /// An unauthenticated request fails closed with `401 invalid_client`.
- #[tokio::test]
- async fn revoke_unauthenticated_request_is_401() -> TestResult {
- let mut conn = lazy_redis()?;
- let pool = lazy_pool()?;
- let store = store();
- let service = service()?;
- let auth = no_credentials();
- let ring = test_keys()?;
- let mut stores = RevokeStores {
- pool: &pool,
- keys: &ring,
- conn: &mut conn,
- store: &store,
- service: &service,
- };
- let ctx = RevokeContext {
- token_endpoint: "https://auth.example.test/token",
- now: time::OffsetDateTime::now_utc(),
- request_id: Some("req-revoke-test"),
- };
- let request = RevokeRequest {
- token: "some-token",
- token_type_hint: None,
- auth: &auth,
- };
- match revoke_tokens(&mut stores, &ctx, &request).await {
- Err(error) => {
- if error.code() != OAuthErrorCode::InvalidClient {
- return Err(TestError::Unexpected(
- "unauthenticated request must be invalid_client".to_owned(),
- ));
- }
- if error.status() != StatusCode::UNAUTHORIZED {
- return Err(TestError::Unexpected(
- "unauthenticated request must be 401".to_owned(),
- ));
- }
- let response = error.into_response();
- if response.status() != StatusCode::UNAUTHORIZED
- || response.headers().get(header::WWW_AUTHENTICATE).is_none()
- {
- return Err(TestError::Unexpected(
- "401 must carry a WWW-Authenticate challenge".to_owned(),
- ));
- }
- }
- Ok(_) => {
- return Err(TestError::Unexpected(
- "unauthenticated request must not succeed".to_owned(),
- ));
- }
- }
- Ok(())
- }
- /// A missing `token` is `invalid_request`, not a silent success.
- #[tokio::test]
- async fn revoke_missing_token_is_invalid_request() -> TestResult {
- let mut conn = lazy_redis()?;
- let pool = lazy_pool()?;
- let store = store();
- let service = service()?;
- let auth = no_credentials();
- let ring = test_keys()?;
- let mut stores = RevokeStores {
- pool: &pool,
- keys: &ring,
- conn: &mut conn,
- store: &store,
- service: &service,
- };
- let ctx = RevokeContext {
- token_endpoint: "https://auth.example.test/token",
- now: time::OffsetDateTime::now_utc(),
- request_id: None,
- };
- let request = RevokeRequest {
- token: "",
- token_type_hint: None,
- auth: &auth,
- };
- match revoke_tokens(&mut stores, &ctx, &request).await {
- Err(error) if error.code() == OAuthErrorCode::InvalidRequest => Ok(()),
- Err(_) | Ok(_) => Err(TestError::Unexpected(
- "missing token must be invalid_request".to_owned(),
- )),
- }
- }
- /// A request missing the `token` form field is `400 invalid_request`
- /// (RFC 7009 §2.1: `token` is REQUIRED; §2.2.1 defines the error response).
- #[tokio::test]
- async fn revoke_missing_token_form_is_400_invalid_request() -> TestResult {
- let app = Router::new()
- .route("/revoke", post(revoke_handler))
- .with_state(handler_state()?);
- let request = Request::post("/revoke")
- .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
- .header(header::AUTHORIZATION, "Basic cmV2b2tlLXRlc3Q6c2VjcmV0")
- .body(Body::from("token_type_hint=access_token"))
- .map_err(|error| TestError::Unexpected(format!("request build failed: {error}")))?;
- let response = match app.oneshot(request).await {
- Ok(response) => response,
- Err(never) => match never {},
- };
- if response.status() != StatusCode::BAD_REQUEST {
- return Err(TestError::Unexpected(format!(
- "a missing token parameter must be 400, got {}",
- response.status()
- )));
- }
- let bytes = response.into_body().collect().await?.to_bytes();
- let body: Value = serde_json::from_slice(&bytes)?;
- if body.get("error").and_then(Value::as_str) != Some("invalid_request") {
- return Err(TestError::Unexpected(
- "a missing token parameter must render invalid_request".to_owned(),
- ));
- }
- Ok(())
- }
|