From db38d25b5b403e72ecc352ea00cbf22366789e32 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 20 Jun 2024 13:23:04 +0100 Subject: [PATCH] sdk: Test the OIDC helper methods. --- crates/matrix-sdk/src/oidc/tests.rs | 133 ++++++++++++++++++++++++++-- 1 file changed, 128 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk/src/oidc/tests.rs b/crates/matrix-sdk/src/oidc/tests.rs index 5d574fc8f..d42419068 100644 --- a/crates/matrix-sdk/src/oidc/tests.rs +++ b/crates/matrix-sdk/src/oidc/tests.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use anyhow::Context as _; use assert_matches::assert_matches; @@ -14,7 +14,9 @@ use mas_oidc_client::{ use matrix_sdk_base::SessionMeta; use matrix_sdk_test::{async_test, test_json}; use ruma::ServerName; +use serde_json::json; use stream_assert::{assert_next_matches, assert_pending}; +use tempfile::tempdir; use url::Url; use wiremock::{ matchers::{method, path}, @@ -26,15 +28,20 @@ use super::{ AuthorizationCode, AuthorizationError, AuthorizationResponse, Oidc, OidcError, OidcSession, OidcSessionTokens, RedirectUriQueryParseError, UserSession, }; -use crate::{test_utils::test_client_builder, Client}; +use crate::{ + oidc::registrations::{ClientId, OidcRegistrations}, + test_utils::test_client_builder, + Client, Error, +}; const CLIENT_ID: &str = "test_client_id"; +const REDIRECT_URI_STRING: &str = "http://matrix.example.com/oidc/callback"; pub fn mock_registered_client_data() -> (ClientCredentials, VerifiedClientMetadata) { ( ClientCredentials::None { client_id: CLIENT_ID.to_owned() }, ClientMetadata { - redirect_uris: Some(vec![]), // empty vector is ok lol + redirect_uris: Some(vec![Url::parse(REDIRECT_URI_STRING).unwrap()]), token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None), ..ClientMetadata::default() } @@ -59,6 +66,122 @@ pub fn mock_session(tokens: OidcSessionTokens) -> OidcSession { } } +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/unstable/org.matrix.msc2965/auth_issuer")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({"issuer": issuer}))) + .mount(&server) + .await; + + 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()), + latest_id_token: None, + }; + + let oidc = Oidc { + client: client.clone(), + backend: Arc::new( + MockImpl::new().mark_insecure().next_session_tokens(session_tokens.clone()), + ), + }; + + let (client_credentials, client_metadata) = mock_registered_client_data(); + + // The mock backend doesn't support registration so set a static registration. + let mut static_registrations = HashMap::new(); + static_registrations.insert(issuer_url, ClientId(client_credentials.client_id().to_owned())); + + let registrations_path = tempdir().unwrap().path().join("oidc").join("registrations.json"); + let registrations = + OidcRegistrations::new(®istrations_path, client_metadata.clone(), static_registrations) + .unwrap(); + + Ok((oidc, server, client_metadata, registrations)) +} + +#[async_test] +async fn test_high_level_login() -> anyhow::Result<()> { + let (oidc, _server, metadata, registrations) = mock_environment().await.unwrap(); + + assert!(oidc.issuer().is_none()); + assert!(oidc.client_metadata().is_none()); + assert!(oidc.client_credentials().is_none()); + + let authorization_data = + oidc.url_for_oidc_login(metadata.clone(), registrations).await.unwrap(); + + assert!(oidc.issuer().is_some()); + assert!(oidc.client_metadata().is_some()); + assert!(oidc.client_credentials().is_some()); + + let mut callback_uri = metadata.redirect_uris.clone().unwrap().first().unwrap().clone(); + callback_uri.set_query(Some(&format!("code=42&state={}", authorization_data.state))); + + oidc.login_with_oidc_callback(&authorization_data, callback_uri).await?; + + Ok(()) +} + +#[async_test] +async fn test_high_level_login_cancellation() -> anyhow::Result<()> { + let (oidc, _server, metadata, registrations) = mock_environment().await.unwrap(); + + let authorization_data = + oidc.url_for_oidc_login(metadata.clone(), registrations).await.unwrap(); + + assert!(oidc.issuer().is_some()); + assert!(oidc.client_metadata().is_some()); + assert!(oidc.client_credentials().is_some()); + + let mut callback_uri = metadata.redirect_uris.clone().unwrap().first().unwrap().clone(); + callback_uri + .set_query(Some(&format!("error=access_denied&state={}", authorization_data.state))); + + let error = oidc.login_with_oidc_callback(&authorization_data, callback_uri).await.unwrap_err(); + + assert_matches!(error, Error::Oidc(OidcError::CancelledAuthorization)); + + Ok(()) +} + +#[async_test] +async fn test_high_level_login_invalid_state() -> anyhow::Result<()> { + let (oidc, _server, metadata, registrations) = mock_environment().await.unwrap(); + + let authorization_data = + oidc.url_for_oidc_login(metadata.clone(), registrations).await.unwrap(); + + assert!(oidc.issuer().is_some()); + assert!(oidc.client_metadata().is_some()); + assert!(oidc.client_credentials().is_some()); + + let mut callback_uri = metadata.redirect_uris.clone().unwrap().first().unwrap().clone(); + callback_uri.set_query(Some("code=42&state=imposter_alert")); + + let error = oidc.login_with_oidc_callback(&authorization_data, callback_uri).await.unwrap_err(); + + assert_matches!(error, Error::Oidc(OidcError::InvalidState)); + + Ok(()) +} + #[async_test] async fn test_login() -> anyhow::Result<()> { let client = test_client_builder(Some("https://example.org".to_owned())).build().await?; @@ -70,7 +193,7 @@ async fn test_login() -> anyhow::Result<()> { 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 = "http://matrix.example.com/oidc/callback"; + 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?; @@ -181,7 +304,7 @@ async fn test_finish_authorization() -> anyhow::Result<()> { // Assuming a non-empty state "123"... let state = "state".to_owned(); - let redirect_uri = "http://matrix.example.com/oidc/callback"; + let redirect_uri = REDIRECT_URI_STRING; let auth_validation_data = AuthorizationValidationData { state: state.clone(), nonce: "nonce".to_owned(),