Skip to main content

haste_server/auth_n/oidc/routes/
scope.rs

1use crate::{
2    auth_n::{
3        oidc::{
4            error::{OIDCError, OIDCErrorCode},
5            extract::client_app::OIDCClientApplication,
6            routes::route_string::oidc_route_string,
7        },
8        session,
9    },
10    extract::{
11        csrf_token::CSRFToken,
12        path_tenant::{ProjectIdentifier, TenantIdentifier},
13    },
14    services::ServerState,
15};
16use axum::{
17    Form,
18    extract::{OriginalUri, State},
19    response::{IntoResponse, Response},
20};
21use axum_extra::{extract::Cached, routing::TypedPath};
22use haste_fhir_search::SearchEngine;
23use haste_fhir_terminology::FHIRTerminology;
24use haste_jwt::scopes::Scopes;
25use haste_repository::{
26    Repository,
27    admin::ProjectModelAdmin,
28    types::scope::{ClientId, CreateScope, UserId},
29};
30use serde::Deserialize;
31use std::sync::Arc;
32use tower_sessions::Session;
33
34#[derive(TypedPath)]
35#[typed_path("/scope")]
36pub struct ScopePost;
37
38#[derive(Deserialize, Debug)]
39pub struct ScopeForm {
40    pub csrf_token: String,
41    pub client_id: String,
42    pub response_type: String,
43    pub state: String,
44    pub code_challenge: String,
45    pub code_challenge_method: String,
46    pub scope: haste_jwt::scopes::Scopes,
47    pub redirect_uri: String,
48    pub accept: Option<String>,
49}
50
51pub fn verify_requested_scope_is_subset(
52    requested: &Scopes,
53    allowed: &Scopes,
54) -> Result<(), OIDCError> {
55    for scope in requested.0.iter() {
56        if !allowed.0.contains(scope) {
57            return Err(OIDCError::new(
58                OIDCErrorCode::InvalidScope,
59                Some(format!(
60                    "Requested scope '{}' is not allowed. Check client configuration for what scopes are allowed.",
61                    String::from(scope.clone())
62                )),
63                None,
64            ));
65        }
66    }
67    Ok(())
68}
69
70pub async fn scope_post<
71    Repo: Repository + Send + Sync,
72    Search: SearchEngine + Send + Sync,
73    Terminology: FHIRTerminology + Send + Sync,
74>(
75    _: ScopePost,
76    _uri: OriginalUri,
77    CSRFToken(csrf_token): CSRFToken,
78    State(app_state): State<Arc<ServerState<Repo, Search, Terminology>>>,
79    Cached(current_session): Cached<Session>,
80    OIDCClientApplication(client_app): OIDCClientApplication,
81    Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
82    Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
83    Form(scope_data): Form<ScopeForm>,
84) -> Result<Response, OIDCError> {
85    if csrf_token != scope_data.csrf_token {
86        return Err(OIDCError::new(
87            OIDCErrorCode::InvalidRequest,
88            Some("Invalid CSRF Token.".to_string()),
89            Some(scope_data.redirect_uri.clone()),
90        ));
91    }
92
93    let completed_auth_state = session::user::get_completed_authorization_state(&current_session)
94        .await
95        .map_err(|_| {
96            OIDCError::new(
97                OIDCErrorCode::ServerError,
98                Some("Failed to retrieve user from session.".to_string()),
99                Some(scope_data.redirect_uri.clone()),
100            )
101        })?;
102
103    if let Some("on") = scope_data.accept.as_deref() {
104        verify_requested_scope_is_subset(
105            &scope_data.scope,
106            &Scopes::from(
107                client_app
108                    .scope
109                    .as_ref()
110                    .and_then(|s| s.value.clone())
111                    .unwrap_or_default(),
112            ),
113        )?;
114
115        ProjectModelAdmin::create(
116            &*app_state.repo,
117            &tenant,
118            &project,
119            CreateScope {
120                client: ClientId::new(scope_data.client_id.clone()),
121                user_: UserId::new(completed_auth_state.user.id),
122                scope: scope_data.scope.clone(),
123            },
124        )
125        .await
126        .map_err(|_| {
127            OIDCError::new(
128                OIDCErrorCode::ServerError,
129                Some("Failed to create scope authorization.".to_string()),
130                Some(scope_data.redirect_uri.clone()),
131            )
132        })?;
133
134        let authorization_route = oidc_route_string(&tenant, &project, "auth/authorize")
135            .to_str()
136            .expect("Could not create authorize route.")
137            .to_string()
138            + "?client_id="
139            + scope_data.client_id.as_str()
140            + "&response_type="
141            + scope_data.response_type.as_str()
142            + "&state="
143            + scope_data.state.as_str()
144            + "&code_challenge="
145            + scope_data.code_challenge.as_str()
146            + "&code_challenge_method="
147            + scope_data.code_challenge_method.as_str()
148            + "&scope="
149            + String::from(scope_data.scope).as_str()
150            + "&redirect_uri="
151            + scope_data.redirect_uri.as_str();
152        let redirect = axum::response::Redirect::to(&authorization_route);
153        Ok(redirect.into_response())
154    } else {
155        Err(OIDCError::new(
156            OIDCErrorCode::AccessDenied,
157            Some("User did not accept the requested scopes.".to_string()),
158            Some(scope_data.redirect_uri),
159        ))
160    }
161}