1use 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 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 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 let client_auth_signing_alg_values_supported = Some(SUPPORTED_SIGNING_ALGORITHMS.to_vec());
55
56 let jwt_signing_alg_values_supported = Some(key_store.available_signing_algorithms());
58
59 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 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 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}