1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

handlers: Add test for the compatibility login API

This commit is contained in:
Quentin Gliech
2023-02-23 14:47:13 +01:00
parent e1391fcb37
commit 97635375cc
2 changed files with 291 additions and 3 deletions

View File

@ -17,12 +17,12 @@ use rand::{
distributions::{Alphanumeric, DistString},
RngCore,
};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use thiserror::Error;
static DEVICE_ID_LENGTH: usize = 10;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Device {
id: String,

View File

@ -117,7 +117,7 @@ pub enum Identifier {
#[skip_serializing_none]
#[serde_as]
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct ResponseBody {
access_token: String,
device_id: Device,
@ -386,3 +386,291 @@ async fn user_password_login(
Ok((session, user))
}
#[cfg(test)]
mod tests {
use hyper::Request;
use rand::distributions::{Alphanumeric, DistString};
use sqlx::PgPool;
use super::*;
use crate::test_utils::{init_tracing, RequestBuilderExt, ResponseExt, TestState};
/// Test that the server advertises the right login flows.
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_get_login(pool: PgPool) {
init_tracing();
let state = TestState::from_pool(pool).await.unwrap();
// Now let's try to login with the password, without asking for a refresh token.
let request = Request::get("/_matrix/client/v3/login").empty();
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let body: serde_json::Value = response.json();
assert_eq!(
body,
serde_json::json!({
"flows": [
{
"type": "m.login.password",
},
{
"type": "m.login.sso",
"org.matrix.msc3824.delegated_oidc_compatibility": true,
},
{
"type": "m.login.token",
}
],
})
);
}
/// Test that a user can login with a password using the Matrix
/// compatibility API.
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_user_password_login(pool: PgPool) {
init_tracing();
let state = TestState::from_pool(pool).await.unwrap();
// Let's provision a user and add a password to it. This part is hard to test
// with just HTTP requests, so we'll use the repository directly.
let mut repo = state.repository().await.unwrap();
let user = repo
.user()
.add(&mut state.rng(), &state.clock, "alice".to_owned())
.await
.unwrap();
let (version, hashed_password) = state
.password_manager
.hash(
&mut state.rng(),
Zeroizing::new("password".to_owned().into_bytes()),
)
.await
.unwrap();
repo.user_password()
.add(
&mut state.rng(),
&state.clock,
&user,
version,
hashed_password,
None,
)
.await
.unwrap();
repo.save().await.unwrap();
// Now let's try to login with the password, without asking for a refresh token.
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "alice",
},
"password": "password",
}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let body: ResponseBody = response.json();
assert!(!body.access_token.is_empty());
assert_eq!(body.device_id.as_str().len(), 10);
assert_eq!(body.user_id, "@alice:example.com");
assert_eq!(body.refresh_token, None);
assert_eq!(body.expires_in_ms, None);
// Do the same, but this time ask for a refresh token.
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "alice",
},
"password": "password",
"refresh_token": true,
}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let body: ResponseBody = response.json();
assert!(!body.access_token.is_empty());
assert_eq!(body.device_id.as_str().len(), 10);
assert_eq!(body.user_id, "@alice:example.com");
assert!(body.refresh_token.is_some());
assert!(body.expires_in_ms.is_some());
// Try to login with a wrong password.
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "alice",
},
"password": "wrongpassword",
}));
let response = state.request(request).await;
response.assert_status(StatusCode::FORBIDDEN);
let body: serde_json::Value = response.json();
assert_eq!(body["errcode"], "M_UNAUTHORIZED");
// Try to login with a wrong username.
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "bob",
},
"password": "wrongpassword",
}));
let old_body = body;
let response = state.request(request).await;
response.assert_status(StatusCode::FORBIDDEN);
let body: serde_json::Value = response.json();
// The response should be the same as the previous one, so that we don't leak if
// it's the user that is invalid or the password.
assert_eq!(body, old_body);
}
/// Test the response of an unsupported login flow.
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_unsupported_login(pool: PgPool) {
init_tracing();
let state = TestState::from_pool(pool).await.unwrap();
// Try to login with an unsupported login flow.
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.unsupported",
}));
let response = state.request(request).await;
response.assert_status(StatusCode::BAD_REQUEST);
let body: serde_json::Value = response.json();
assert_eq!(body["errcode"], "M_UNRECOGNIZED");
}
/// Test `m.login.token` login flow.
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_login_token_login(pool: PgPool) {
init_tracing();
let state = TestState::from_pool(pool).await.unwrap();
// Provision a user
let mut repo = state.repository().await.unwrap();
let user = repo
.user()
.add(&mut state.rng(), &state.clock, "alice".to_owned())
.await
.unwrap();
repo.save().await.unwrap();
// First try with an invalid token
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.token",
"token": "someinvalidtoken",
}));
let response = state.request(request).await;
response.assert_status(StatusCode::FORBIDDEN);
let body: serde_json::Value = response.json();
assert_eq!(body["errcode"], "M_UNAUTHORIZED");
let (device, token) = get_login_token(&state, &user).await;
// Try to login with the token.
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.token",
"token": token,
}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let body: ResponseBody = response.json();
assert!(!body.access_token.is_empty());
assert_eq!(body.device_id, device);
assert_eq!(body.user_id, "@alice:example.com");
assert_eq!(body.refresh_token, None);
assert_eq!(body.expires_in_ms, None);
// Try again with the same token, it should fail.
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.token",
"token": token,
}));
let response = state.request(request).await;
response.assert_status(StatusCode::FORBIDDEN);
let body: serde_json::Value = response.json();
assert_eq!(body["errcode"], "M_UNAUTHORIZED");
// Try to login, but wait too long before sending the request.
let (_device, token) = get_login_token(&state, &user).await;
// Advance the clock to make the token expire.
state.clock.advance(Duration::minutes(1));
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.token",
"token": token,
}));
let response = state.request(request).await;
response.assert_status(StatusCode::FORBIDDEN);
let body: serde_json::Value = response.json();
assert_eq!(body["errcode"], "M_UNAUTHORIZED");
}
/// Get a login token for a user.
/// Returns the device and the token.
///
/// # Panics
///
/// Panics if the repository fails.
async fn get_login_token(state: &TestState, user: &User) -> (Device, String) {
// XXX: This is a bit manual, but this is what basically the SSO login flow
// does.
let mut repo = state.repository().await.unwrap();
// Generate a device and a token randomly
let token = Alphanumeric.sample_string(&mut state.rng(), 32);
let device = Device::generate(&mut state.rng());
// Start a compat SSO login flow
let login = repo
.compat_sso_login()
.add(
&mut state.rng(),
&state.clock,
token.clone(),
"http://example.com/".parse().unwrap(),
)
.await
.unwrap();
// Complete the flow by fulfilling it with a session
let compat_session = repo
.compat_session()
.add(&mut state.rng(), &state.clock, user, device.clone())
.await
.unwrap();
repo.compat_sso_login()
.fulfill(&state.clock, login, &compat_session)
.await
.unwrap();
repo.save().await.unwrap();
(device, token)
}
}