Skip to main content

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

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