test(oidc): Use mock server and client as much as possible

We keep the mock backend for endpoints that require an ID token for now,
as it would involve generating them on the fly.
And since support for ID tokens is going to be removed, it is not worth
it to implement that.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
This commit is contained in:
Kévin Commaille
2025-02-25 12:40:11 +01:00
committed by Damir Jelić
parent 5791ac9b76
commit 9b406cff87
7 changed files with 403 additions and 388 deletions

View File

@@ -27,7 +27,7 @@ use mas_oidc_client::{
client_credentials::ClientCredentials,
errors::ClientErrorCode,
iana::oauth::OAuthTokenTypeHint,
oidc::{ProviderMetadata, ProviderMetadataVerificationError, VerifiedProviderMetadata},
oidc::{ProviderMetadataVerificationError, VerifiedProviderMetadata},
registration::{ClientRegistrationResponse, VerifiedClientMetadata},
IdToken,
},
@@ -35,14 +35,12 @@ use mas_oidc_client::{
use url::Url;
use super::{OidcBackend, OidcError, RefreshedSessionTokens};
use crate::authentication::oidc::{AuthorizationCode, OauthDiscoveryError, OidcSessionTokens};
use crate::{
authentication::oidc::{AuthorizationCode, OauthDiscoveryError, OidcSessionTokens},
test_utils::mocks::oauth::MockServerMetadataBuilder,
};
pub(crate) const ISSUER_URL: &str = "https://oidc.example.com/issuer";
pub(crate) const AUTHORIZATION_URL: &str = "https://oidc.example.com/authorization";
pub(crate) const REVOCATION_URL: &str = "https://oidc.example.com/revocation";
pub(crate) const REGISTRATION_URL: &str = "https://oidc.example.com/register";
pub(crate) const TOKEN_URL: &str = "https://oidc.example.com/token";
pub(crate) const JWKS_URL: &str = "https://oidc.example.com/jwks";
pub(crate) const CLIENT_ID: &str = "test_client_id";
#[derive(Debug)]
@@ -50,23 +48,6 @@ pub(crate) struct MockImpl {
/// Must be an HTTPS URL.
issuer: String,
/// Must be an HTTPS URL.
authorization_endpoint: String,
/// Must be an HTTPS URL.
token_endpoint: String,
/// Must be an HTTPS URL.
jwks_uri: String,
/// Must be an HTTPS URL.
revocation_endpoint: String,
/// Must be an HTTPS URL.
registration_endpoint: Option<Url>,
account_management_uri: Option<String>,
/// The next session tokens that will be returned by a login or refresh.
next_session_tokens: Option<OidcSessionTokens>,
@@ -87,14 +68,8 @@ impl MockImpl {
pub fn new() -> Self {
Self {
issuer: ISSUER_URL.to_owned(),
authorization_endpoint: AUTHORIZATION_URL.to_owned(),
token_endpoint: TOKEN_URL.to_owned(),
jwks_uri: JWKS_URL.to_owned(),
revocation_endpoint: REVOCATION_URL.to_owned(),
registration_endpoint: Some(Url::parse(REGISTRATION_URL).unwrap()),
next_session_tokens: None,
expected_refresh_token: None,
account_management_uri: None,
num_refreshes: Default::default(),
revoked_tokens: Default::default(),
is_insecure: false,
@@ -115,16 +90,6 @@ impl MockImpl {
self.is_insecure = true;
self
}
pub fn registration_endpoint(mut self, registration_endpoint: Option<Url>) -> Self {
self.registration_endpoint = registration_endpoint;
self
}
pub fn account_management_uri(mut self, uri: String) -> Self {
self.account_management_uri = Some(uri);
self
}
}
#[async_trait::async_trait]
@@ -143,24 +108,10 @@ impl OidcBackend for MockImpl {
.into());
}
Ok(ProviderMetadata {
issuer: Some(self.issuer.clone()),
authorization_endpoint: Some(Url::parse(&self.authorization_endpoint).unwrap()),
revocation_endpoint: Some(Url::parse(&self.revocation_endpoint).unwrap()),
token_endpoint: Some(Url::parse(&self.token_endpoint).unwrap()),
registration_endpoint: self.registration_endpoint.clone(),
jwks_uri: Some(Url::parse(&self.jwks_uri).unwrap()),
response_types_supported: Some(vec![]),
subject_types_supported: Some(vec![]),
id_token_signing_alg_values_supported: Some(vec![]),
account_management_uri: self
.account_management_uri
.as_ref()
.map(|uri| Url::parse(uri).unwrap()),
..Default::default()
}
.validate(&self.issuer)
.map_err(OidcDiscoveryError::from)?)
Ok(MockServerMetadataBuilder::new(&self.issuer)
.build()
.validate(&self.issuer)
.expect("server metadata should pass validation"))
}
async fn trade_authorization_code_for_tokens(

View File

@@ -258,21 +258,22 @@ mod tests {
use matrix_sdk_base::SessionMeta;
use matrix_sdk_test::async_test;
use ruma::{owned_device_id, owned_user_id};
use wiremock::{
matchers::{method, path},
Mock, MockServer, ResponseTemplate,
};
use super::compute_session_hash;
use crate::{
authentication::oidc::{
backend::mock::{MockImpl, ISSUER_URL},
cross_process::SessionHash,
tests,
tests::mock_registered_client_data,
Oidc, OidcSessionTokens,
tests::prev_session_tokens,
Oidc,
},
test_utils::{
client::{
oauth::{mock_session, mock_session_tokens},
MockClientBuilder,
},
mocks::MatrixMockServer,
},
test_utils::test_client_builder,
Error,
};
@@ -281,17 +282,13 @@ mod tests {
// Create a client that will use sqlite databases.
let tmp_dir = tempfile::tempdir()?;
let client = test_client_builder(Some("https://example.org".to_owned()))
.sqlite_store(&tmp_dir, None)
let client = MockClientBuilder::new("https://example.org".to_owned())
.sqlite_store(&tmp_dir)
.unlogged()
.build()
.await
.unwrap();
.await;
let tokens = OidcSessionTokens {
access_token: "prev-access-token".to_owned(),
refresh_token: Some("prev-refresh-token".to_owned()),
latest_id_token: None,
};
let tokens = mock_session_tokens();
client.oidc().enable_cross_process_refresh_lock("test".to_owned()).await?;
@@ -305,7 +302,7 @@ mod tests {
)?;
let session_hash = compute_session_hash(&tokens);
client.oidc().restore_session(tests::mock_session(tokens.clone())).await?;
client.oidc().restore_session(mock_session(tokens.clone(), ISSUER_URL.to_owned())).await?;
assert_eq!(client.oidc().session_tokens().unwrap(), tokens);
@@ -328,37 +325,23 @@ mod tests {
#[async_test]
async fn test_finish_login() -> anyhow::Result<()> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/_matrix/client/r0/account/whoami"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"user_id": "@joe:example.org",
"device_id": "D3V1C31D",
})))
.expect(1)
.named("`GET /whoami` good token")
.mount(&server)
.await;
let server = MatrixMockServer::new().await;
server.mock_who_am_i().ok().expect(1).named("whoami").mount().await;
let tmp_dir = tempfile::tempdir()?;
let client =
test_client_builder(Some(server.uri())).sqlite_store(&tmp_dir, None).build().await?;
let oidc = Oidc { client: client.clone(), backend: Arc::new(MockImpl::new()) };
// Restore registered client.
let (client_credentials, client_metadata) = mock_registered_client_data();
oidc.restore_registered_client(ISSUER_URL.to_owned(), client_metadata, client_credentials);
let client = server
.client_builder()
.sqlite_store(&tmp_dir)
.registered_with_oauth(server.server().uri())
.build()
.await;
let oidc = client.oidc();
// Enable cross-process lock.
oidc.enable_cross_process_refresh_lock("lock".to_owned()).await?;
// Simulate we've done finalize_authorization / restore_session before.
let session_tokens = OidcSessionTokens {
access_token: "access".to_owned(),
refresh_token: Some("refresh".to_owned()),
latest_id_token: None,
};
let session_tokens = mock_session_tokens();
oidc.set_session_tokens(session_tokens.clone());
// Now, finishing logging will get the user and device ids.
@@ -394,22 +377,14 @@ mod tests {
// refreshes whenever one spawns two refreshes around the same time.
let tmp_dir = tempfile::tempdir()?;
let client = test_client_builder(Some("https://example.org".to_owned()))
.sqlite_store(&tmp_dir, None)
let client = MockClientBuilder::new("https://example.org".to_owned())
.sqlite_store(&tmp_dir)
.unlogged()
.build()
.await?;
.await;
let prev_tokens = OidcSessionTokens {
access_token: "prev-access-token".to_owned(),
refresh_token: Some("prev-refresh-token".to_owned()),
latest_id_token: None,
};
let next_tokens = OidcSessionTokens {
access_token: "next-access-token".to_owned(),
refresh_token: Some("next-refresh-token".to_owned()),
latest_id_token: None,
};
let prev_tokens = prev_session_tokens();
let next_tokens = mock_session_tokens();
let backend = Arc::new(
MockImpl::new()
@@ -422,7 +397,7 @@ mod tests {
oidc.enable_cross_process_refresh_lock("lock".to_owned()).await?;
// Restore the session.
oidc.restore_session(tests::mock_session(prev_tokens.clone())).await?;
oidc.restore_session(mock_session(prev_tokens.clone(), ISSUER_URL.to_owned())).await?;
// Immediately try to refresh the access token twice in parallel.
for result in join_all([oidc.refresh_access_token(), oidc.refresh_access_token()]).await {
@@ -450,17 +425,8 @@ mod tests {
#[async_test]
async fn test_cross_process_concurrent_refresh() -> anyhow::Result<()> {
// Create the backend.
let prev_tokens = OidcSessionTokens {
access_token: "prev-access-token".to_owned(),
refresh_token: Some("prev-refresh-token".to_owned()),
latest_id_token: None,
};
let next_tokens = OidcSessionTokens {
access_token: "next-access-token".to_owned(),
refresh_token: Some("next-refresh-token".to_owned()),
latest_id_token: None,
};
let prev_tokens = prev_session_tokens();
let next_tokens = mock_session_tokens();
let backend = Arc::new(
MockImpl::new()
@@ -470,35 +436,39 @@ mod tests {
// Create the first client.
let tmp_dir = tempfile::tempdir()?;
let client = test_client_builder(Some("https://example.org".to_owned()))
.sqlite_store(&tmp_dir, None)
let client = MockClientBuilder::new("https://example.org".to_owned())
.sqlite_store(&tmp_dir)
.unlogged()
.build()
.await?;
.await;
let oidc = Oidc { client: client.clone(), backend: backend.clone() };
oidc.enable_cross_process_refresh_lock("client1".to_owned()).await?;
oidc.restore_session(tests::mock_session(prev_tokens.clone())).await?;
oidc.restore_session(mock_session(prev_tokens.clone(), ISSUER_URL.to_owned())).await?;
// Create a second client, without restoring it, to test that a token update
// before restoration doesn't cause new issues.
let unrestored_client = test_client_builder(Some("https://example.org".to_owned()))
.sqlite_store(&tmp_dir, None)
let unrestored_client = MockClientBuilder::new("https://example.org".to_owned())
.sqlite_store(&tmp_dir)
.unlogged()
.build()
.await?;
.await;
let unrestored_oidc = Oidc { client: unrestored_client.clone(), backend: backend.clone() };
unrestored_oidc.enable_cross_process_refresh_lock("unrestored_client".to_owned()).await?;
{
// Create a third client that will run a refresh while the others two are doing
// nothing.
let client3 = test_client_builder(Some("https://example.org".to_owned()))
.sqlite_store(&tmp_dir, None)
let client3 = MockClientBuilder::new("https://example.org".to_owned())
.sqlite_store(&tmp_dir)
.unlogged()
.build()
.await?;
.await;
let oidc3 = Oidc { client: client3.clone(), backend: backend.clone() };
oidc3.enable_cross_process_refresh_lock("client3".to_owned()).await?;
oidc3.restore_session(tests::mock_session(prev_tokens.clone())).await?;
oidc3.restore_session(mock_session(prev_tokens.clone(), ISSUER_URL.to_owned())).await?;
// Run a refresh in the second client; this will invalidate the tokens from the
// first token.
@@ -530,7 +500,7 @@ mod tests {
Box::new(|_| panic!("save_session_callback shouldn't be called here")),
)?;
oidc.restore_session(tests::mock_session(prev_tokens.clone())).await?;
oidc.restore_session(mock_session(prev_tokens.clone(), ISSUER_URL.to_owned())).await?;
// And this client is now aware of the latest tokens.
let xp_manager =
@@ -588,36 +558,25 @@ mod tests {
#[async_test]
async fn test_logout() -> anyhow::Result<()> {
let server = MatrixMockServer::new().await;
let oauth_server = server.oauth();
oauth_server.mock_server_metadata().ok().expect(1..).named("server_metadata").mount().await;
oauth_server.mock_revocation().ok().expect(1).named("token").mount().await;
let tmp_dir = tempfile::tempdir()?;
let client = test_client_builder(Some("https://example.org".to_owned()))
.sqlite_store(&tmp_dir, None)
.build()
.await?;
let tokens = OidcSessionTokens {
access_token: "prev-access-token".to_owned(),
refresh_token: Some("prev-refresh-token".to_owned()),
latest_id_token: None,
};
let backend = Arc::new(MockImpl::new());
let oidc = Oidc { client: client.clone(), backend: backend.clone() };
let client = server.client_builder().sqlite_store(&tmp_dir).unlogged().build().await;
let oidc = client.oidc();
// Enable cross-process lock.
oidc.enable_cross_process_refresh_lock("lock".to_owned()).await?;
// Restore the session.
oidc.restore_session(tests::mock_session(tokens.clone())).await?;
let tokens = mock_session_tokens();
oidc.restore_session(mock_session(tokens.clone(), server.server().uri())).await?;
oidc.logout().await?;
// The access token has been invalidated.
{
let revoked = backend.revoked_tokens.lock().unwrap();
assert_eq!(revoked.len(), 1);
assert_eq!(*revoked, &[tokens.access_token]);
}
{
// The cross process lock has been correctly updated, and all the hashes are
// empty after a logout.

View File

@@ -351,16 +351,9 @@ impl<'a> LoginWithQrCode<'a> {
mod test {
use assert_matches2::{assert_let, assert_matches};
use futures_util::{join, StreamExt};
use mas_oidc_client::types::{
iana::oauth::OAuthClientAuthenticationMethod,
oidc::ApplicationType,
registration::{ClientMetadata, Localized},
requests::GrantType,
};
use matrix_sdk_base::crypto::types::{qr_login::QrCodeModeData, SecretsBundle};
use matrix_sdk_test::async_test;
use serde_json::json;
use url::Url;
use super::*;
use crate::{
@@ -370,7 +363,7 @@ mod test {
},
config::RequestConfig,
http_client::HttpClient,
test_utils::mocks::MatrixMockServer,
test_utils::{client::oauth::mock_client_metadata, mocks::MatrixMockServer},
};
enum AliceBehaviour {
@@ -388,26 +381,6 @@ mod test {
ExpiredToken,
}
fn client_metadata() -> VerifiedClientMetadata {
let client_uri = Url::parse("https://github.com/matrix-org/matrix-rust-sdk")
.expect("Couldn't parse client URI");
ClientMetadata {
application_type: Some(ApplicationType::Native),
redirect_uris: None,
grant_types: Some(vec![GrantType::DeviceCode]),
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
client_name: Some(Localized::new("test-matrix-rust-sdk-qrlogin".to_owned(), [])),
contacts: Some(vec!["root@127.0.0.1".to_owned()]),
client_uri: Some(Localized::new(client_uri.clone(), [])),
policy_uri: Some(Localized::new(client_uri.clone(), [])),
tos_uri: Some(Localized::new(client_uri, [])),
..Default::default()
}
.validate()
.unwrap()
}
fn secrets_bundle() -> SecretsBundle {
let json = json!({
"cross_signing": {
@@ -512,7 +485,7 @@ mod test {
let qr_code = alice.qr_code_data().clone();
let oidc = bob.oidc();
let login_bob = oidc.login_with_qr_code(&qr_code, client_metadata());
let login_bob = oidc.login_with_qr_code(&qr_code, mock_client_metadata());
let mut updates = login_bob.subscribe_to_progress();
let updates_task = tokio::spawn(async move {
@@ -599,7 +572,7 @@ mod test {
let qr_code = alice.qr_code_data().clone();
let oidc = bob.oidc();
let login_bob = oidc.login_with_qr_code(&qr_code, client_metadata());
let login_bob = oidc.login_with_qr_code(&qr_code, mock_client_metadata());
let mut updates = login_bob.subscribe_to_progress();
let _updates_task = tokio::spawn(async move {
@@ -722,7 +695,7 @@ mod test {
let qr_code = alice.qr_code_data().clone();
let oidc = bob.oidc();
let login_bob = oidc.login_with_qr_code(&qr_code, client_metadata());
let login_bob = oidc.login_with_qr_code(&qr_code, mock_client_metadata());
let mut updates = login_bob.subscribe_to_progress();
let _updates_task = tokio::spawn(async move {

View File

@@ -7,99 +7,66 @@ use mas_oidc_client::{
account_management::AccountManagementActionFull,
authorization_code::AuthorizationValidationData,
},
types::{
errors::ClientErrorCode,
iana::oauth::OAuthClientAuthenticationMethod,
registration::{ClientMetadata, VerifiedClientMetadata},
requests::Prompt,
},
types::{errors::ClientErrorCode, registration::VerifiedClientMetadata, requests::Prompt},
};
use matrix_sdk_base::SessionMeta;
use matrix_sdk_test::{async_test, test_json};
use matrix_sdk_test::async_test;
use ruma::ServerName;
use serde_json::{json, Value as JsonValue};
use serde_json::json;
use stream_assert::{assert_next_matches, assert_pending};
use tempfile::tempdir;
use url::Url;
use wiremock::{
matchers::{method, path},
Mock, MockServer, ResponseTemplate,
Mock, ResponseTemplate,
};
use super::{
backend::mock::{MockImpl, AUTHORIZATION_URL, CLIENT_ID, ISSUER_URL},
registrations::{ClientId, OidcRegistrations},
AuthorizationCode, AuthorizationError, AuthorizationResponse, Oidc, OidcError, OidcSession,
OidcSessionTokens, RedirectUriQueryParseError, UserSession,
backend::mock::{MockImpl, ISSUER_URL},
registrations::OidcRegistrations,
AuthorizationCode, AuthorizationError, AuthorizationResponse, Oidc, OidcError,
OidcSessionTokens, RedirectUriQueryParseError,
};
use crate::{
test_utils::{
client::MockClientBuilder, no_retry_test_client_with_server, test_client_builder,
client::{
oauth::{mock_client_id, mock_client_metadata, mock_session, mock_session_tokens},
MockClientBuilder,
},
mocks::{oauth::MockServerMetadataBuilder, MatrixMockServer},
},
Client, Error,
};
const REDIRECT_URI_STRING: &str = "http://matrix.example.com/oidc/callback";
const REDIRECT_URI_STRING: &str = "http://127.0.0.1:6778/oidc/callback";
pub fn mock_client_metadata() -> VerifiedClientMetadata {
ClientMetadata {
redirect_uris: Some(vec![Url::parse(REDIRECT_URI_STRING).unwrap()]),
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
..ClientMetadata::default()
}
.validate()
.expect("validate client metadata")
}
pub fn mock_registered_client_data() -> (ClientId, VerifiedClientMetadata) {
(ClientId(CLIENT_ID.to_owned()), mock_client_metadata())
}
pub fn mock_session(tokens: OidcSessionTokens) -> OidcSession {
let (client_id, metadata) = mock_registered_client_data();
OidcSession {
client_id,
metadata,
user: UserSession {
meta: SessionMeta {
user_id: ruma::user_id!("@u:e.uk").to_owned(),
device_id: ruma::device_id!("XYZ").to_owned(),
},
tokens,
issuer: ISSUER_URL.to_owned(),
},
}
}
pub async fn mock_environment(
) -> anyhow::Result<(Oidc, MockServer, VerifiedClientMetadata, OidcRegistrations)> {
let server = MockServer::start().await;
let issuer = ISSUER_URL.to_owned();
let issuer_url = Url::parse(&issuer).unwrap();
Mock::given(method("GET"))
.and(path("/_matrix/client/r0/account/whoami"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"user_id": "@joe:example.org",
"device_id": "D3V1C31D"
})))
.mount(&server)
.await;
let client = test_client_builder(Some(server.uri())).build().await?;
let session_tokens = OidcSessionTokens {
access_token: "4cc3ss".to_owned(),
refresh_token: Some("r3fr3$h".to_owned()),
/// Different session tokens than the ones returned by the mock server's
/// token endpoint.
pub(crate) fn prev_session_tokens() -> OidcSessionTokens {
OidcSessionTokens {
access_token: "prev-access-token".to_owned(),
refresh_token: Some("prev-refresh-token".to_owned()),
latest_id_token: None,
};
}
}
async fn mock_environment(
) -> anyhow::Result<(Oidc, MatrixMockServer, VerifiedClientMetadata, OidcRegistrations)> {
let server = MatrixMockServer::new().await;
let issuer_url = Url::parse(ISSUER_URL).unwrap();
server.mock_who_am_i().ok().named("whoami").mount().await;
let client = server.client_builder().unlogged().build().await;
let session_tokens = mock_session_tokens();
let oidc = Oidc {
client,
backend: Arc::new(MockImpl::new().mark_insecure().next_session_tokens(session_tokens)),
};
let (client_id, client_metadata) = mock_registered_client_data();
let client_id = mock_client_id();
let client_metadata = mock_client_metadata();
// The mock backend doesn't support registration so set a static registration.
let mut static_registrations = HashMap::new();
@@ -189,18 +156,20 @@ async fn test_high_level_login_invalid_state() -> anyhow::Result<()> {
#[async_test]
async fn test_login() -> anyhow::Result<()> {
let client = test_client_builder(Some("https://example.org".to_owned())).build().await?;
let server = MatrixMockServer::new().await;
let issuer = server.server().uri();
let oauth_server = server.oauth();
oauth_server.mock_server_metadata().ok().expect(1).mount().await;
let client = server.client_builder().registered_with_oauth(server.server().uri()).build().await;
let oidc = client.oidc();
let device_id = "D3V1C31D".to_owned(); // yo this is 1999 speaking
let oidc = Oidc { client: client.clone(), backend: Arc::new(MockImpl::new()) };
let (client_credentials, client_metadata) = mock_registered_client_data();
oidc.restore_registered_client(ISSUER_URL.to_owned(), client_metadata, client_credentials);
let redirect_uri_str = REDIRECT_URI_STRING;
let redirect_uri = Url::parse(redirect_uri_str)?;
let mut authorization_data = oidc.login(redirect_uri, Some(device_id.clone()))?.build().await?;
let authorization_data = oidc.login(redirect_uri, Some(device_id.clone()))?.build().await?;
tracing::debug!("authorization data URL = {}", authorization_data.url);
@@ -214,7 +183,7 @@ async fn test_login() -> anyhow::Result<()> {
num_expected -= 1;
}
"client_id" => {
assert_eq!(val, CLIENT_ID);
assert_eq!(val, "test_client_id");
num_expected -= 1;
}
"redirect_uri" => {
@@ -246,8 +215,8 @@ async fn test_login() -> anyhow::Result<()> {
let nonce = nonce.context("missing nonce")?;
assert_eq!(nonce, state.nonce);
authorization_data.url.set_query(None);
assert_eq!(authorization_data.url, Url::parse(AUTHORIZATION_URL).unwrap(),);
assert!(authorization_data.url.as_str().starts_with(&issuer));
assert_eq!(authorization_data.url.path(), "/oauth2/authorize");
Ok(())
}
@@ -284,21 +253,17 @@ fn test_authorization_response() -> anyhow::Result<()> {
#[async_test]
async fn test_finish_authorization() -> anyhow::Result<()> {
let client = test_client_builder(Some("https://example.org".to_owned())).build().await?;
let client = MockClientBuilder::new("https://example.org".to_owned())
.registered_with_oauth(ISSUER_URL)
.build()
.await;
let session_tokens = OidcSessionTokens {
access_token: "4cc3ss".to_owned(),
refresh_token: Some("r3fr3$h".to_owned()),
latest_id_token: None,
};
let session_tokens = mock_session_tokens();
let oidc = Oidc {
client: client.clone(),
backend: Arc::new(MockImpl::new().next_session_tokens(session_tokens.clone())),
};
let (client_credentials, client_metadata) = mock_registered_client_data();
oidc.restore_registered_client(ISSUER_URL.to_owned(), client_metadata, client_credentials);
// If the state is missing, then any attempt to finish authorizing will fail.
let res = oidc
.finish_authorization(AuthorizationCode { code: "42".to_owned(), state: "none".to_owned() })
@@ -341,7 +306,7 @@ async fn test_finish_authorization() -> anyhow::Result<()> {
oidc.finish_authorization(AuthorizationCode { code: "1337".to_owned(), state: state.clone() })
.await?;
assert_eq!(oidc.session_tokens(), Some(session_tokens));
assert!(oidc.session_tokens().is_some());
assert!(oidc.data().unwrap().authorization_data.lock().await.get(&state).is_none());
Ok(())
@@ -349,18 +314,12 @@ async fn test_finish_authorization() -> anyhow::Result<()> {
#[async_test]
async fn test_oidc_session() -> anyhow::Result<()> {
let client = test_client_builder(Some("https://example.org".to_owned())).build().await?;
let client = MockClientBuilder::new("https://example.org".to_owned()).unlogged().build().await;
let oidc = client.oidc();
let backend = Arc::new(MockImpl::new());
let oidc = Oidc { client: client.clone(), backend: backend.clone() };
let tokens = OidcSessionTokens {
access_token: "4cc3ss".to_owned(),
refresh_token: Some("r3fr3sh".to_owned()),
latest_id_token: None,
};
let session = mock_session(tokens.clone());
let tokens = mock_session_tokens();
let issuer = ISSUER_URL;
let session = mock_session(tokens.clone(), issuer.to_owned());
oidc.restore_session(session.clone()).await?;
// Test a few extra getters.
@@ -375,7 +334,7 @@ async fn test_oidc_session() -> anyhow::Result<()> {
let full_session = oidc.full_session().unwrap();
assert_eq!(full_session.client_id.0, CLIENT_ID);
assert_eq!(full_session.client_id.0, "test_client_id");
assert_eq!(full_session.metadata, session.metadata);
assert_eq!(full_session.user.meta, session.user.meta);
assert_eq!(full_session.user.tokens, tokens);
@@ -386,29 +345,13 @@ async fn test_oidc_session() -> anyhow::Result<()> {
#[async_test]
async fn test_insecure_clients() -> anyhow::Result<()> {
let server = MockServer::start().await;
let server_url = server.uri();
let server = MatrixMockServer::new().await;
let server_url = server.server().uri();
Mock::given(method("GET"))
.and(path("/.well-known/matrix/client"))
.respond_with(ResponseTemplate::new(200).set_body_raw(
test_json::WELL_KNOWN.to_string().replace("HOMESERVER_URL", server_url.as_ref()),
"application/json",
))
.mount(&server)
.await;
server.mock_well_known().ok().expect(1).named("well_known").mount().await;
let prev_tokens = OidcSessionTokens {
access_token: "prev-access-token".to_owned(),
refresh_token: Some("prev-refresh-token".to_owned()),
latest_id_token: None,
};
let next_tokens = OidcSessionTokens {
access_token: "next-access-token".to_owned(),
refresh_token: Some("next-refresh-token".to_owned()),
latest_id_token: None,
};
let prev_tokens = prev_session_tokens();
let next_tokens = mock_session_tokens();
for client in [
// Create an insecure client with the homeserver_url method.
@@ -430,7 +373,7 @@ async fn test_insecure_clients() -> anyhow::Result<()> {
let oidc = Oidc { client: client.clone(), backend: backend.clone() };
// Restore the previous session so we have an existing set of refresh tokens.
oidc.restore_session(mock_session(prev_tokens.clone())).await?;
oidc.restore_session(mock_session(prev_tokens.clone(), ISSUER_URL.to_owned())).await?;
let mut session_token_stream = oidc.session_tokens_stream().expect("stream available");
@@ -454,47 +397,56 @@ async fn test_insecure_clients() -> anyhow::Result<()> {
#[async_test]
async fn test_register_client() {
let client = test_client_builder(Some("https://example.org".to_owned())).build().await.unwrap();
let server = MatrixMockServer::new().await;
let oauth_server = server.oauth();
let client = server.client_builder().unlogged().build().await;
let oidc = client.oidc();
let client_metadata = mock_client_metadata();
// Server doesn't support registration, it fails.
let backend = Arc::new(MockImpl::new().registration_endpoint(None));
let oidc = Oidc { client: client.clone(), backend };
oauth_server
.mock_server_metadata()
.ok_without_registration()
.expect(1)
.named("metadata_without_registration")
.mount()
.await;
let result = oidc.register_client(client_metadata.clone(), None).await;
assert_matches!(result, Err(OidcError::NoRegistrationSupport));
server.verify_and_reset().await;
// Server supports registration, it succeeds.
let backend = Arc::new(MockImpl::new());
let oidc = Oidc { client: client.clone(), backend };
oauth_server
.mock_server_metadata()
.ok()
.expect(1)
.named("metadata_with_registration")
.mount()
.await;
oauth_server.mock_registration().ok().expect(1).named("registration").mount().await;
let response = oidc.register_client(client_metadata.clone(), None).await.unwrap();
assert_eq!(response.client_id, CLIENT_ID);
assert_eq!(response.client_id, "test_client_id");
let auth_data = oidc.data().unwrap();
assert_eq!(auth_data.issuer, ISSUER_URL);
// There is a difference of ending slash between the strings so we parse them
// with `Url` which will normalize that.
assert_eq!(Url::parse(&auth_data.issuer), Url::parse(&server.server().uri()));
assert_eq!(auth_data.client_id.0, response.client_id);
assert_eq!(auth_data.metadata, client_metadata);
}
#[async_test]
async fn test_management_url_cache() {
let client = MockClientBuilder::new("http://localhost".to_owned()).unlogged().build().await;
let backend = Arc::new(
MockImpl::new().mark_insecure().account_management_uri("http://localhost".to_owned()),
);
let oidc = Oidc { client: client.clone(), backend: backend.clone() };
let server = MatrixMockServer::new().await;
let tokens = OidcSessionTokens {
access_token: "4cc3ss".to_owned(),
refresh_token: Some("r3fr3sh".to_owned()),
latest_id_token: None,
};
let oauth_server = server.oauth();
oauth_server.mock_server_metadata().ok().expect(1).mount().await;
let session = mock_session(tokens.clone());
oidc.restore_session(session.clone())
.await
.expect("We should be able to restore an OIDC session");
let client = server.client_builder().logged_in_with_oauth(server.server().uri()).build().await;
let oidc = client.oidc();
// The cache should not contain the entry.
assert!(!client.inner.caches.provider_metadata.lock().await.contains("PROVIDER_METADATA"));
@@ -508,25 +460,22 @@ async fn test_management_url_cache() {
// Check that the provider metadata has been inserted into the cache.
assert!(client.inner.caches.provider_metadata.lock().await.contains("PROVIDER_METADATA"));
}
fn mock_oidc_provider_metadata(issuer: &str) -> JsonValue {
json!({
"issuer": issuer,
"authorization_endpoint": issuer,
"token_endpoint": issuer,
"jwks_uri": issuer,
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["rs256"],
})
// Another parameter doesn't make another request for the metadata.
let management_url = oidc
.account_management_url(Some(AccountManagementActionFull::SessionsList))
.await
.expect("We should be able to fetch the account management url");
assert!(management_url.is_some());
}
#[async_test]
async fn test_provider_metadata() {
let (client, server) = no_retry_test_client_with_server().await;
let server = MatrixMockServer::new().await;
let client = server.client_builder().unlogged().build().await;
let oidc = client.oidc();
let issuer = server.uri();
let issuer = server.server().uri();
// The endpoint is not mocked so it is not supported.
let error = oidc.provider_metadata().await.unwrap_err();
@@ -538,28 +487,21 @@ async fn test_provider_metadata() {
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"issuer": issuer})))
.expect(1)
.named("auth_issuer")
.mount(&server)
.mount(server.server())
.await;
let metadata = MockServerMetadataBuilder::new(&issuer).build();
Mock::given(method("GET"))
.and(path("/.well-known/openid-configuration"))
.respond_with(
ResponseTemplate::new(200).set_body_json(mock_oidc_provider_metadata(&issuer)),
)
.respond_with(ResponseTemplate::new(200).set_body_json(metadata))
.expect(1)
.named("openid-configuration")
.mount(&server)
.mount(server.server())
.await;
oidc.provider_metadata().await.unwrap();
// Mock the `GET /auth_metadata` endpoint.
Mock::given(method("GET"))
.and(path("/_matrix/client/unstable/org.matrix.msc2965/auth_metadata"))
.respond_with(
ResponseTemplate::new(200).set_body_json(mock_oidc_provider_metadata(&issuer)),
)
.expect(1)
.named("auth_metadata")
.mount(&server)
.await;
let oauth_server = server.oauth();
oauth_server.mock_server_metadata().ok().expect(1).named("auth_metadata").mount().await;
oidc.provider_metadata().await.unwrap();
}

View File

@@ -15,7 +15,7 @@
//! Augmented [`ClientBuilder`] that can set up an already logged-in user.
use matrix_sdk_base::{store::StoreConfig, SessionMeta};
use ruma::{api::MatrixVersion, device_id, user_id};
use ruma::{api::MatrixVersion, owned_device_id, owned_user_id};
use crate::{
authentication::matrix::{MatrixSession, MatrixSessionTokens},
@@ -27,7 +27,7 @@ use crate::{
#[allow(missing_debug_implementations)]
pub struct MockClientBuilder {
builder: ClientBuilder,
logged_in: bool,
auth_state: AuthState,
}
impl MockClientBuilder {
@@ -36,18 +36,32 @@ impl MockClientBuilder {
/// default).
pub(crate) fn new(homeserver: String) -> Self {
let default_builder = Client::builder()
.homeserver_url(homeserver)
.homeserver_url(&homeserver)
.server_versions([MatrixVersion::V1_12])
.request_config(RequestConfig::new().disable_retry());
Self { builder: default_builder, logged_in: true }
Self { builder: default_builder, auth_state: AuthState::LoggedInWithMatrixAuth }
}
/// Doesn't log-in a user.
///
/// Authenticated requests will fail if this is called.
pub fn unlogged(mut self) -> Self {
self.logged_in = false;
self.auth_state = AuthState::None;
self
}
/// The client is registered with the OAuth 2.0 API.
#[cfg(feature = "experimental-oidc")]
pub fn registered_with_oauth(mut self, issuer: impl Into<String>) -> Self {
self.auth_state = AuthState::RegisteredWithOauth { issuer: issuer.into() };
self
}
/// The user is already logged in with the OAuth 2.0 API.
#[cfg(feature = "experimental-oidc")]
pub fn logged_in_with_oauth(mut self, issuer: impl Into<String>) -> Self {
self.auth_state = AuthState::LoggedInWithOauth { issuer: issuer.into() };
self
}
@@ -57,27 +71,143 @@ impl MockClientBuilder {
self
}
/// Use an SQLite store at the given path for the underlying
/// [`ClientBuilder`].
#[cfg(feature = "sqlite")]
pub fn sqlite_store(mut self, path: impl AsRef<std::path::Path>) -> Self {
self.builder = self.builder.sqlite_store(path, None);
self
}
/// Finish building the client into the final [`Client`] instance.
pub async fn build(self) -> Client {
let client = self.builder.build().await.expect("building client failed");
if self.logged_in {
client
.matrix_auth()
.restore_session(MatrixSession {
meta: SessionMeta {
user_id: user_id!("@example:localhost").to_owned(),
device_id: device_id!("DEVICEID").to_owned(),
},
tokens: MatrixSessionTokens {
access_token: "1234".to_owned(),
refresh_token: None,
},
})
.await
.unwrap();
}
self.auth_state.maybe_restore_client(&client).await;
client
}
}
/// The possible authentication states of a [`Client`] built with
/// [`MockClientBuilder`].
enum AuthState {
/// The client is not logged in.
None,
/// The client is logged in with the native Matrix API.
LoggedInWithMatrixAuth,
/// The client is registered with the OAuth 2.0 API.
#[cfg(feature = "experimental-oidc")]
RegisteredWithOauth { issuer: String },
/// The client is logged in with the OAuth 2.0 API.
#[cfg(feature = "experimental-oidc")]
LoggedInWithOauth { issuer: String },
}
impl AuthState {
/// Restore the given [`Client`] according to this [`AuthState`], if
/// necessary.
async fn maybe_restore_client(self, client: &Client) {
match self {
AuthState::None => {}
AuthState::LoggedInWithMatrixAuth => {
client
.matrix_auth()
.restore_session(MatrixSession {
meta: mock_session_meta(),
tokens: MatrixSessionTokens {
access_token: "1234".to_owned(),
refresh_token: None,
},
})
.await
.unwrap();
}
#[cfg(feature = "experimental-oidc")]
AuthState::RegisteredWithOauth { issuer } => {
client.oidc().restore_registered_client(
issuer,
oauth::mock_client_metadata(),
oauth::mock_client_id(),
);
}
#[cfg(feature = "experimental-oidc")]
AuthState::LoggedInWithOauth { issuer } => {
client
.oidc()
.restore_session(oauth::mock_session(oauth::mock_session_tokens(), issuer))
.await
.unwrap();
}
}
}
}
fn mock_session_meta() -> SessionMeta {
SessionMeta {
user_id: owned_user_id!("@example:localhost"),
device_id: owned_device_id!("DEVICEID"),
}
}
/// Mock client data for the OAuth 2.0 API.
#[cfg(feature = "experimental-oidc")]
pub mod oauth {
use mas_oidc_client::types::{
iana::oauth::OAuthClientAuthenticationMethod,
oidc::ApplicationType,
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
requests::GrantType,
};
use url::Url;
use crate::authentication::oidc::{
registrations::ClientId, OidcSession, OidcSessionTokens, UserSession,
};
/// An OAuth 2.0 `ClientId`, for unit or integration tests.
pub fn mock_client_id() -> ClientId {
ClientId("test_client_id".to_owned())
}
/// `VerifiedClientMetadata` that should be valid in most cases, for unit or
/// integration tests.
pub fn mock_client_metadata() -> VerifiedClientMetadata {
let redirect_uri = Url::parse("http://127.0.0.1/").expect("redirect URI should be valid");
let client_uri = Url::parse("https://github.com/matrix-org/matrix-rust-sdk")
.expect("client URI should be valid");
ClientMetadata {
application_type: Some(ApplicationType::Native),
redirect_uris: Some(vec![redirect_uri]),
grant_types: Some(vec![
GrantType::AuthorizationCode,
GrantType::RefreshToken,
GrantType::DeviceCode,
]),
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
client_name: Some(Localized::new("matrix-rust-sdk-test".to_owned(), [])),
client_uri: Some(Localized::new(client_uri, [])),
..Default::default()
}
.validate()
.expect("client metadata should pass validation")
}
/// An [`OidcSessionTokens`], for unit or integration tests.
pub fn mock_session_tokens() -> OidcSessionTokens {
OidcSessionTokens {
access_token: "1234".to_owned(),
refresh_token: Some("ZYXWV".to_owned()),
latest_id_token: None,
}
}
/// An [`OidcSession`] to restore, for unit or integration tests.
pub fn mock_session(tokens: OidcSessionTokens, issuer: String) -> OidcSession {
OidcSession {
client_id: mock_client_id(),
metadata: mock_client_metadata(),
user: UserSession { meta: super::mock_session_meta(), tokens, issuer },
}
}
}

View File

@@ -976,6 +976,13 @@ impl MatrixMockServer {
.and(header("authorization", "Bearer 1234"));
MockEndpoint { mock, server: &self.server, endpoint: QueryKeysEndpoint }
}
/// Creates a prebuilt mock for the endpoint used to discover the URL of a
/// homeserver.
pub fn mock_well_known(&self) -> MockEndpoint<'_, WellKnownEndpoint> {
let mock = Mock::given(method("GET")).and(path_regex(r"^/.well-known/matrix/client"));
MockEndpoint { mock, server: &self.server, endpoint: WellKnownEndpoint }
}
}
/// Parameter to [`MatrixMockServer::sync_room`].
@@ -2383,11 +2390,11 @@ impl<'a> MockEndpoint<'a, SetRoomPinnedEventsEndpoint> {
pub struct WhoAmIEndpoint;
impl<'a> MockEndpoint<'a, WhoAmIEndpoint> {
/// Returns a successful response with the user ID `@joe:example.org` and no
/// device ID.
/// Returns a successful response with a user ID and device ID.
pub fn ok(self) -> MatrixMock<'a> {
let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"user_id": "@joe:example.org",
"device_id": "D3V1C31D",
})));
MatrixMock { server: self.server, mock }
@@ -2423,3 +2430,19 @@ impl<'a> MockEndpoint<'a, QueryKeysEndpoint> {
MatrixMock { server: self.server, mock }
}
}
/// A prebuilt mock for `GET /.well-known/matrix/client` request.
pub struct WellKnownEndpoint;
impl<'a> MockEndpoint<'a, WellKnownEndpoint> {
/// Returns a successful response.
pub fn ok(self) -> MatrixMock<'a> {
let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"m.homeserver": {
"base_url": self.server.uri(),
},
})));
MatrixMock { server: self.server, mock }
}
}

View File

@@ -98,6 +98,13 @@ impl OauthMockServer<'_> {
let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/token"));
MockEndpoint { mock, server: self.server, endpoint: TokenEndpoint }
}
/// Creates a prebuilt mock for the OAuth 2.0 endpoint used to revoke a
/// token.
pub fn mock_revocation(&self) -> MockEndpoint<'_, RevocationEndpoint> {
let mock = Mock::given(method("POST")).and(path_regex(r"^/oauth2/revoke"));
MockEndpoint { mock, server: self.server, endpoint: RevocationEndpoint }
}
}
/// A prebuilt mock for a `GET /auth_metadata` request.
@@ -122,22 +129,33 @@ impl<'a> MockEndpoint<'a, ServerMetadataEndpoint> {
MatrixMock { server: self.server, mock }
}
/// Returns a successful metadata response without the registration
/// endpoint.
pub fn ok_without_registration(self) -> MatrixMock<'a> {
let metadata =
MockServerMetadataBuilder::new(&self.server.uri()).without_registration().build();
let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(metadata));
MatrixMock { server: self.server, mock }
}
}
/// Helper struct to construct a `ProviderMetadata` for integration tests.
#[derive(Debug, Clone)]
struct MockServerMetadataBuilder {
pub struct MockServerMetadataBuilder {
issuer: Url,
with_device_authorization: bool,
with_registration: bool,
}
impl MockServerMetadataBuilder {
/// Construct a `MockServerMetadataBuilder` that will generate all the
/// supported fields.
fn new(issuer: &str) -> Self {
pub fn new(issuer: &str) -> Self {
let issuer = Url::parse(issuer).expect("We should be able to parse the issuer");
Self { issuer, with_device_authorization: true }
Self { issuer, with_device_authorization: true, with_registration: true }
}
/// Don't generate the field for the device authorization endpoint.
@@ -146,6 +164,12 @@ impl MockServerMetadataBuilder {
self
}
/// Don't generate the field for the registration endpoint.
fn without_registration(mut self) -> Self {
self.with_registration = false;
self
}
/// The authorization endpoint of this server.
fn authorization_endpoint(&self) -> Url {
self.issuer.join("oauth2/authorize").unwrap()
@@ -185,13 +209,14 @@ impl MockServerMetadataBuilder {
pub fn build(&self) -> ProviderMetadata {
let device_authorization_endpoint =
self.with_device_authorization.then(|| self.device_authorization_endpoint());
let registration_endpoint = self.with_registration.then(|| self.registration_endpoint());
ProviderMetadata {
issuer: Some(self.issuer.to_string()),
authorization_endpoint: Some(self.authorization_endpoint()),
token_endpoint: Some(self.token_endpoint()),
jwks_uri: Some(self.jwks_uri()),
registration_endpoint: Some(self.registration_endpoint()),
registration_endpoint,
revocation_endpoint: Some(self.revocation_endpoint()),
account_management_uri: Some(self.account_management_uri()),
device_authorization_endpoint,
@@ -279,3 +304,15 @@ impl<'a> MockEndpoint<'a, TokenEndpoint> {
MatrixMock { server: self.server, mock }
}
}
/// A prebuilt mock for a `POST /oauth/revoke` request.
pub struct RevocationEndpoint;
impl<'a> MockEndpoint<'a, RevocationEndpoint> {
/// Returns a successful revocation response.
pub fn ok(self) -> MatrixMock<'a> {
let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({})));
MatrixMock { server: self.server, mock }
}
}