haste_server/auth_n/oidc/routes/federated/
initiate.rs1use crate::{
2 ServerEnvironmentVariables,
3 auth_n::oidc::{
4 code_verification::{generate_code_challenge, generate_code_verifier},
5 extract::client_app::OIDCClientApplication,
6 routes::{
7 authorize::redirect_authorize_uri, federated::callback::create_federated_callback_url,
8 },
9 },
10 extract::path_tenant::{Project, ProjectIdentifier, TenantIdentifier},
11 fhir_client::{FHIRServerClient, ServerCTX},
12 services::AppState,
13};
14use axum::{
15 extract::{OriginalUri, State},
16 response::Redirect,
17};
18use axum_extra::{extract::Cached, routing::TypedPath};
19use haste_fhir_client::FHIRClient;
20use haste_fhir_model::r4::generated::{
21 resources::{IdentityProvider, Project as FHIRProject, Resource, ResourceType},
22 terminology::{IdentityProviderPkceChallengeMethod, IssueType},
23};
24use haste_fhir_operation_error::OperationOutcomeError;
25use haste_fhir_search::SearchEngine;
26use haste_fhir_terminology::FHIRTerminology;
27use haste_jwt::{ProjectId, TenantId};
28use haste_rate_limit::RateLimit;
29use haste_repository::{
30 Repository, types::authorization_code::PKCECodeChallengeMethod, utilities::generate_id,
31};
32use serde::{Deserialize, Serialize};
33use std::sync::Arc;
34use tower_sessions::Session;
35use url::Url;
36
37#[derive(TypedPath, Deserialize)]
38#[typed_path("/federated/{identity_provider_id}/initiate")]
39pub struct FederatedInitiate {
40 pub identity_provider_id: String,
41}
42
43pub fn validate_identity_provider_in_project(
44 identity_provider_id: &str,
45 project: &FHIRProject,
46) -> Result<(), OperationOutcomeError> {
47 if let Some(identity_providers) = &project.identityProvider {
48 for ip_ref in identity_providers {
49 if let Some(ref_id) = &ip_ref.reference.as_ref().and_then(|r| r.value.as_ref()) {
50 if ref_id.as_str() == &format!("IdentityProvider/{}", identity_provider_id) {
51 return Ok(());
52 }
53 }
54 }
55 }
56 Err(OperationOutcomeError::error(
57 IssueType::Forbidden(None),
58 "The specified identity provider is not associated with the project.".to_string(),
59 ))
60}
61
62pub async fn get_idp<
63 Repo: Repository + Send + Sync,
64 Search: SearchEngine + Send + Sync,
65 Terminology: FHIRTerminology + Send + Sync,
66>(
67 tenant: &TenantId,
68 fhir_client: Arc<FHIRServerClient<Repo, Search, Terminology>>,
69 rate_limit: Arc<dyn RateLimit>,
70 identity_provider_id: String,
71) -> Result<IdentityProvider, OperationOutcomeError> {
72 let identity_provider = fhir_client
73 .read(
74 Arc::new(ServerCTX::system(
75 tenant.clone(),
76 ProjectId::System,
77 fhir_client.clone(),
78 rate_limit.clone(),
79 )),
80 ResourceType::IdentityProvider,
81 identity_provider_id,
82 )
83 .await?
84 .and_then(|r| match r {
85 Resource::IdentityProvider(ip) => Some(ip),
86 _ => None,
87 })
88 .ok_or_else(|| {
89 OperationOutcomeError::error(
90 IssueType::NotFound(None),
91 "The specified identity provider was not found.".to_string(),
92 )
93 })?;
94
95 Ok(identity_provider)
96}
97
98#[derive(Deserialize, Serialize, Clone)]
99pub struct IDPSessionInfo {
100 pub state: String,
101 pub redirect_to: String,
102 pub project: ProjectId,
103 pub code_verifier: Option<String>,
104}
105
106fn federated_session_info_key(idp_id: &str) -> String {
107 format!("federated_initiate_{}", idp_id)
108}
109
110pub async fn get_idp_session_info(
111 session: &Session,
112 idp: &IdentityProvider,
113) -> Result<IDPSessionInfo, OperationOutcomeError> {
114 let idp_id = idp.id.as_ref().ok_or_else(|| {
115 OperationOutcomeError::error(
116 IssueType::Invalid(None),
117 "Identity Provider resource is missing an ID.".to_string(),
118 )
119 })?;
120
121 let info: IDPSessionInfo = session
122 .get(federated_session_info_key(idp_id).as_str())
123 .await
124 .map_err(|_| {
125 OperationOutcomeError::error(
126 IssueType::Exception(None),
127 "Failed to retrieve session information.".to_string(),
128 )
129 })?
130 .ok_or_else(|| {
131 OperationOutcomeError::error(
132 IssueType::NotFound(None),
133 "No session information found for the specified identity provider.".to_string(),
134 )
135 })?;
136
137 Ok(info)
138}
139
140async fn set_session_info(
141 session: &mut Session,
142 project_id: ProjectId,
143 idp: &IdentityProvider,
144 uri: &OriginalUri,
145) -> Result<IDPSessionInfo, OperationOutcomeError> {
146 let idp_id = idp.id.as_ref().ok_or_else(|| {
147 OperationOutcomeError::error(
148 IssueType::Invalid(None),
149 "Identity Provider resource is missing an ID.".to_string(),
150 )
151 })?;
152
153 let state = generate_id(Some(20));
154
155 let mut info = IDPSessionInfo {
156 state,
157 redirect_to: redirect_authorize_uri(
158 uri,
159 &FederatedInitiate {
160 identity_provider_id: idp_id.clone(),
161 }
162 .to_string(),
163 ),
164 project: project_id,
165 code_verifier: None,
166 };
167
168 if let Some(oidc) = &idp.oidc {
169 if let Some(pkce) = &oidc.pkce {
170 if pkce.enabled.as_ref().and_then(|b| b.value).unwrap_or(false) {
171 let code_verifier = generate_code_verifier();
172 info.code_verifier = Some(code_verifier);
173 }
174 }
175 }
176
177 session
178 .insert(federated_session_info_key(idp_id).as_str(), &info)
179 .await
180 .map_err(|_| {
181 OperationOutcomeError::error(
182 IssueType::Exception(None),
183 "Failed to set session information.".to_string(),
184 )
185 })?;
186
187 Ok(info)
188}
189
190fn oidc_pkce_challenge_method(
191 challenge: &IdentityProviderPkceChallengeMethod,
192) -> Option<PKCECodeChallengeMethod> {
193 match challenge {
194 IdentityProviderPkceChallengeMethod::S256(None) => Some(PKCECodeChallengeMethod::S256),
195 IdentityProviderPkceChallengeMethod::Plain(None) => Some(PKCECodeChallengeMethod::Plain),
196 _ => None,
197 }
198}
199
200async fn create_federated_authorization_url(
201 session: &mut Session,
202 tenant: &TenantId,
203 project: ProjectId,
204 api_uri: &str,
205 original_uri: &OriginalUri,
206 identity_provider: &IdentityProvider,
207) -> Result<Url, OperationOutcomeError> {
208 if let Some(oidc) = &identity_provider.oidc {
209 let mut authorization_url = oidc
210 .authorization_endpoint
211 .value
212 .as_ref()
213 .and_then(|s| Url::parse(s).ok())
214 .ok_or_else(|| {
215 OperationOutcomeError::error(
216 IssueType::Invalid(None),
217 "Invalid authorization endpoint URL for identity provider".to_string(),
218 )
219 })?;
220
221 let client_id = oidc.client.clientId.value.as_ref().ok_or_else(|| {
222 OperationOutcomeError::error(
223 IssueType::Invalid(None),
224 "Missing client ID for identity provider.".to_string(),
225 )
226 })?;
227
228 let scopes = oidc.scopes.as_ref().map(|s| {
229 s.iter()
230 .filter_map(|v| v.value.as_ref())
231 .map(|s| s.as_str())
232 .collect::<Vec<_>>()
233 .join(" ")
234 });
235
236 authorization_url.set_query(Some("response_type=code"));
237 authorization_url
238 .query_pairs_mut()
239 .append_pair("client_id", client_id)
240 .append_pair("scope", &scopes.unwrap_or_default())
241 .append_pair(
242 "redirect_uri",
243 &create_federated_callback_url(
244 api_uri,
245 tenant,
246 &identity_provider.id.clone().unwrap_or_default(),
247 )?,
248 );
249
250 let info = set_session_info(session, project, &identity_provider, original_uri).await?;
251 authorization_url
252 .query_pairs_mut()
253 .append_pair("state", &info.state);
254 if let Some(code_verifier) = info.code_verifier
255 && let Some(challenge_method) = oidc
256 .pkce
257 .as_ref()
258 .and_then(|p| p.code_challenge_method.as_ref())
259 .and_then(|c| oidc_pkce_challenge_method(c))
260 {
261 let code_challenge = generate_code_challenge(&code_verifier, &challenge_method)?;
262 authorization_url
263 .query_pairs_mut()
264 .append_pair("code_challenge", &code_challenge);
265 authorization_url
266 .query_pairs_mut()
267 .append_pair("code_challenge_method", &String::from(challenge_method));
268 }
269
270 Ok(authorization_url)
271 } else {
272 return Err(OperationOutcomeError::error(
273 IssueType::NotFound(None),
274 "The specified identity provider was not found.".to_string(),
275 ));
276 }
277}
278
279pub async fn federated_initiate<
280 Repo: Repository + Send + Sync,
281 Search: SearchEngine + Send + Sync,
282 Terminology: FHIRTerminology + Send + Sync,
283>(
284 FederatedInitiate {
285 identity_provider_id,
286 }: FederatedInitiate,
287 Cached(mut current_session): Cached<Session>,
288 uri: OriginalUri,
289 State(state): State<Arc<AppState<Repo, Search, Terminology>>>,
290 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
291 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
292 Cached(Project(project_resource)): Cached<Project>,
293 OIDCClientApplication(_client_app): OIDCClientApplication,
294 _uri: OriginalUri,
295) -> Result<Redirect, OperationOutcomeError> {
296 let api_uri = state.config.get(ServerEnvironmentVariables::APIURI)?;
297 validate_identity_provider_in_project(&identity_provider_id, &project_resource)?;
298 let identity_provider = get_idp(
299 &tenant,
300 state.fhir_client.clone(),
301 state.rate_limit.clone(),
302 identity_provider_id,
303 )
304 .await?;
305
306 let federated_authorization_url = create_federated_authorization_url(
307 &mut current_session,
308 &tenant,
309 project,
310 &api_uri,
311 &uri,
312 &identity_provider,
313 )
314 .await?;
315
316 Ok(Redirect::to(federated_authorization_url.as_str()))
317}