haste_server/auth_n/oidc/routes/
scope.rs1use crate::{
2 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(¤t_session)
81 .await
82 .map_err(|_| {
83 OIDCError::new(
84 OIDCErrorCode::ServerError,
85 Some("Failed to retrieve user from session.".to_string()),
86 Some(scope_data.redirect_uri.clone()),
87 )
88 })?
89 .unwrap();
90
91 if let Some("on") = scope_data.accept.as_ref().map(String::as_str) {
92 verify_requested_scope_is_subset(
93 &scope_data.scope,
94 &Scopes::from(
95 client_app
96 .scope
97 .as_ref()
98 .and_then(|s| s.value.clone())
99 .unwrap_or_default(),
100 ),
101 )?;
102
103 ProjectAuthAdmin::create(
104 &*app_state.repo,
105 &tenant,
106 &project,
107 CreateScope {
108 client: ClientId::new(scope_data.client_id.clone()),
109 user_: UserId::new(user.id),
110 scope: scope_data.scope.clone(),
111 },
112 )
113 .await
114 .map_err(|_| {
115 OIDCError::new(
116 OIDCErrorCode::ServerError,
117 Some("Failed to create scope authorization.".to_string()),
118 Some(scope_data.redirect_uri.clone()),
119 )
120 })?;
121
122 let authorization_route = oidc_route_string(&tenant, &project, "auth/authorize")
123 .to_str()
124 .expect("Could not create authorize route.")
125 .to_string()
126 + "?client_id="
127 + scope_data.client_id.as_str()
128 + "&response_type="
129 + scope_data.response_type.as_str()
130 + "&state="
131 + scope_data.state.as_str()
132 + "&code_challenge="
133 + scope_data.code_challenge.as_str()
134 + "&code_challenge_method="
135 + scope_data.code_challenge_method.as_str()
136 + "&scope="
137 + String::from(scope_data.scope).as_str()
138 + "&redirect_uri="
139 + scope_data.redirect_uri.as_str();
140 let redirect = axum::response::Redirect::to(&authorization_route);
141 Ok(redirect.into_response())
142 } else {
143 Err(OIDCError::new(
144 OIDCErrorCode::AccessDenied,
145 Some("User did not accept the requested scopes.".to_string()),
146 Some(scope_data.redirect_uri),
147 ))
148 }
149}