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