mod support;
use oauth_as::{ClientId, ErrorCode, IntrospectionResponse, TokenRequest, TokenTypeHint};
use support::{
confidential_client, mint_code_token, other_confidential_client, server_with, ManualClock,
CONFIDENTIAL_REDIRECT, CONFIDENTIAL_SECRET, OTHER_CONFIDENTIAL_SECRET,
};
#[tokio::test]
async fn revoking_an_access_token_makes_it_inactive_to_introspection() {
let clock = ManualClock::at_epoch();
let srv = server_with(clock, vec![confidential_client()]).await;
let issued = mint_code_token(
&srv,
"confidential-app",
Some(CONFIDENTIAL_SECRET),
CONFIDENTIAL_REDIRECT,
"read",
"user-1",
)
.await;
srv.revoke(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&issued.access_token,
Some(TokenTypeHint::AccessToken),
)
.await
.expect("revoking a live access token must succeed");
let resp = srv
.introspection_response(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&issued.access_token,
)
.await
.unwrap();
assert_eq!(resp, IntrospectionResponse::inactive());
}
#[tokio::test]
async fn revoking_a_refresh_token_also_kills_the_access_tokens_of_the_same_grant() {
let clock = ManualClock::at_epoch();
let srv = server_with(clock, vec![confidential_client()]).await;
let issued = mint_code_token(
&srv,
"confidential-app",
Some(CONFIDENTIAL_SECRET),
CONFIDENTIAL_REDIRECT,
"read",
"user-1",
)
.await;
let refresh_token = issued.refresh_token.clone().expect("a refresh token");
srv.revoke(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&refresh_token,
Some(TokenTypeHint::RefreshToken),
)
.await
.expect("revoking a live refresh token must succeed");
let resp = srv
.introspection_response(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&issued.access_token,
)
.await
.unwrap();
assert_eq!(
resp,
IntrospectionResponse::inactive(),
"the access token issued with the revoked refresh token is still live"
);
}
#[tokio::test]
async fn revoking_a_refresh_token_makes_the_next_refresh_invalid_grant() {
let clock = ManualClock::at_epoch();
let srv = server_with(clock, vec![confidential_client()]).await;
let issued = mint_code_token(
&srv,
"confidential-app",
Some(CONFIDENTIAL_SECRET),
CONFIDENTIAL_REDIRECT,
"read",
"user-1",
)
.await;
let refresh_token = issued.refresh_token.expect("a refresh token was issued");
srv.revoke(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&refresh_token,
Some(TokenTypeHint::RefreshToken),
)
.await
.expect("revoking a live refresh token must succeed");
let err = srv
.token(TokenRequest::RefreshToken {
client_id: ClientId::new("confidential-app"),
client_secret: Some(CONFIDENTIAL_SECRET.to_string()),
refresh_token,
scope: None,
})
.await
.expect_err("a revoked refresh token must not redeem");
assert_eq!(err.error, ErrorCode::InvalidGrant);
}
#[tokio::test]
async fn revoking_an_unknown_token_still_returns_ok() {
let clock = ManualClock::at_epoch();
let srv = server_with(clock, vec![confidential_client()]).await;
srv.revoke(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
"this-token-was-never-issued",
None,
)
.await
.expect("RFC 7009 s2.2: an unknown token must not produce an error response");
}
#[tokio::test]
async fn revocation_is_idempotent() {
let clock = ManualClock::at_epoch();
let srv = server_with(clock, vec![confidential_client()]).await;
let issued = mint_code_token(
&srv,
"confidential-app",
Some(CONFIDENTIAL_SECRET),
CONFIDENTIAL_REDIRECT,
"read",
"user-1",
)
.await;
for attempt in 0..2 {
srv.revoke(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&issued.access_token,
Some(TokenTypeHint::AccessToken),
)
.await
.unwrap_or_else(|e| panic!("revocation attempt {attempt} must succeed, got {e:?}"));
}
}
#[tokio::test]
async fn a_client_cannot_revoke_another_clients_token_and_it_still_works() {
let clock = ManualClock::at_epoch();
let srv = server_with(
clock,
vec![confidential_client(), other_confidential_client()],
)
.await;
let issued = mint_code_token(
&srv,
"confidential-app",
Some(CONFIDENTIAL_SECRET),
CONFIDENTIAL_REDIRECT,
"read write",
"user-1",
)
.await;
let refresh_token = issued
.refresh_token
.clone()
.expect("a refresh token was issued");
srv.revoke(
&ClientId::new("other-app"),
Some(OTHER_CONFIDENTIAL_SECRET),
&issued.access_token,
Some(TokenTypeHint::AccessToken),
)
.await
.expect("the wire answer for someone else's token is still success");
let after_access_attempt = srv
.introspection_response(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&issued.access_token,
)
.await
.unwrap();
assert!(
after_access_attempt.active,
"another client's revoke attempt must not touch the access token"
);
srv.revoke(
&ClientId::new("other-app"),
Some(OTHER_CONFIDENTIAL_SECRET),
&refresh_token,
Some(TokenTypeHint::RefreshToken),
)
.await
.expect("the wire answer for someone else's refresh token is still success");
srv.token(TokenRequest::RefreshToken {
client_id: ClientId::new("confidential-app"),
client_secret: Some(CONFIDENTIAL_SECRET.to_string()),
refresh_token,
scope: None,
})
.await
.expect("another client's failed revoke attempt must not have destroyed the refresh chain");
}
#[tokio::test]
async fn wrong_type_hint_still_revokes_the_right_token() {
for hint_for_access in [
None,
Some(TokenTypeHint::AccessToken),
Some(TokenTypeHint::RefreshToken),
] {
let clock = ManualClock::at_epoch();
let srv = server_with(clock, vec![confidential_client()]).await;
let issued = mint_code_token(
&srv,
"confidential-app",
Some(CONFIDENTIAL_SECRET),
CONFIDENTIAL_REDIRECT,
"read",
"user-1",
)
.await;
srv.revoke(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&issued.access_token,
hint_for_access,
)
.await
.unwrap();
let resp = srv
.introspection_response(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&issued.access_token,
)
.await
.unwrap();
assert_eq!(
resp,
IntrospectionResponse::inactive(),
"hint {hint_for_access:?} must not stop the access token from being revoked"
);
}
for hint_for_refresh in [
None,
Some(TokenTypeHint::AccessToken),
Some(TokenTypeHint::RefreshToken),
] {
let clock = ManualClock::at_epoch();
let srv = server_with(clock, vec![confidential_client()]).await;
let issued = mint_code_token(
&srv,
"confidential-app",
Some(CONFIDENTIAL_SECRET),
CONFIDENTIAL_REDIRECT,
"read",
"user-1",
)
.await;
let refresh_token = issued.refresh_token.expect("a refresh token was issued");
srv.revoke(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&refresh_token,
hint_for_refresh,
)
.await
.unwrap();
let err = srv
.token(TokenRequest::RefreshToken {
client_id: ClientId::new("confidential-app"),
client_secret: Some(CONFIDENTIAL_SECRET.to_string()),
refresh_token,
scope: None,
})
.await
.expect_err(&format!(
"hint {hint_for_refresh:?} must not stop the refresh token from being revoked"
));
assert_eq!(err.error, ErrorCode::InvalidGrant);
}
}
#[tokio::test]
async fn revocation_requires_client_authentication() {
let clock = ManualClock::at_epoch();
let srv = server_with(clock, vec![confidential_client()]).await;
let issued = mint_code_token(
&srv,
"confidential-app",
Some(CONFIDENTIAL_SECRET),
CONFIDENTIAL_REDIRECT,
"read",
"user-1",
)
.await;
let err = srv
.revoke(
&ClientId::new("confidential-app"),
None,
&issued.access_token,
None,
)
.await
.expect_err("a confidential client presenting no secret must not be authenticated");
assert_eq!(err.error, ErrorCode::InvalidClient);
let err = srv
.revoke(
&ClientId::new("confidential-app"),
Some("not-the-real-secret"),
&issued.access_token,
None,
)
.await
.expect_err("the wrong secret must not authenticate");
assert_eq!(err.error, ErrorCode::InvalidClient);
let err = srv
.revoke(
&ClientId::new("no-such-client"),
Some("anything"),
&issued.access_token,
None,
)
.await
.expect_err("an unregistered client_id must not be authenticated");
assert_eq!(err.error, ErrorCode::InvalidClient);
let resp = srv
.introspection_response(
&ClientId::new("confidential-app"),
Some(CONFIDENTIAL_SECRET),
&issued.access_token,
)
.await
.unwrap();
assert!(
resp.active,
"an unauthenticated revoke attempt must not have revoked anything"
);
}