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, &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}