Skip to main content

haste_server/auth_n/oidc/routes/federated/
initiate.rs

1use 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}