mas_handlers/oauth2/
discovery.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use axum::{Json, extract::State, response::IntoResponse};
8use mas_iana::oauth::{
9    OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod,
10    PkceCodeChallengeMethod,
11};
12use mas_jose::jwa::SUPPORTED_SIGNING_ALGORITHMS;
13use mas_keystore::Keystore;
14use mas_router::UrlBuilder;
15use oauth2_types::{
16    oidc::{ClaimType, ProviderMetadata, SubjectType},
17    requests::{Display, GrantType, Prompt, ResponseMode},
18    scope,
19};
20use serde::Serialize;
21
22use crate::SiteConfig;
23
24#[derive(Debug, Serialize)]
25struct DiscoveryResponse {
26    #[serde(flatten)]
27    standard: ProviderMetadata,
28
29    #[serde(rename = "org.matrix.matrix-authentication-service.graphql_endpoint")]
30    graphql_endpoint: url::Url,
31
32    // As per MSC2965
33    account_management_uri: url::Url,
34    account_management_actions_supported: Vec<String>,
35}
36
37#[tracing::instrument(name = "handlers.oauth2.discovery.get", skip_all)]
38#[allow(clippy::too_many_lines)]
39pub(crate) async fn get(
40    State(key_store): State<Keystore>,
41    State(url_builder): State<UrlBuilder>,
42    State(site_config): State<SiteConfig>,
43) -> impl IntoResponse {
44    // This is how clients can authenticate
45    let client_auth_methods_supported = Some(vec![
46        OAuthClientAuthenticationMethod::ClientSecretBasic,
47        OAuthClientAuthenticationMethod::ClientSecretPost,
48        OAuthClientAuthenticationMethod::ClientSecretJwt,
49        OAuthClientAuthenticationMethod::PrivateKeyJwt,
50        OAuthClientAuthenticationMethod::None,
51    ]);
52
53    // Those are the algorithms supported by `mas-jose`
54    let client_auth_signing_alg_values_supported = Some(SUPPORTED_SIGNING_ALGORITHMS.to_vec());
55
56    // This is how we can sign stuff
57    let jwt_signing_alg_values_supported = Some(key_store.available_signing_algorithms());
58
59    // Prepare all the endpoints
60    let issuer = Some(url_builder.oidc_issuer().into());
61    let authorization_endpoint = Some(url_builder.oauth_authorization_endpoint());
62    let token_endpoint = Some(url_builder.oauth_token_endpoint());
63    let device_authorization_endpoint = Some(url_builder.oauth_device_authorization_endpoint());
64    let jwks_uri = Some(url_builder.jwks_uri());
65    let introspection_endpoint = Some(url_builder.oauth_introspection_endpoint());
66    let revocation_endpoint = Some(url_builder.oauth_revocation_endpoint());
67    let userinfo_endpoint = Some(url_builder.oidc_userinfo_endpoint());
68    let registration_endpoint = Some(url_builder.oauth_registration_endpoint());
69
70    let scopes_supported = Some(vec![scope::OPENID.to_string(), scope::EMAIL.to_string()]);
71
72    let response_types_supported = Some(vec![
73        OAuthAuthorizationEndpointResponseType::Code.into(),
74        OAuthAuthorizationEndpointResponseType::IdToken.into(),
75        OAuthAuthorizationEndpointResponseType::CodeIdToken.into(),
76    ]);
77
78    let response_modes_supported = Some(vec![
79        ResponseMode::FormPost,
80        ResponseMode::Query,
81        ResponseMode::Fragment,
82    ]);
83
84    let grant_types_supported = Some(vec![
85        GrantType::AuthorizationCode,
86        GrantType::RefreshToken,
87        GrantType::ClientCredentials,
88        GrantType::DeviceCode,
89    ]);
90
91    let token_endpoint_auth_methods_supported = client_auth_methods_supported.clone();
92    let token_endpoint_auth_signing_alg_values_supported =
93        client_auth_signing_alg_values_supported.clone();
94
95    let revocation_endpoint_auth_methods_supported = client_auth_methods_supported.clone();
96    let revocation_endpoint_auth_signing_alg_values_supported =
97        client_auth_signing_alg_values_supported.clone();
98
99    let introspection_endpoint_auth_methods_supported =
100        client_auth_methods_supported.map(|v| v.into_iter().map(Into::into).collect());
101    let introspection_endpoint_auth_signing_alg_values_supported =
102        client_auth_signing_alg_values_supported;
103
104    let code_challenge_methods_supported = Some(vec![
105        PkceCodeChallengeMethod::Plain,
106        PkceCodeChallengeMethod::S256,
107    ]);
108
109    let subject_types_supported = Some(vec![SubjectType::Public]);
110
111    let id_token_signing_alg_values_supported = jwt_signing_alg_values_supported.clone();
112    let userinfo_signing_alg_values_supported = jwt_signing_alg_values_supported;
113
114    let display_values_supported = Some(vec![Display::Page]);
115
116    let claim_types_supported = Some(vec![ClaimType::Normal]);
117
118    let claims_supported = Some(vec![
119        "iss".to_owned(),
120        "sub".to_owned(),
121        "aud".to_owned(),
122        "iat".to_owned(),
123        "exp".to_owned(),
124        "nonce".to_owned(),
125        "auth_time".to_owned(),
126        "at_hash".to_owned(),
127        "c_hash".to_owned(),
128    ]);
129
130    let claims_parameter_supported = Some(false);
131    let request_parameter_supported = Some(false);
132    let request_uri_parameter_supported = Some(false);
133
134    let prompt_values_supported = Some({
135        let mut v = vec![Prompt::None, Prompt::Login];
136        // Advertise for prompt=create if password registration is enabled
137        // TODO: we may want to be able to forward that to upstream providers if they
138        // support it
139        if site_config.password_registration_enabled {
140            v.push(Prompt::Create);
141        }
142        v
143    });
144
145    let standard = ProviderMetadata {
146        issuer,
147        authorization_endpoint,
148        token_endpoint,
149        jwks_uri,
150        registration_endpoint,
151        scopes_supported,
152        response_types_supported,
153        response_modes_supported,
154        grant_types_supported,
155        token_endpoint_auth_methods_supported,
156        token_endpoint_auth_signing_alg_values_supported,
157        revocation_endpoint,
158        revocation_endpoint_auth_methods_supported,
159        revocation_endpoint_auth_signing_alg_values_supported,
160        introspection_endpoint,
161        introspection_endpoint_auth_methods_supported,
162        introspection_endpoint_auth_signing_alg_values_supported,
163        code_challenge_methods_supported,
164        userinfo_endpoint,
165        subject_types_supported,
166        id_token_signing_alg_values_supported,
167        userinfo_signing_alg_values_supported,
168        display_values_supported,
169        claim_types_supported,
170        claims_supported,
171        claims_parameter_supported,
172        request_parameter_supported,
173        request_uri_parameter_supported,
174        prompt_values_supported,
175        device_authorization_endpoint,
176        ..ProviderMetadata::default()
177    };
178
179    Json(DiscoveryResponse {
180        standard,
181        graphql_endpoint: url_builder.graphql_endpoint(),
182        account_management_uri: url_builder.account_management_uri(),
183        // This needs to be kept in sync with what is supported in the frontend,
184        // see frontend/src/routes/__root.tsx
185        account_management_actions_supported: vec![
186            "org.matrix.profile".to_owned(),
187            "org.matrix.sessions_list".to_owned(),
188            "org.matrix.session_view".to_owned(),
189            "org.matrix.session_end".to_owned(),
190            "org.matrix.cross_signing_reset".to_owned(),
191        ],
192    })
193}
194
195#[cfg(test)]
196mod tests {
197    use hyper::{Request, StatusCode};
198    use oauth2_types::oidc::ProviderMetadata;
199    use sqlx::PgPool;
200
201    use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
202
203    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
204    async fn test_valid_discovery_metadata(pool: PgPool) {
205        setup();
206        let state = TestState::from_pool(pool).await.unwrap();
207
208        let request = Request::get("/.well-known/openid-configuration").empty();
209        let response = state.request(request).await;
210        response.assert_status(StatusCode::OK);
211
212        let metadata: ProviderMetadata = response.json();
213        metadata
214            .validate(state.url_builder.oidc_issuer().as_str())
215            .expect("Invalid metadata");
216    }
217}