summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2024-09-29 02:02:41 -0400
committerOwen Jacobson <owen@grimoire.ca>2024-09-29 02:04:01 -0400
commit6c054c5b8d43a818ccfa9087960dc19b286e6bb7 (patch)
treec1eff6f13a1ac3ba102fcfa22de9f25ed30546d0
parent0b1cb80dd0b0f90c4892de7e7a2d18a076ecbdf2 (diff)
Reimplement the logout machinery in terms of token IDs, not token secrets.
This (a) reduces the amount of passing secrets around that's needed, and (b) allows tests to log out in a more straightforwards manner. Ish. The fixtures are a mess, but so is the nomenclature. Fix the latter and the former will probably follow.
-rw-r--r--.sqlx/query-b5305455d7a3bcd21d39d6e6eeff67a7bfb8402c09651f4034beb487c3d7d58f.json (renamed from .sqlx/query-8b057bc13014ee61cc2abc15b487ef0e138989e0b82cd4ff4f7187af6fb529d2.json)4
-rw-r--r--src/events/routes/test.rs43
-rw-r--r--src/login/app.rs11
-rw-r--r--src/login/routes.rs17
-rw-r--r--src/login/routes/test/logout.rs30
-rw-r--r--src/repo/token.rs10
-rw-r--r--src/test/fixtures/identity.rs12
7 files changed, 93 insertions, 34 deletions
diff --git a/.sqlx/query-8b057bc13014ee61cc2abc15b487ef0e138989e0b82cd4ff4f7187af6fb529d2.json b/.sqlx/query-b5305455d7a3bcd21d39d6e6eeff67a7bfb8402c09651f4034beb487c3d7d58f.json
index 0d3dcdf..1be8e07 100644
--- a/.sqlx/query-8b057bc13014ee61cc2abc15b487ef0e138989e0b82cd4ff4f7187af6fb529d2.json
+++ b/.sqlx/query-b5305455d7a3bcd21d39d6e6eeff67a7bfb8402c09651f4034beb487c3d7d58f.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n delete\n from token\n where secret = $1\n returning id as \"id: Id\"\n ",
+ "query": "\n delete\n from token\n where id = $1\n returning id as \"id: Id\"\n ",
"describe": {
"columns": [
{
@@ -16,5 +16,5 @@
false
]
},
- "hash": "8b057bc13014ee61cc2abc15b487ef0e138989e0b82cd4ff4f7187af6fb529d2"
+ "hash": "b5305455d7a3bcd21d39d6e6eeff67a7bfb8402c09651f4034beb487c3d7d58f"
}
diff --git a/src/events/routes/test.rs b/src/events/routes/test.rs
index 0b62b5b..820192d 100644
--- a/src/events/routes/test.rs
+++ b/src/events/routes/test.rs
@@ -355,6 +355,7 @@ async fn terminates_on_token_expiry() {
let subscriber_creds = fixtures::login::create_with_password(&app).await;
let subscriber =
fixtures::identity::identity(&app, &subscriber_creds, &fixtures::ancient()).await;
+
let routes::Events(events) = routes::events(State(app.clone()), subscriber, None)
.await
.expect("subscribe never fails");
@@ -380,3 +381,45 @@ async fn terminates_on_token_expiry() {
.await
.is_none());
}
+
+#[tokio::test]
+async fn terminates_on_logout() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let channel = fixtures::channel::create(&app, &fixtures::now()).await;
+ let sender = fixtures::login::create(&app).await;
+
+ // Subscribe via the endpoint
+
+ let subscriber_creds = fixtures::login::create_with_password(&app).await;
+ let subscriber_token =
+ fixtures::identity::logged_in(&app, &subscriber_creds, &fixtures::now()).await;
+ let subscriber =
+ fixtures::identity::from_token(&app, &subscriber_token, &fixtures::now()).await;
+
+ let routes::Events(events) = routes::events(State(app.clone()), subscriber.clone(), None)
+ .await
+ .expect("subscribe never fails");
+
+ // Verify the resulting stream's behaviour
+
+ app.logins()
+ .logout(&subscriber.token)
+ .await
+ .expect("expiring tokens succeeds");
+
+ // These should not be delivered.
+ let messages = [
+ fixtures::message::send(&app, &sender, &channel, &fixtures::now()).await,
+ fixtures::message::send(&app, &sender, &channel, &fixtures::now()).await,
+ fixtures::message::send(&app, &sender, &channel, &fixtures::now()).await,
+ ];
+
+ assert!(events
+ .filter(|types::ResumableEvent(_, event)| future::ready(messages.contains(event)))
+ .next()
+ .immediately()
+ .await
+ .is_none());
+}
diff --git a/src/login/app.rs b/src/login/app.rs
index b8916a8..182c62c 100644
--- a/src/login/app.rs
+++ b/src/login/app.rs
@@ -120,16 +120,13 @@ impl<'a> Logins<'a> {
Ok(())
}
- pub async fn logout(&self, secret: &IdentitySecret) -> Result<(), ValidateError> {
+ pub async fn logout(&self, token: &token::Id) -> Result<(), ValidateError> {
let mut tx = self.db.begin().await?;
- let token = tx
- .tokens()
- .revoke(secret)
- .await
- .not_found(|| ValidateError::InvalidToken)?;
+ tx.tokens().revoke(token).await?;
tx.commit().await?;
- self.logins.broadcast(&types::TokenRevoked::from(token));
+ self.logins
+ .broadcast(&types::TokenRevoked::from(token.clone()));
Ok(())
}
diff --git a/src/login/routes.rs b/src/login/routes.rs
index 4664063..8d9e938 100644
--- a/src/login/routes.rs
+++ b/src/login/routes.rs
@@ -78,27 +78,32 @@ struct LogoutRequest {}
async fn on_logout(
State(app): State<App>,
+ RequestedAt(now): RequestedAt,
identity: IdentityToken,
// This forces the only valid request to be `{}`, and not the infinite
// variation allowed when there's no body extractor.
Json(LogoutRequest {}): Json<LogoutRequest>,
) -> Result<(IdentityToken, StatusCode), LogoutError> {
if let Some(secret) = identity.secret() {
- app.logins().logout(&secret).await.map_err(LogoutError)?;
+ let (token, _) = app.logins().validate(&secret, &now).await?;
+ app.logins().logout(&token).await?;
}
let identity = identity.clear();
Ok((identity, StatusCode::NO_CONTENT))
}
-#[derive(Debug)]
-struct LogoutError(app::ValidateError);
+#[derive(Debug, thiserror::Error)]
+#[error(transparent)]
+enum LogoutError {
+ ValidateError(#[from] app::ValidateError),
+ DatabaseError(#[from] sqlx::Error),
+}
impl IntoResponse for LogoutError {
fn into_response(self) -> Response {
- let Self(error) = self;
- match error {
- error @ app::ValidateError::InvalidToken => {
+ match self {
+ error @ Self::ValidateError(app::ValidateError::InvalidToken) => {
(StatusCode::UNAUTHORIZED, error.to_string()).into_response()
}
other => Internal::from(other).into_response(),
diff --git a/src/login/routes/test/logout.rs b/src/login/routes/test/logout.rs
index 05594be..20b0d55 100644
--- a/src/login/routes/test/logout.rs
+++ b/src/login/routes/test/logout.rs
@@ -22,6 +22,7 @@ async fn successful() {
let (response_identity, response_status) = routes::on_logout(
State(app.clone()),
+ fixtures::now(),
identity.clone(),
Json(routes::LogoutRequest {}),
)
@@ -57,10 +58,14 @@ async fn no_identity() {
// Call the endpoint
let identity = fixtures::identity::not_logged_in();
- let (identity, status) =
- routes::on_logout(State(app), identity, Json(routes::LogoutRequest {}))
- .await
- .expect("logged out with no token");
+ let (identity, status) = routes::on_logout(
+ State(app),
+ fixtures::now(),
+ identity,
+ Json(routes::LogoutRequest {}),
+ )
+ .await
+ .expect("logged out with no token");
// Verify the return value's basic structure
@@ -77,12 +82,19 @@ async fn invalid_token() {
// Call the endpoint
let identity = fixtures::identity::fictitious();
- let routes::LogoutError(error) =
- routes::on_logout(State(app), identity, Json(routes::LogoutRequest {}))
- .await
- .expect_err("logged out with an invalid token");
+ let error = routes::on_logout(
+ State(app),
+ fixtures::now(),
+ identity,
+ Json(routes::LogoutRequest {}),
+ )
+ .await
+ .expect_err("logged out with an invalid token");
// Verify the return value's basic structure
- assert!(matches!(error, app::ValidateError::InvalidToken));
+ assert!(matches!(
+ error,
+ routes::LogoutError::ValidateError(app::ValidateError::InvalidToken)
+ ));
}
diff --git a/src/repo/token.rs b/src/repo/token.rs
index 5f39e1d..d96c094 100644
--- a/src/repo/token.rs
+++ b/src/repo/token.rs
@@ -48,20 +48,20 @@ impl<'c> Tokens<'c> {
}
// Revoke a token by its secret.
- pub async fn revoke(&mut self, secret: &IdentitySecret) -> Result<Id, sqlx::Error> {
- let token = sqlx::query_scalar!(
+ pub async fn revoke(&mut self, token: &Id) -> Result<(), sqlx::Error> {
+ sqlx::query_scalar!(
r#"
delete
from token
- where secret = $1
+ where id = $1
returning id as "id: Id"
"#,
- secret,
+ token,
)
.fetch_one(&mut *self.0)
.await?;
- Ok(token)
+ Ok(())
}
// Expire and delete all tokens that haven't been used more recently than
diff --git a/src/test/fixtures/identity.rs b/src/test/fixtures/identity.rs
index bdd7881..633fb8a 100644
--- a/src/test/fixtures/identity.rs
+++ b/src/test/fixtures/identity.rs
@@ -22,11 +22,8 @@ pub async fn logged_in(app: &App, login: &(String, Password), now: &RequestedAt)
IdentityToken::new().set(token)
}
-pub async fn identity(app: &App, login: &(String, Password), issued_at: &RequestedAt) -> Identity {
- let secret = logged_in(app, login, issued_at)
- .await
- .secret()
- .expect("successful login generates a secret");
+pub async fn from_token(app: &App, token: &IdentityToken, issued_at: &RequestedAt) -> Identity {
+ let secret = token.secret().expect("identity token has a secret");
let (token, login) = app
.logins()
.validate(&secret, issued_at)
@@ -36,6 +33,11 @@ pub async fn identity(app: &App, login: &(String, Password), issued_at: &Request
Identity { token, login }
}
+pub async fn identity(app: &App, login: &(String, Password), issued_at: &RequestedAt) -> Identity {
+ let secret = logged_in(app, login, issued_at).await;
+ from_token(app, &secret, issued_at).await
+}
+
pub fn secret(identity: &IdentityToken) -> IdentitySecret {
identity.secret().expect("identity contained a secret")
}