You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-11-20 12:02:22 +03:00
743 lines
23 KiB
Rust
743 lines
23 KiB
Rust
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use axum::{extract::State, response::IntoResponse, Json};
|
|
use chrono::Duration;
|
|
use hyper::StatusCode;
|
|
use mas_data_model::{CompatSession, CompatSsoLoginState, Device, TokenType, User};
|
|
use mas_storage::{
|
|
compat::{
|
|
CompatAccessTokenRepository, CompatRefreshTokenRepository, CompatSessionRepository,
|
|
CompatSsoLoginRepository,
|
|
},
|
|
job::{JobRepositoryExt, ProvisionDeviceJob},
|
|
user::{UserPasswordRepository, UserRepository},
|
|
BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess,
|
|
};
|
|
use rand::{CryptoRng, RngCore};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_with::{serde_as, skip_serializing_none, DurationMilliSeconds};
|
|
use thiserror::Error;
|
|
use zeroize::Zeroizing;
|
|
|
|
use super::{MatrixError, MatrixHomeserver};
|
|
use crate::{impl_from_error_for_route, passwords::PasswordManager};
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(tag = "type")]
|
|
enum LoginType {
|
|
#[serde(rename = "m.login.password")]
|
|
Password,
|
|
|
|
// we will leave MSC3824 `actions` as undefined for this auth type as unclear
|
|
// how it should be interpreted
|
|
#[serde(rename = "m.login.token")]
|
|
Token,
|
|
|
|
#[serde(rename = "m.login.sso")]
|
|
Sso {
|
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
identity_providers: Vec<SsoIdentityProvider>,
|
|
#[serde(rename = "org.matrix.msc3824.delegated_oidc_compatibility")]
|
|
delegated_oidc_compatibility: bool,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct SsoIdentityProvider {
|
|
id: &'static str,
|
|
name: &'static str,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct LoginTypes {
|
|
flows: Vec<LoginType>,
|
|
}
|
|
|
|
#[tracing::instrument(name = "handlers.compat.login.get", skip_all)]
|
|
pub(crate) async fn get(State(password_manager): State<PasswordManager>) -> impl IntoResponse {
|
|
let flows = if password_manager.is_enabled() {
|
|
vec![
|
|
LoginType::Password,
|
|
LoginType::Sso {
|
|
identity_providers: vec![],
|
|
delegated_oidc_compatibility: true,
|
|
},
|
|
LoginType::Token,
|
|
]
|
|
} else {
|
|
vec![
|
|
LoginType::Sso {
|
|
identity_providers: vec![],
|
|
delegated_oidc_compatibility: true,
|
|
},
|
|
LoginType::Token,
|
|
]
|
|
};
|
|
|
|
let res = LoginTypes { flows };
|
|
|
|
Json(res)
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct RequestBody {
|
|
#[serde(flatten)]
|
|
credentials: Credentials,
|
|
|
|
#[serde(default)]
|
|
refresh_token: bool,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(tag = "type")]
|
|
pub enum Credentials {
|
|
#[serde(rename = "m.login.password")]
|
|
Password {
|
|
identifier: Identifier,
|
|
password: String,
|
|
},
|
|
|
|
#[serde(rename = "m.login.token")]
|
|
Token { token: String },
|
|
|
|
#[serde(other)]
|
|
Unsupported,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(tag = "type")]
|
|
pub enum Identifier {
|
|
#[serde(rename = "m.id.user")]
|
|
User { user: String },
|
|
|
|
#[serde(other)]
|
|
Unsupported,
|
|
}
|
|
|
|
#[skip_serializing_none]
|
|
#[serde_as]
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct ResponseBody {
|
|
access_token: String,
|
|
device_id: Device,
|
|
user_id: String,
|
|
refresh_token: Option<String>,
|
|
#[serde_as(as = "Option<DurationMilliSeconds<i64>>")]
|
|
expires_in_ms: Option<Duration>,
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum RouteError {
|
|
#[error(transparent)]
|
|
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
|
|
|
#[error("unsupported login method")]
|
|
Unsupported,
|
|
|
|
#[error("user not found")]
|
|
UserNotFound,
|
|
|
|
#[error("session not found")]
|
|
SessionNotFound,
|
|
|
|
#[error("user has no password")]
|
|
NoPassword,
|
|
|
|
#[error("password verification failed")]
|
|
PasswordVerificationFailed(#[source] anyhow::Error),
|
|
|
|
#[error("login took too long")]
|
|
LoginTookTooLong,
|
|
|
|
#[error("invalid login token")]
|
|
InvalidLoginToken,
|
|
}
|
|
|
|
impl_from_error_for_route!(mas_storage::RepositoryError);
|
|
|
|
impl IntoResponse for RouteError {
|
|
fn into_response(self) -> axum::response::Response {
|
|
sentry::capture_error(&self);
|
|
match self {
|
|
Self::Internal(_) | Self::SessionNotFound => MatrixError {
|
|
errcode: "M_UNKNOWN",
|
|
error: "Internal server error",
|
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
|
},
|
|
Self::Unsupported => MatrixError {
|
|
errcode: "M_UNRECOGNIZED",
|
|
error: "Invalid login type",
|
|
status: StatusCode::BAD_REQUEST,
|
|
},
|
|
Self::UserNotFound | Self::NoPassword | Self::PasswordVerificationFailed(_) => {
|
|
MatrixError {
|
|
errcode: "M_UNAUTHORIZED",
|
|
error: "Invalid username/password",
|
|
status: StatusCode::FORBIDDEN,
|
|
}
|
|
}
|
|
Self::LoginTookTooLong => MatrixError {
|
|
errcode: "M_UNAUTHORIZED",
|
|
error: "Login token expired",
|
|
status: StatusCode::FORBIDDEN,
|
|
},
|
|
Self::InvalidLoginToken => MatrixError {
|
|
errcode: "M_UNAUTHORIZED",
|
|
error: "Invalid login token",
|
|
status: StatusCode::FORBIDDEN,
|
|
},
|
|
}
|
|
.into_response()
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(name = "handlers.compat.login.post", skip_all, err)]
|
|
pub(crate) async fn post(
|
|
mut rng: BoxRng,
|
|
clock: BoxClock,
|
|
State(password_manager): State<PasswordManager>,
|
|
mut repo: BoxRepository,
|
|
State(homeserver): State<MatrixHomeserver>,
|
|
Json(input): Json<RequestBody>,
|
|
) -> Result<impl IntoResponse, RouteError> {
|
|
let (session, user) = match (password_manager.is_enabled(), input.credentials) {
|
|
(
|
|
true,
|
|
Credentials::Password {
|
|
identifier: Identifier::User { user },
|
|
password,
|
|
},
|
|
) => {
|
|
user_password_login(
|
|
&mut rng,
|
|
&clock,
|
|
&password_manager,
|
|
&mut repo,
|
|
user,
|
|
password,
|
|
)
|
|
.await?
|
|
}
|
|
|
|
(_, Credentials::Token { token }) => token_login(&mut repo, &clock, &token).await?,
|
|
|
|
_ => {
|
|
return Err(RouteError::Unsupported);
|
|
}
|
|
};
|
|
|
|
let user_id = format!("@{username}:{homeserver}", username = user.username);
|
|
|
|
// If the client asked for a refreshable token, make it expire
|
|
let expires_in = if input.refresh_token {
|
|
// TODO: this should be configurable
|
|
Some(Duration::minutes(5))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let access_token = TokenType::CompatAccessToken.generate(&mut rng);
|
|
let access_token = repo
|
|
.compat_access_token()
|
|
.add(&mut rng, &clock, &session, access_token, expires_in)
|
|
.await?;
|
|
|
|
let refresh_token = if input.refresh_token {
|
|
let refresh_token = TokenType::CompatRefreshToken.generate(&mut rng);
|
|
let refresh_token = repo
|
|
.compat_refresh_token()
|
|
.add(&mut rng, &clock, &session, &access_token, refresh_token)
|
|
.await?;
|
|
Some(refresh_token.token)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
repo.save().await?;
|
|
|
|
Ok(Json(ResponseBody {
|
|
access_token: access_token.token,
|
|
device_id: session.device,
|
|
user_id,
|
|
refresh_token,
|
|
expires_in_ms: expires_in,
|
|
}))
|
|
}
|
|
|
|
async fn token_login(
|
|
repo: &mut BoxRepository,
|
|
clock: &dyn Clock,
|
|
token: &str,
|
|
) -> Result<(CompatSession, User), RouteError> {
|
|
let login = repo
|
|
.compat_sso_login()
|
|
.find_by_token(token)
|
|
.await?
|
|
.ok_or(RouteError::InvalidLoginToken)?;
|
|
|
|
let now = clock.now();
|
|
let session_id = match login.state {
|
|
CompatSsoLoginState::Pending => {
|
|
tracing::error!(
|
|
compat_sso_login.id = %login.id,
|
|
"Exchanged a token for a login that was not fullfilled yet"
|
|
);
|
|
return Err(RouteError::InvalidLoginToken);
|
|
}
|
|
CompatSsoLoginState::Fulfilled {
|
|
fulfilled_at,
|
|
session_id,
|
|
..
|
|
} => {
|
|
if now > fulfilled_at + Duration::seconds(30) {
|
|
return Err(RouteError::LoginTookTooLong);
|
|
}
|
|
|
|
session_id
|
|
}
|
|
CompatSsoLoginState::Exchanged {
|
|
exchanged_at,
|
|
session_id,
|
|
..
|
|
} => {
|
|
if now > exchanged_at + Duration::seconds(30) {
|
|
// TODO: log that session out
|
|
tracing::error!(
|
|
compat_sso_login.id = %login.id,
|
|
compat_session.id = %session_id,
|
|
"Login token exchanged a second time more than 30s after"
|
|
);
|
|
}
|
|
|
|
return Err(RouteError::InvalidLoginToken);
|
|
}
|
|
};
|
|
|
|
let session = repo
|
|
.compat_session()
|
|
.lookup(session_id)
|
|
.await?
|
|
.ok_or(RouteError::SessionNotFound)?;
|
|
|
|
let user = repo
|
|
.user()
|
|
.lookup(session.user_id)
|
|
.await?
|
|
.ok_or(RouteError::UserNotFound)?;
|
|
|
|
repo.compat_sso_login().exchange(clock, login).await?;
|
|
|
|
Ok((session, user))
|
|
}
|
|
|
|
async fn user_password_login(
|
|
mut rng: &mut (impl RngCore + CryptoRng + Send),
|
|
clock: &impl Clock,
|
|
password_manager: &PasswordManager,
|
|
repo: &mut BoxRepository,
|
|
username: String,
|
|
password: String,
|
|
) -> Result<(CompatSession, User), RouteError> {
|
|
// Find the user
|
|
let user = repo
|
|
.user()
|
|
.find_by_username(&username)
|
|
.await?
|
|
.ok_or(RouteError::UserNotFound)?;
|
|
|
|
// Lookup its password
|
|
let user_password = repo
|
|
.user_password()
|
|
.active(&user)
|
|
.await?
|
|
.ok_or(RouteError::NoPassword)?;
|
|
|
|
// Verify the password
|
|
let password = Zeroizing::new(password.into_bytes());
|
|
|
|
let new_password_hash = password_manager
|
|
.verify_and_upgrade(
|
|
&mut rng,
|
|
user_password.version,
|
|
password,
|
|
user_password.hashed_password.clone(),
|
|
)
|
|
.await
|
|
.map_err(RouteError::PasswordVerificationFailed)?;
|
|
|
|
if let Some((version, hashed_password)) = new_password_hash {
|
|
// Save the upgraded password if needed
|
|
repo.user_password()
|
|
.add(
|
|
&mut rng,
|
|
clock,
|
|
&user,
|
|
version,
|
|
hashed_password,
|
|
Some(&user_password),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
// Now that the user credentials have been verified, start a new compat session
|
|
let device = Device::generate(&mut rng);
|
|
repo.job()
|
|
.schedule_job(ProvisionDeviceJob::new(&user, &device))
|
|
.await?;
|
|
|
|
let session = repo
|
|
.compat_session()
|
|
.add(&mut rng, clock, &user, device)
|
|
.await?;
|
|
|
|
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 get the login flows
|
|
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 the server doesn't allow login with a password if the password
|
|
/// manager is disabled
|
|
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
|
async fn test_password_disabled(pool: PgPool) {
|
|
init_tracing();
|
|
let state = {
|
|
let mut state = TestState::from_pool(pool).await.unwrap();
|
|
state.password_manager = PasswordManager::disabled();
|
|
state
|
|
};
|
|
|
|
// Now let's get the login flows
|
|
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.sso",
|
|
"org.matrix.msc3824.delegated_oidc_compatibility": true,
|
|
},
|
|
{
|
|
"type": "m.login.token",
|
|
}
|
|
],
|
|
})
|
|
);
|
|
|
|
// Try to login with a password, it should be rejected
|
|
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::BAD_REQUEST);
|
|
let body: serde_json::Value = response.json();
|
|
assert_eq!(body["errcode"], "M_UNRECOGNIZED");
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|