matrix_sdk/authentication/qrcode/
oauth_client.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::pin::Pin;
16
17use futures_core::Future;
18use mas_oidc_client::types::{
19    oidc::VerifiedProviderMetadata,
20    scope::{MatrixApiScopeToken, ScopeToken},
21};
22use oauth2::{
23    basic::BasicClient, ClientId, DeviceAuthorizationUrl, EndpointNotSet, EndpointSet,
24    HttpClientError, HttpRequest, Scope, StandardDeviceAuthorizationResponse, TokenResponse,
25    TokenUrl,
26};
27use vodozemac::Curve25519PublicKey;
28
29use super::DeviceAuthorizationOauthError;
30use crate::{authentication::oidc::OidcSessionTokens, http_client::HttpClient};
31
32/// An OAuth 2.0 specific HTTP client.
33///
34/// This is used to communicate with the OAuth 2.0 authorization server
35/// exclusively.
36pub(super) struct OauthClient {
37    /// Oauth 2.0 Basic client.
38    inner: BasicClient<EndpointNotSet, EndpointSet, EndpointNotSet, EndpointNotSet, EndpointSet>,
39    http_client: HttpClient,
40}
41
42impl OauthClient {
43    pub(super) fn new(
44        client_id: String,
45        server_metadata: &VerifiedProviderMetadata,
46        http_client: HttpClient,
47    ) -> Result<Self, DeviceAuthorizationOauthError> {
48        let client_id = ClientId::new(client_id);
49
50        let token_endpoint = TokenUrl::from_url(server_metadata.token_endpoint().clone());
51
52        // We can use the device authorization endpoint to attempt to log in this new
53        // device, though the other, existing device will do that using the
54        // verification URL.
55        let device_authorization_endpoint = server_metadata
56            .device_authorization_endpoint
57            .clone()
58            .map(DeviceAuthorizationUrl::from_url)
59            .ok_or(DeviceAuthorizationOauthError::NoDeviceAuthorizationEndpoint)?;
60
61        let oauth2_client = BasicClient::new(client_id)
62            .set_token_uri(token_endpoint)
63            .set_device_authorization_url(device_authorization_endpoint);
64
65        Ok(Self { inner: oauth2_client, http_client })
66    }
67
68    pub(super) async fn request_device_authorization(
69        &self,
70        device_id: Curve25519PublicKey,
71    ) -> Result<StandardDeviceAuthorizationResponse, DeviceAuthorizationOauthError> {
72        let scopes = [
73            ScopeToken::MatrixApi(MatrixApiScopeToken::Full),
74            ScopeToken::try_with_matrix_device(device_id.to_base64()).expect(
75                "We should be able to create a scope token from a \
76                 Curve25519 public key encoded as base64",
77            ),
78        ]
79        .into_iter()
80        .map(|scope| Scope::new(scope.to_string()));
81
82        let details: StandardDeviceAuthorizationResponse = self
83            .inner
84            .exchange_device_code()
85            .add_scopes(scopes)
86            .request_async(&self.http_client)
87            .await?;
88
89        Ok(details)
90    }
91
92    pub(super) async fn wait_for_tokens(
93        &self,
94        details: &StandardDeviceAuthorizationResponse,
95    ) -> Result<OidcSessionTokens, DeviceAuthorizationOauthError> {
96        let response = self
97            .inner
98            .exchange_device_access_token(details)
99            .request_async(&self.http_client, tokio::time::sleep, None)
100            .await?;
101
102        let tokens = OidcSessionTokens {
103            access_token: response.access_token().secret().to_owned(),
104            refresh_token: response.refresh_token().map(|t| t.secret().to_owned()),
105            latest_id_token: None,
106        };
107
108        Ok(tokens)
109    }
110}
111
112impl<'c> oauth2::AsyncHttpClient<'c> for HttpClient {
113    type Error = HttpClientError<reqwest::Error>;
114
115    type Future =
116        Pin<Box<dyn Future<Output = Result<oauth2::HttpResponse, Self::Error>> + Send + Sync + 'c>>;
117
118    fn call(&'c self, request: HttpRequest) -> Self::Future {
119        Box::pin(async move {
120            let response = self.inner.call(request).await?;
121
122            Ok(response)
123        })
124    }
125}