haste_server/auth_n/oidc/routes/
scope.rs1use crate::auth_n::constant_time;
2use crate::{
3 auth_n::{
4 oidc::{
5 error::{OIDCError, OIDCErrorCode},
6 extract::client_app::find_client_app,
7 routes::route_string::oidc_route_string,
8 utilities::is_valid_redirect_url,
9 },
10 session,
11 },
12 extract::{
13 csrf_token::CSRFToken,
14 path_tenant::{ProjectIdentifier, TenantIdentifier},
15 },
16 services::ServerState,
17};
18use axum::{
19 extract::{OriginalUri, State},
20 response::{IntoResponse, Response},
21};
22use axum_extra::{
23 extract::{Cached, Form},
24 routing::TypedPath,
25};
26use haste_fhir_search::SearchEngine;
27use haste_fhir_terminology::FHIRTerminology;
28use haste_jwt::scopes::{OIDCScope, Scope, Scopes, SmartScope};
29use haste_repository::{
30 Repository,
31 admin::ProjectModelAdmin,
32 types::scope::{ClientId, CreateScope, UserId},
33};
34use serde::{Deserialize, Deserializer};
35use std::sync::Arc;
36use tower_sessions::Session;
37
38#[derive(TypedPath)]
39#[typed_path("/scope")]
40pub struct ScopePost;
41
42#[derive(Deserialize, Debug)]
43pub struct ScopeForm {
44 pub csrf_token: String,
45 pub client_id: String,
46 pub response_type: String,
47 pub state: String,
48 pub code_challenge: String,
49 pub code_challenge_method: String,
50 #[serde(default, deserialize_with = "scopes_from_values")]
54 pub scope: Scopes,
55 pub redirect_uri: String,
56 pub launch: Option<String>,
58 pub aud: Option<String>,
61 pub accept: Option<String>,
62}
63
64fn scopes_from_values<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Scopes, D::Error> {
67 let values = Vec::<String>::deserialize(deserializer)?;
68 Scopes::try_from(values.join(" ").as_str()).map_err(serde::de::Error::custom)
69}
70
71pub fn is_fixed_scope(scope: &Scope) -> bool {
76 matches!(
77 scope,
78 Scope::OIDC(OIDCScope::OpenId)
79 | Scope::SMART(SmartScope::LaunchSystem(_) | SmartScope::LaunchType(_))
80 )
81}
82
83pub fn verify_requested_scope_is_subset(
89 requested: &Scopes,
90 allowed: &Scopes,
91) -> Result<(), OIDCError> {
92 for scope in requested.0.iter() {
93 if !allowed.covers_scope(scope) {
94 return Err(OIDCError::new(
95 OIDCErrorCode::InvalidScope,
96 Some(format!(
97 "Requested scope '{}' is not allowed. Check client configuration for what scopes are allowed.",
98 String::from(scope.clone())
99 )),
100 None,
101 ));
102 }
103 }
104 Ok(())
105}
106
107pub async fn scope_post<
108 Repo: Repository + Send + Sync,
109 Search: SearchEngine + Send + Sync,
110 Terminology: FHIRTerminology + Send + Sync,
111>(
112 _: ScopePost,
113 _uri: OriginalUri,
114 CSRFToken(csrf_token): CSRFToken,
115 State(app_state): State<Arc<ServerState<Repo, Search, Terminology>>>,
116 Cached(current_session): Cached<Session>,
117 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
118 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
119 Form(scope_data): Form<ScopeForm>,
120) -> Result<Response, OIDCError> {
121 let client_app = find_client_app(
124 &app_state,
125 tenant.clone(),
126 project.clone(),
127 scope_data.client_id.clone(),
128 )
129 .await?;
130
131 let error_redirect_uri = is_valid_redirect_url(&scope_data.redirect_uri, &client_app)
135 .then(|| scope_data.redirect_uri.clone());
136
137 if !constant_time::str_eq(&csrf_token, &scope_data.csrf_token) {
138 return Err(OIDCError::new(
139 OIDCErrorCode::InvalidRequest,
140 Some("Invalid CSRF Token.".to_string()),
141 error_redirect_uri,
142 ));
143 }
144
145 let completed_auth_state = session::user::get_completed_authorization_state(¤t_session)
146 .await
147 .map_err(|_| {
148 OIDCError::new(
149 OIDCErrorCode::ServerError,
150 Some("Failed to retrieve user from session.".to_string()),
151 error_redirect_uri.clone(),
152 )
153 })?;
154
155 if let Some("on") = scope_data.accept.as_deref()
159 && !scope_data.scope.0.is_empty()
160 {
161 let client_scope = client_app
162 .scope
163 .as_ref()
164 .and_then(|s| s.value.clone())
165 .unwrap_or_default();
166
167 let allowed = Scopes::try_from(client_scope.as_str()).map_err(|_| {
168 OIDCError::new(
169 OIDCErrorCode::InvalidScope,
170 Some("Client application has an invalid scope configured.".to_string()),
171 error_redirect_uri.clone(),
172 )
173 })?;
174 verify_requested_scope_is_subset(&scope_data.scope, &allowed)?;
175
176 let client_id = client_app.id.clone().ok_or_else(|| {
177 OIDCError::new(
178 OIDCErrorCode::ServerError,
179 Some("Failed to retrieve client ID.".to_string()),
180 None,
181 )
182 })?;
183
184 ProjectModelAdmin::create(
185 &*app_state.repo,
186 &tenant,
187 &project,
188 CreateScope {
189 client: ClientId::new(client_id.clone()),
190 user_: UserId::new(completed_auth_state.user.id),
191 scope: scope_data.scope.clone(),
192 },
193 )
194 .await
195 .map_err(|_| {
196 OIDCError::new(
197 OIDCErrorCode::ServerError,
198 Some("Failed to create scope authorization.".to_string()),
199 error_redirect_uri.clone(),
200 )
201 })?;
202
203 let mut query = url::form_urlencoded::Serializer::new(String::new());
207 query
208 .append_pair("client_id", client_id.as_str())
209 .append_pair("response_type", scope_data.response_type.as_str())
210 .append_pair("state", scope_data.state.as_str())
211 .append_pair("code_challenge", scope_data.code_challenge.as_str())
212 .append_pair(
213 "code_challenge_method",
214 scope_data.code_challenge_method.as_str(),
215 )
216 .append_pair("scope", String::from(scope_data.scope).as_str())
217 .append_pair("redirect_uri", scope_data.redirect_uri.as_str());
218 if let Some(launch) = scope_data.launch.as_deref() {
219 query.append_pair("launch", launch);
220 }
221 if let Some(aud) = scope_data.aud.as_deref() {
222 query.append_pair("aud", aud);
223 }
224
225 let authorization_route = oidc_route_string(&tenant, &project, "auth/authorize")
226 .to_str()
227 .expect("Could not create authorize route.")
228 .to_string()
229 + "?"
230 + query.finish().as_str();
231 let redirect = axum::response::Redirect::to(&authorization_route);
232 Ok(redirect.into_response())
233 } else {
234 Err(OIDCError::new(
235 OIDCErrorCode::AccessDenied,
236 Some("User did not accept the requested scopes.".to_string()),
237 error_redirect_uri,
238 ))
239 }
240}