use crate::endpoints::client_auth;
use crate::endpoints::params::Params;
use crate::server::AuthorizationServer;
use crate::store::digest;
use rustlavel_http::{IntoResponse, Request, Response, Status};
use rustlavel_oauth::OAuthError;
pub async fn revoke(server: AuthorizationServer, mut request: Request) -> Response {
let params = Params::from_body(&mut request);
match run(&server, &request, ¶ms).await {
Ok(()) => Response::new(Status::OK).with_header("cache-control", "no-store"),
Err(error) => error.into_response().with_header("cache-control", "no-store"),
}
}
async fn run(
server: &AuthorizationServer,
request: &Request,
params: &Params,
) -> Result<(), OAuthError> {
let credentials = client_auth::presented(request, params)?;
let client = client_auth::authenticate(server, &credentials).await?;
let presented = params.required("token")?;
let hint = params.get("token_type_hint")?;
let hash = digest(presented);
let refresh_first = hint != Some("access_token");
if refresh_first && revoke_refresh(server, &hash, &client.id).await {
return Ok(());
}
if revoke_access(server, &hash, &client.id).await {
return Ok(());
}
if !refresh_first {
revoke_refresh(server, &hash, &client.id).await;
}
Ok(())
}
async fn revoke_refresh(server: &AuthorizationServer, hash: &str, client_id: &str) -> bool {
let Ok(Some(record)) = server.tokens().find_refresh(hash).await else { return false };
if record.client_id != client_id {
return false;
}
if let Err(error) = server.tokens().revoke_family(&record.family).await {
rustlavel_core::error!("oauth: could not revoke family {}: {error}", record.family);
}
true
}
async fn revoke_access(server: &AuthorizationServer, hash: &str, client_id: &str) -> bool {
let Ok(Some(record)) = server.tokens().find_access(hash).await else { return false };
if record.client_id != client_id {
return false;
}
if let Err(error) = server.tokens().revoke_access(&record.id).await {
rustlavel_core::error!("oauth: could not revoke access token: {error}");
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::{Client, MemoryClientStore};
use rustlavel_http::Method;
use rustlavel_oauth::{Scopes, TokenResponse};
fn server() -> AuthorizationServer {
AuthorizationServer::new(
MemoryClientStore::new()
.with(Client::confidential("web", "s3cret").scopes(Scopes::of(["read"])))
.with(Client::confidential("other", "s3cret")),
)
}
async fn issued(server: &AuthorizationServer) -> TokenResponse {
server
.issue("web", Some("7"), &Scopes::of(["read"]), "family-1", true)
.await
.expect("issued")
}
async fn post(server: &AuthorizationServer, fields: &[(&str, &str)]) -> Result<(), OAuthError> {
let mut request = Request::new(Method::Post, "/oauth/revoke").with_form(fields);
let params = Params::from_body(&mut request);
run(server, &request, ¶ms).await
}
fn credentials(id: &str) -> [(&str, &str); 2] {
[("client_id", id), ("client_secret", "s3cret")]
}
#[tokio::test]
async fn revoking_an_access_token_stops_it_working() {
let server = server();
let token = issued(&server).await;
let [id, secret] = credentials("web");
post(&server, &[("token", &token.access_token), id, secret]).await.expect("revoked");
assert!(server.validate_bearer(&token.access_token).await.is_err());
}
#[tokio::test]
async fn revoking_a_refresh_token_takes_its_access_token_with_it() {
let server = server();
let token = issued(&server).await;
let refresh = token.refresh_token.clone().expect("refresh");
let [id, secret] = credentials("web");
post(&server, &[("token", &refresh), id, secret]).await.expect("revoked");
assert!(server.validate_bearer(&token.access_token).await.is_err());
}
#[tokio::test]
async fn an_unknown_token_is_a_success_rather_than_an_oracle() {
let [id, secret] = credentials("web");
assert!(post(&server(), &[("token", "no-such-token"), id, secret]).await.is_ok());
}
#[tokio::test]
async fn another_clients_token_is_a_success_and_stays_live() {
let server = server();
let token = issued(&server).await;
let [id, secret] = credentials("other");
post(&server, &[("token", &token.access_token), id, secret]).await.expect("no error");
assert!(server.validate_bearer(&token.access_token).await.is_ok());
}
#[tokio::test]
async fn a_wrong_hint_still_finds_the_token() {
let server = server();
let token = issued(&server).await;
let [id, secret] = credentials("web");
post(
&server,
&[("token", &token.access_token), ("token_type_hint", "refresh_token"), id, secret],
)
.await
.expect("revoked");
assert!(server.validate_bearer(&token.access_token).await.is_err());
}
#[tokio::test]
async fn revocation_requires_a_client_and_a_token() {
let server = server();
let token = issued(&server).await;
let unauthenticated = post(&server, &[("token", &token.access_token)]).await.unwrap_err();
assert_eq!(unauthenticated.code, rustlavel_oauth::OAuthErrorCode::InvalidClient);
assert!(server.validate_bearer(&token.access_token).await.is_ok());
let [id, secret] = credentials("web");
let no_token = post(&server, &[id, secret]).await.unwrap_err();
assert!(no_token.to_string().contains("`token` parameter is required"));
}
#[tokio::test]
async fn a_wrong_secret_revokes_nothing() {
let server = server();
let token = issued(&server).await;
let error = post(
&server,
&[("token", &token.access_token), ("client_id", "web"), ("client_secret", "wrong")],
)
.await
.unwrap_err();
assert_eq!(error.code, rustlavel_oauth::OAuthErrorCode::InvalidClient);
assert!(server.validate_bearer(&token.access_token).await.is_ok());
}
#[tokio::test]
async fn revoking_twice_is_still_a_success() {
let server = server();
let token = issued(&server).await;
let [id, secret] = credentials("web");
post(&server, &[("token", &token.access_token), id, secret]).await.expect("revoked");
post(&server, &[("token", &token.access_token), id, secret]).await.expect("still fine");
}
}