haste_server/auth_n/oidc/routes/
discovery.rs1use crate::{
2 auth_n::oidc::{
3 error::{OIDCError, OIDCErrorCode},
4 routes::{authorize, jwks, token},
5 },
6 extract::path_tenant::{ProjectIdentifier, TenantIdentifier},
7 route_path::{api_v1_oidc_auth_path, api_v1_oidc_path, project_path},
8 services::AppState,
9};
10use axum::{
11 extract::{FromRequestParts, Json, Path, State},
12 http::request::Parts,
13 response::{IntoResponse, Response},
14};
15use axum_extra::extract::Cached;
16use haste_fhir_search::SearchEngine;
17use haste_fhir_terminology::FHIRTerminology;
18use haste_jwt::{ProjectId, TenantId, scopes::Scopes};
19use haste_repository::Repository;
20use serde::{Deserialize, Serialize};
21use std::sync::Arc;
22use url::Url;
23
24#[derive(Serialize, Deserialize, Debug, Clone)]
25pub struct WellKnownDiscoveryDocument {
26 pub issuer: String,
27 pub authorization_endpoint: String,
28 pub jwks_uri: String,
29 pub token_endpoint: String,
30 pub scopes_supported: Vec<String>,
31 pub response_types_supported: Vec<String>,
32 pub token_endpoint_auth_methods_supported: Vec<String>,
33 pub id_token_signing_alg_values_supported: Vec<String>,
34 pub subject_types_supported: Vec<String>,
35}
36
37#[derive(Serialize, Deserialize, Debug, Clone)]
38pub struct OAuthProtectedResourceDocument {
39 resource: String,
44
45 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
56 authorization_servers: Option<Vec<String>>,
57
58 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
69 jwks_uri: Option<String>,
70
71 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
79 scopes_supported: Option<Vec<String>>,
80
81 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
92 bearer_methods_supported: Option<Vec<String>>,
93
94 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
102 resource_signing_alg_values_supported: Option<Vec<String>>,
103
104 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
111 resource_name: Option<String>,
112
113 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
120 resource_documentation: Option<String>,
121
122 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
129 resource_policy_uri: Option<String>,
130
131 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
137 resource_tos_uri: Option<String>,
138
139 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
145 tls_client_certificate_bound_access_tokens: Option<bool>,
146
147 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
153 authorization_details_types_supported: Option<Vec<String>>,
154
155 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
162 dpop_signing_alg_values_supported: Option<Vec<String>>,
163
164 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
170 dpop_bound_access_tokens_required: Option<bool>,
171}
172
173#[derive(Deserialize, Clone)]
174pub struct ResourcePath {
175 pub resource: String,
176}
177
178impl<S: Send + Sync> FromRequestParts<S> for ResourcePath {
179 type Rejection = Response;
180
181 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
182 let Path(resource) = Path::<ResourcePath>::from_request_parts(parts, state)
183 .await
184 .map_err(|err| err.into_response())?;
185
186 Ok(resource)
187 }
188}
189
190pub async fn oauth_protected_resource<
191 Repo: Repository + Send + Sync,
192 Search: SearchEngine + Send + Sync,
193 Terminology: FHIRTerminology + Send + Sync,
194>(
195 Cached(ResourcePath { resource }): Cached<ResourcePath>,
196 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
197 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
198 State(state): State<Arc<AppState<Repo, Search, Terminology>>>,
199) -> Result<Json<OAuthProtectedResourceDocument>, OIDCError> {
200 let api_url_string = state
201 .config
202 .get(crate::ServerEnvironmentVariables::APIURI)
203 .unwrap_or_default();
204
205 if api_url_string.is_empty() {
206 return Err(OIDCError::new(
207 OIDCErrorCode::ServerError,
208 Some("API_URL is not set in the configuration".to_string()),
209 None,
210 ));
211 }
212
213 let Ok(api_url) = Url::parse(&api_url_string) else {
214 return Err(OIDCError::new(
215 OIDCErrorCode::ServerError,
216 Some("Invalid API_URL format".to_string()),
217 None,
218 ));
219 };
220
221 let default_scopes =
223 Scopes::try_from("openid profile user/*.* offline_access").unwrap_or_default();
224
225 let oauth_protected_resource = OAuthProtectedResourceDocument {
226 resource: api_url
227 .join(
228 &project_path(&tenant, &project)
229 .join(&resource)
230 .to_str()
231 .unwrap_or_default()
232 .to_string(),
233 )
234 .unwrap()
235 .to_string(),
236 authorization_servers: Some(vec![
237 api_url
238 .join(project_path(&tenant, &project).to_str().unwrap())
239 .unwrap()
240 .to_string(),
241 ]),
242 jwks_uri: None,
243
244 scopes_supported: Some(
245 default_scopes
246 .0
247 .into_iter()
248 .map(|s| String::from(s))
249 .collect::<Vec<_>>(),
250 ),
251 bearer_methods_supported: None,
252 resource_signing_alg_values_supported: None,
253 resource_name: None,
254 resource_documentation: None,
255 resource_policy_uri: None,
256 resource_tos_uri: None,
257 tls_client_certificate_bound_access_tokens: None,
258 authorization_details_types_supported: None,
259 dpop_signing_alg_values_supported: None,
260 dpop_bound_access_tokens_required: None,
261 };
262
263 Ok(Json(oauth_protected_resource))
264}
265
266pub fn create_oidc_discovery_document(
267 tenant: &TenantId,
268 project: &ProjectId,
269 api_url_string: &str,
270) -> Result<WellKnownDiscoveryDocument, OIDCError> {
271 if api_url_string.is_empty() {
272 return Err(OIDCError::new(
273 OIDCErrorCode::ServerError,
274 Some("API_URL is not set in the configuration".to_string()),
275 None,
276 ));
277 }
278
279 let Ok(api_url) = Url::parse(&api_url_string) else {
280 return Err(OIDCError::new(
281 OIDCErrorCode::ServerError,
282 Some("Invalid API_URL format".to_string()),
283 None,
284 ));
285 };
286
287 let authorize_path = api_v1_oidc_auth_path(tenant, project).join(
288 &authorize::AuthorizePath
289 .to_string()
290 .strip_prefix("/")
291 .unwrap(),
292 );
293
294 let token_path = api_v1_oidc_auth_path(tenant, project)
295 .join(&token::TokenPath.to_string().strip_prefix("/").unwrap());
296
297 let jwks_path = api_v1_oidc_path(tenant, project)
298 .join(&jwks::JWKSPath.to_string().strip_prefix("/").unwrap());
299
300 let oidc_response = WellKnownDiscoveryDocument {
301 issuer: api_url.to_string(),
302 authorization_endpoint: api_url
303 .join(authorize_path.to_str().unwrap_or_default())
304 .unwrap()
305 .to_string(),
306 token_endpoint: api_url
307 .join(token_path.to_str().unwrap_or_default())
308 .unwrap()
309 .to_string(),
310 jwks_uri: api_url
311 .join(jwks_path.to_str().unwrap_or_default())
312 .unwrap()
313 .to_string(),
314 scopes_supported: vec![
315 "openid".to_string(),
316 "profile".to_string(),
317 "email".to_string(),
318 "offline_access".to_string(),
319 ],
320 response_types_supported: vec![
321 "code".to_string(),
322 "id_token".to_string(),
323 "id_token token".to_string(),
324 ],
325 token_endpoint_auth_methods_supported: vec![
326 "client_secret_basic".to_string(),
327 "client_secret_post".to_string(),
328 ],
329 id_token_signing_alg_values_supported: vec!["RS256".to_string()],
330 subject_types_supported: vec!["public".to_string()],
331 };
332
333 Ok(oidc_response)
334}
335
336pub async fn openid_configuration<
337 Repo: Repository + Send + Sync,
338 Search: SearchEngine + Send + Sync,
339 Terminology: FHIRTerminology + Send + Sync,
340>(
341 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
342 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
343 State(state): State<Arc<AppState<Repo, Search, Terminology>>>,
344) -> Result<Json<WellKnownDiscoveryDocument>, OIDCError> {
345 let api_url_string = state
346 .config
347 .get(crate::ServerEnvironmentVariables::APIURI)
348 .unwrap_or_default();
349
350 Ok(Json(create_oidc_discovery_document(
351 &tenant,
352 &project,
353 &api_url_string,
354 )?))
355}