Skip to main content

haste_server/auth_n/oidc/routes/
mod.rs

1use crate::{
2    auth_n::oidc::middleware::{
3        AuthSessionValidationLayer, OIDCParameterInjectLayer, ParameterConfig, project_exists,
4    },
5    services::ServerState,
6};
7use axum::{
8    Router,
9    http::{
10        HeaderValue,
11        header::{CACHE_CONTROL, PRAGMA},
12    },
13    middleware,
14};
15use axum_extra::routing::RouterExt;
16use haste_fhir_search::SearchEngine;
17use haste_fhir_terminology::FHIRTerminology;
18use haste_repository::Repository;
19use std::sync::{Arc, LazyLock};
20use tower::ServiceBuilder;
21use tower_http::set_header::SetResponseHeaderLayer;
22
23mod authorize;
24pub mod discovery;
25pub mod federated;
26pub mod interactions;
27pub mod introspect;
28mod jwks;
29pub mod route_string;
30pub mod scope;
31pub mod style;
32pub mod token;
33
34static AUTHORIZE_PARAMETERS: LazyLock<Arc<ParameterConfig>> = LazyLock::new(|| {
35    Arc::new(ParameterConfig {
36        required_parameters: vec![
37            "client_id".to_string(),
38            "response_type".to_string(),
39            "state".to_string(),
40            "code_challenge".to_string(),
41            "code_challenge_method".to_string(),
42        ],
43        optional_parameters: vec![
44            "scope".to_string(),
45            "redirect_uri".to_string(),
46            // SMART App Launch: the FHIR server the app will call.
47            "aud".to_string(),
48            // SMART EHR launch: the opaque launch value the app was opened with.
49            "launch".to_string(),
50        ],
51    })
52});
53
54static LOGOUT_PARAMETERS: LazyLock<Arc<ParameterConfig>> = LazyLock::new(|| {
55    Arc::new(ParameterConfig {
56        required_parameters: vec!["client_id".to_string()],
57        optional_parameters: vec!["redirect_uri".to_string()],
58    })
59});
60
61pub fn create_router<
62    Repo: Repository + Send + Sync,
63    Search: SearchEngine + Send + Sync,
64    Terminology: FHIRTerminology + Send + Sync,
65>(
66    state: Arc<ServerState<Repo, Search, Terminology>>,
67) -> Router<Arc<ServerState<Repo, Search, Terminology>>> {
68    Router::new()
69        .merge(Router::new().typed_get(jwks::jwks_get))
70        .merge(Router::new().typed_get(style::smart_style_get))
71        .merge(federated::federated_router())
72        .nest(
73            "/auth",
74            Router::new()
75                // Responses that carry tokens must not be cached (RFC 6749
76                // section 5.1): Cache-Control for HTTP/1.1 caches and Pragma
77                // for HTTP/1.0 ones. An introspection response describes a
78                // token, so it is not cached either.
79                .merge(
80                    Router::new()
81                        .typed_post(token::token)
82                        .typed_post(introspect::introspect)
83                        .route_layer(
84                            ServiceBuilder::new()
85                                .layer(SetResponseHeaderLayer::overriding(
86                                    CACHE_CONTROL,
87                                    HeaderValue::from_static("no-store"),
88                                ))
89                                .layer(SetResponseHeaderLayer::overriding(
90                                    PRAGMA,
91                                    HeaderValue::from_static("no-cache"),
92                                )),
93                        ),
94                )
95                .merge(
96                    Router::new()
97                        .merge(
98                            Router::new()
99                                .typed_post(authorize::authorize)
100                                .typed_get(authorize::authorize)
101                                .route_layer(ServiceBuilder::new().layer(
102                                    OIDCParameterInjectLayer::new((*AUTHORIZE_PARAMETERS).clone()),
103                                )),
104                        )
105                        // The approval form: it posts one `scope` field per
106                        // approved scope and reads its own parameters.
107                        .merge(Router::new().typed_post(scope::scope_post))
108                        .route_layer(
109                            ServiceBuilder::new()
110                                .layer(AuthSessionValidationLayer::new("interactions/login")),
111                        ),
112                ),
113        )
114        .nest("/interactions", interactions::interactions_router())
115        .route_layer(
116            ServiceBuilder::new().layer(middleware::from_fn_with_state(state, project_exists)),
117        )
118}