diff --git a/crates/matrix-sdk/src/authentication/oidc/backend/mock.rs b/crates/matrix-sdk/src/authentication/oidc/backend/mock.rs index 043cff419..9cea3b460 100644 --- a/crates/matrix-sdk/src/authentication/oidc/backend/mock.rs +++ b/crates/matrix-sdk/src/authentication/oidc/backend/mock.rs @@ -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, - - account_management_uri: Option, - /// The next session tokens that will be returned by a login or refresh. next_session_tokens: Option, @@ -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) -> 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( diff --git a/crates/matrix-sdk/src/authentication/oidc/cross_process.rs b/crates/matrix-sdk/src/authentication/oidc/cross_process.rs index 1e0453c5f..58a9c1518 100644 --- a/crates/matrix-sdk/src/authentication/oidc/cross_process.rs +++ b/crates/matrix-sdk/src/authentication/oidc/cross_process.rs @@ -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. diff --git a/crates/matrix-sdk/src/authentication/oidc/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oidc/qrcode/login.rs index b610a50b0..0054523a2 100644 --- a/crates/matrix-sdk/src/authentication/oidc/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oidc/qrcode/login.rs @@ -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 { diff --git a/crates/matrix-sdk/src/authentication/oidc/tests.rs b/crates/matrix-sdk/src/authentication/oidc/tests.rs index 8f1ee0f70..eaa46c45a 100644 --- a/crates/matrix-sdk/src/authentication/oidc/tests.rs +++ b/crates/matrix-sdk/src/authentication/oidc/tests.rs @@ -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(); } diff --git a/crates/matrix-sdk/src/test_utils/client.rs b/crates/matrix-sdk/src/test_utils/client.rs index b22494077..8dfe79872 100644 --- a/crates/matrix-sdk/src/test_utils/client.rs +++ b/crates/matrix-sdk/src/test_utils/client.rs @@ -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) -> 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) -> 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) -> 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 }, + } + } +} diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index 2c5975ee5..3af1d02e4 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -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 } + } +} diff --git a/crates/matrix-sdk/src/test_utils/mocks/oauth.rs b/crates/matrix-sdk/src/test_utils/mocks/oauth.rs index 525e9b0a1..4b3fd9856 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/oauth.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/oauth.rs @@ -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 } + } +}