refactor(auth-qrcode): Use oauth2 crate instead of openidconnect

The MSCs are now only based on OAuth 2.0, which is simpler than OpenID Connect.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
This commit is contained in:
Kévin Commaille
2025-02-02 18:42:54 +01:00
parent 57919f5480
commit d7dc1c9b5b
6 changed files with 68 additions and 200 deletions

61
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"] }

View File

@@ -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<OidcClient, DeviceAuhorizationOidcError> {
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;
}

View File

@@ -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(),

View File

@@ -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<reqwest::Error>,
StandardErrorResponse<CoreErrorResponseType>,
>,
),
DeviceAuthorization(#[from] BasicRequestTokenError<HttpClientError<reqwest::Error>>),
/// 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<reqwest::Error>,
StandardErrorResponse<DeviceCodeErrorResponseType>,
>,
#[from] RequestTokenError<HttpClientError<reqwest::Error>, DeviceCodeErrorResponse>,
),
/// An error happened during the discovery of the OIDC provider metadata.
#[error(transparent)]
Discovery(#[from] DiscoveryError<HttpClientError<reqwest::Error>>),
}
impl DeviceAuhorizationOidcError {

View File

@@ -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<CoreErrorResponseType>,
CoreTokenResponse,
CoreTokenIntrospectionResponse,
CoreRevocableToken,
CoreRevocationErrorResponse,
HasAuthUrl,
HasDeviceAuthUrl,
HasIntrospectionUrl,
HasRevocationUrl,
HasTokenUrl,
HasUserInfoUrl,
>;
HasTokenUrl = EndpointSet,
> = BasicClient<HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl>;
/// 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<Self, DeviceAuhorizationOidcError> {
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<CoreDeviceAuthorizationResponse, DeviceAuhorizationOidcError> {
) -> Result<StandardDeviceAuthorizationResponse, DeviceAuhorizationOidcError> {
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<OidcSessionTokens, DeviceAuhorizationOidcError> {
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<reqwest::Error>;
type Future = Pin<
Box<
dyn Future<Output = Result<openidconnect::HttpResponse, Self::Error>>
+ Send
+ Sync
+ 'c,
>,
>;
type Future =
Pin<Box<dyn Future<Output = Result<oauth2::HttpResponse, Self::Error>> + Send + Sync + 'c>>;
fn call(&'c self, request: HttpRequest) -> Self::Future {
Box::pin(async move {