haste_server/auth_n/oidc/routes/
scope.rs

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