diff --git a/Cargo.lock b/Cargo.lock index 980a3ac69..75cfde78b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3219,8 +3219,8 @@ dependencies = [ "matrix-sdk-test", "mime", "mime2ext", + "oauth2", "once_cell", - "openidconnect", "percent-encoding", "pin-project-lite", "proptest", @@ -3957,37 +3957,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openidconnect" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd50d4a5e7730e754f94d977efe61f611aadd3131f6a2b464f6e3a4167e8ef7" -dependencies = [ - "base64 0.21.7", - "chrono", - "dyn-clone", - "ed25519-dalek", - "hmac", - "http", - "itertools 0.10.5", - "log", - "oauth2", - "p256", - "p384", - "rand", - "rsa", - "serde", - "serde-value", - "serde_json", - "serde_path_to_error", - "serde_plain", - "serde_with", - "sha2", - "subtle", - "thiserror 1.0.63", - "url", -] - [[package]] name = "openssl" version = "0.10.66" @@ -4075,15 +4044,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "overload" version = "0.1.1" @@ -5298,16 +5258,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -5386,15 +5336,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_plain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "0.6.8" diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index b00e3a411..9c452e365 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -53,7 +53,7 @@ experimental-oidc = [ "dep:rand", "dep:sha2", "dep:tower", - "dep:openidconnect", + "dep:oauth2", ] experimental-widgets = ["dep:language-tags", "dep:uuid"] @@ -129,7 +129,7 @@ tokio = { workspace = true, features = ["macros"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] backoff = { version = "0.4.0", features = ["tokio"] } -openidconnect = { version = "4.0.0", optional = true } +oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest"], optional = true } # only activate reqwest's stream feature on non-wasm, the wasm part seems to not # support *sending* streams, which makes it useless for us. reqwest = { workspace = true, features = ["stream", "gzip", "http2"] } diff --git a/crates/matrix-sdk/src/authentication/qrcode/login.rs b/crates/matrix-sdk/src/authentication/qrcode/login.rs index 9bf13daaa..01535f444 100644 --- a/crates/matrix-sdk/src/authentication/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/qrcode/login.rs @@ -24,7 +24,7 @@ use matrix_sdk_base::{ crypto::types::qr_login::{QrCodeData, QrCodeMode}, SessionMeta, }; -use openidconnect::DeviceCodeErrorResponseType; +use oauth2::DeviceCodeErrorResponseType; use ruma::OwnedDeviceId; use tracing::trace; use vodozemac::ecies::CheckCode; @@ -291,38 +291,33 @@ impl<'a> LoginWithQrCode<'a> { } async fn register_client(&self) -> Result { + let oidc = self.client.oidc(); + // Let's figure out the OIDC issuer, this fetches the info from the homeserver. - let issuer = self - .client - .oidc() + let issuer = oidc .fetch_authentication_issuer() .await .map_err(DeviceAuhorizationOidcError::AuthenticationIssuer)?; // Now we register the client with the OIDC provider. let registration_response = - self.client.oidc().register_client(&issuer, self.client_metadata.clone(), None).await?; + oidc.register_client(&issuer, self.client_metadata.clone(), None).await?; - // Now we need to put the relevant data we got from the regustration response + // Now we need to put the relevant data we got from the registration response // into the `Client`. // TODO: Why isn't `oidc().register_client()` doing this automatically? - self.client.oidc().restore_registered_client( + oidc.restore_registered_client( issuer.clone(), self.client_metadata.clone(), ClientCredentials::None { client_id: registration_response.client_id.clone() }, ); - // We're now switching to the openidconnect crate, it has a bit of a strange API + // We're now switching to the oauth2 crate, it has a bit of a strange API // where you need to provide the HTTP client in every call you make. let http_client = self.client.inner.http_client.clone(); + let server_metadata = oidc.provider_metadata().await?; - OidcClient::new( - registration_response.client_id, - issuer, - http_client, - registration_response.client_secret.as_deref(), - ) - .await + OidcClient::new(registration_response.client_id, &server_metadata, http_client) } } @@ -633,6 +628,7 @@ mod test { }))) .expect(1) + .named("auth_issuer") .mount(server) .await; @@ -640,6 +636,7 @@ mod test { .and(path("/.well-known/openid-configuration")) .respond_with(ResponseTemplate::new(200).set_body_json(open_id_configuration(server))) .expect(1..) + .named("server_metadata") .mount(server) .await; @@ -650,13 +647,14 @@ mod test { "client_id_issued_at": 1716375696 }))) .expect(1) + .named("registration_endpoint") .mount(server) .await; Mock::given(method("GET")) .and(path("/oauth2/keys.json")) .respond_with(ResponseTemplate::new(200).set_body_json(keys_json())) - .expect(1) + .named("jwks") .mount(server) .await; @@ -664,12 +662,14 @@ mod test { .and(path("/oauth2/device")) .respond_with(ResponseTemplate::new(200).set_body_json(device_code(server))) .expect(1) + .named("device_authorization_endpoint") .mount(server) .await; Mock::given(method("POST")) .and(path("/oauth2/token")) .respond_with(token_response) + .named("token_endpoint") .mount(server) .await; } diff --git a/crates/matrix-sdk/src/authentication/qrcode/messages.rs b/crates/matrix-sdk/src/authentication/qrcode/messages.rs index 2abbef5a9..ea59948ba 100644 --- a/crates/matrix-sdk/src/authentication/qrcode/messages.rs +++ b/crates/matrix-sdk/src/authentication/qrcode/messages.rs @@ -14,8 +14,8 @@ use matrix_sdk_base::crypto::types::SecretsBundle; use matrix_sdk_common::deserialized_responses::PrivOwnedStr; -use openidconnect::{ - core::CoreDeviceAuthorizationResponse, EndUserVerificationUrl, VerificationUriComplete, +use oauth2::{ + EndUserVerificationUrl, StandardDeviceAuthorizationResponse, VerificationUriComplete, }; use ruma::serde::StringEnum; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -107,8 +107,8 @@ impl QrAuthMessage { } } -impl From<&CoreDeviceAuthorizationResponse> for AuthorizationGrant { - fn from(value: &CoreDeviceAuthorizationResponse) -> Self { +impl From<&StandardDeviceAuthorizationResponse> for AuthorizationGrant { + fn from(value: &StandardDeviceAuthorizationResponse) -> Self { Self { verification_uri: value.verification_uri().clone(), verification_uri_complete: value.verification_uri_complete().cloned(), diff --git a/crates/matrix-sdk/src/authentication/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/qrcode/mod.rs index 0044f36a6..443fb7848 100644 --- a/crates/matrix-sdk/src/authentication/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/qrcode/mod.rs @@ -24,9 +24,10 @@ use as_variant::as_variant; use matrix_sdk_base::crypto::SecretImportError; -pub use openidconnect::{ - core::CoreErrorResponseType, ConfigurationError, DeviceCodeErrorResponseType, DiscoveryError, - HttpClientError, RequestTokenError, StandardErrorResponse, +pub use oauth2::{ + basic::{BasicErrorResponse, BasicRequestTokenError}, + ConfigurationError, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, HttpClientError, + RequestTokenError, StandardErrorResponse, }; use thiserror::Error; use url::Url; @@ -115,14 +116,9 @@ pub enum DeviceAuhorizationOidcError { #[error(transparent)] Oidc(#[from] crate::authentication::oidc::OidcError), - /// The issuer URL failed to be parsed. - #[error(transparent)] - InvalidIssuerUrl(#[from] url::ParseError), - - /// There was an error with our device configuration right before attempting - /// to wait for the access token to be issued by the OIDC provider. - #[error(transparent)] - Configuration(#[from] ConfigurationError), + /// The OAuth 2.0 server doesn't support the device authorization grant. + #[error("OAuth 2.0 server doesn't support the device authorization grant")] + NoDeviceAuthorizationEndpoint, /// An error happened while we attempted to discover the authentication /// issuer URL. @@ -132,28 +128,14 @@ pub enum DeviceAuhorizationOidcError { /// An error happened while we attempted to request a device authorization /// from the OIDC provider. #[error(transparent)] - DeviceAuthorization( - #[from] - RequestTokenError< - HttpClientError, - StandardErrorResponse, - >, - ), + DeviceAuthorization(#[from] BasicRequestTokenError>), /// An error happened while waiting for the access token to be issued and /// sent to us by the OIDC provider. #[error(transparent)] RequestToken( - #[from] - RequestTokenError< - HttpClientError, - StandardErrorResponse, - >, + #[from] RequestTokenError, DeviceCodeErrorResponse>, ), - - /// An error happened during the discovery of the OIDC provider metadata. - #[error(transparent)] - Discovery(#[from] DiscoveryError>), } impl DeviceAuhorizationOidcError { diff --git a/crates/matrix-sdk/src/authentication/qrcode/oidc_client.rs b/crates/matrix-sdk/src/authentication/qrcode/oidc_client.rs index 1adce855a..423f9509f 100644 --- a/crates/matrix-sdk/src/authentication/qrcode/oidc_client.rs +++ b/crates/matrix-sdk/src/authentication/qrcode/oidc_client.rs @@ -15,117 +15,68 @@ use std::pin::Pin; use futures_core::Future; -use mas_oidc_client::types::scope::{MatrixApiScopeToken, ScopeToken}; -use openidconnect::{ - core::{ - CoreAuthDisplay, CoreAuthPrompt, CoreClaimName, CoreClaimType, CoreClient, - CoreClientAuthMethod, CoreDeviceAuthorizationResponse, CoreErrorResponseType, - CoreGenderClaim, CoreGrantType, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, - CoreJweKeyManagementAlgorithm, CoreResponseMode, CoreResponseType, CoreRevocableToken, - CoreRevocationErrorResponse, CoreSubjectIdentifierType, CoreTokenIntrospectionResponse, - CoreTokenResponse, - }, - AdditionalProviderMetadata, AuthType, ClientId, ClientSecret, DeviceAuthorizationUrl, - EmptyAdditionalClaims, EndpointMaybeSet, EndpointNotSet, EndpointSet, HttpClientError, - HttpRequest, IssuerUrl, OAuth2TokenResponse, ProviderMetadata, Scope, StandardErrorResponse, +use mas_oidc_client::types::{ + oidc::VerifiedProviderMetadata, + scope::{MatrixApiScopeToken, ScopeToken}, +}; +use oauth2::{ + basic::BasicClient, ClientId, DeviceAuthorizationUrl, EndpointNotSet, EndpointSet, + HttpClientError, HttpRequest, Scope, StandardDeviceAuthorizationResponse, TokenResponse, + TokenUrl, }; use vodozemac::Curve25519PublicKey; use super::DeviceAuhorizationOidcError; use crate::{authentication::oidc::OidcSessionTokens, http_client::HttpClient}; -// Obtain the device_authorization_url from the OIDC metadata provider. -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -struct DeviceEndpointProviderMetadata { - device_authorization_endpoint: DeviceAuthorizationUrl, -} -impl AdditionalProviderMetadata for DeviceEndpointProviderMetadata {} - -type DeviceProviderMetadata = ProviderMetadata< - DeviceEndpointProviderMetadata, - CoreAuthDisplay, - CoreClientAuthMethod, - CoreClaimName, - CoreClaimType, - CoreGrantType, - CoreJweContentEncryptionAlgorithm, - CoreJweKeyManagementAlgorithm, - CoreJsonWebKey, - CoreResponseMode, - CoreResponseType, - CoreSubjectIdentifierType, ->; - -/// OpenID Connect Core client. -pub type OidcClientInner< - HasAuthUrl = EndpointSet, +/// Oauth 2.0 Basic client. +type OauthClientInner< + HasAuthUrl = EndpointNotSet, HasDeviceAuthUrl = EndpointSet, HasIntrospectionUrl = EndpointNotSet, HasRevocationUrl = EndpointNotSet, - HasTokenUrl = EndpointMaybeSet, - HasUserInfoUrl = EndpointMaybeSet, -> = openidconnect::Client< - EmptyAdditionalClaims, - CoreAuthDisplay, - CoreGenderClaim, - CoreJweContentEncryptionAlgorithm, - CoreJsonWebKey, - CoreAuthPrompt, - StandardErrorResponse, - CoreTokenResponse, - CoreTokenIntrospectionResponse, - CoreRevocableToken, - CoreRevocationErrorResponse, - HasAuthUrl, - HasDeviceAuthUrl, - HasIntrospectionUrl, - HasRevocationUrl, - HasTokenUrl, - HasUserInfoUrl, ->; + HasTokenUrl = EndpointSet, +> = BasicClient; /// An OIDC specific HTTP client. /// /// This is used to communicate with the OIDC provider exclusively. pub(super) struct OidcClient { - inner: OidcClientInner, + inner: OauthClientInner, http_client: HttpClient, } impl OidcClient { - pub(super) async fn new( + pub(super) fn new( client_id: String, - issuer_url: String, + server_metadata: &VerifiedProviderMetadata, http_client: HttpClient, - client_secret: Option<&str>, ) -> Result { let client_id = ClientId::new(client_id); - let issuer_url = IssuerUrl::new(issuer_url)?; - let client_secret = client_secret.map(|s| ClientSecret::new(s.to_owned())); - // We're fetching the provider metadata which will contain the device - // authorization endpoint. We can use this endpoint to attempt to log in - // this new device, though the other, existing device will do that using the + let token_endpoint = TokenUrl::from_url(server_metadata.token_endpoint().clone()); + + // We can use the device authorization endpoint to attempt to log in this new + // device, though the other, existing device will do that using the // verification URL. - let provider_metadata = - DeviceProviderMetadata::discover_async(issuer_url, &http_client).await?; - let device_authorization_endpoint = - provider_metadata.additional_metadata().device_authorization_endpoint.clone(); + let device_authorization_endpoint = server_metadata + .device_authorization_endpoint + .clone() + .map(DeviceAuthorizationUrl::from_url) + .ok_or(DeviceAuhorizationOidcError::NoDeviceAuthorizationEndpoint)?; - let oidc_client = - CoreClient::from_provider_metadata(provider_metadata, client_id.clone(), client_secret) - .set_device_authorization_url(device_authorization_endpoint) - .set_auth_type(AuthType::RequestBody); + let oauth2_client = BasicClient::new(client_id) + .set_token_uri(token_endpoint) + .set_device_authorization_url(device_authorization_endpoint); - Ok(OidcClient { inner: oidc_client, http_client }) + Ok(OidcClient { inner: oauth2_client, http_client }) } pub(super) async fn request_device_authorization( &self, device_id: Curve25519PublicKey, - ) -> Result { + ) -> Result { let scopes = [ - ScopeToken::Openid, ScopeToken::MatrixApi(MatrixApiScopeToken::Full), ScopeToken::try_with_matrix_device(device_id.to_base64()).expect( "We should be able to create a scope token from a \ @@ -135,7 +86,7 @@ impl OidcClient { .into_iter() .map(|scope| Scope::new(scope.to_string())); - let details: CoreDeviceAuthorizationResponse = self + let details: StandardDeviceAuthorizationResponse = self .inner .exchange_device_code() .add_scopes(scopes) @@ -147,11 +98,11 @@ impl OidcClient { pub(super) async fn wait_for_tokens( &self, - details: &CoreDeviceAuthorizationResponse, + details: &StandardDeviceAuthorizationResponse, ) -> Result { let response = self .inner - .exchange_device_access_token(details)? + .exchange_device_access_token(details) .request_async(&self.http_client, tokio::time::sleep, None) .await?; @@ -165,17 +116,11 @@ impl OidcClient { } } -impl<'c> openidconnect::AsyncHttpClient<'c> for HttpClient { +impl<'c> oauth2::AsyncHttpClient<'c> for HttpClient { type Error = HttpClientError; - type Future = Pin< - Box< - dyn Future> - + Send - + Sync - + 'c, - >, - >; + type Future = + Pin> + Send + Sync + 'c>>; fn call(&'c self, request: HttpRequest) -> Self::Future { Box::pin(async move {