mas_handlers/upstream_oauth2/
authorize.rs1use axum::{
8 extract::{Path, State},
9 response::{IntoResponse, Redirect},
10};
11use axum_extra::extract::Query;
12use hyper::StatusCode;
13use mas_axum_utils::{GenericError, InternalError, cookies::CookieJar};
14use mas_data_model::{BoxClock, BoxRng, UpstreamOAuthProvider};
15use mas_oidc_client::requests::authorization_code::AuthorizationRequestData;
16use mas_router::{PostAuthAction, UrlBuilder};
17use mas_storage::{
18 BoxRepository,
19 upstream_oauth2::{UpstreamOAuthProviderRepository, UpstreamOAuthSessionRepository},
20};
21use thiserror::Error;
22use ulid::Ulid;
23
24use super::{UpstreamSessionsCookie, cache::LazyProviderInfos};
25use crate::{
26 impl_from_error_for_route, upstream_oauth2::cache::MetadataCache,
27 views::shared::OptionalPostAuthAction,
28};
29
30#[derive(Debug, Error)]
31pub(crate) enum RouteError {
32 #[error("Provider not found")]
33 ProviderNotFound,
34
35 #[error(transparent)]
36 Internal(Box<dyn std::error::Error>),
37}
38
39impl_from_error_for_route!(mas_oidc_client::error::DiscoveryError);
40impl_from_error_for_route!(mas_oidc_client::error::AuthorizationError);
41impl_from_error_for_route!(mas_storage::RepositoryError);
42
43impl IntoResponse for RouteError {
44 fn into_response(self) -> axum::response::Response {
45 match self {
46 e @ Self::ProviderNotFound => {
47 GenericError::new(StatusCode::NOT_FOUND, e).into_response()
48 }
49 Self::Internal(e) => InternalError::new(e).into_response(),
50 }
51 }
52}
53
54#[tracing::instrument(
55 name = "handlers.upstream_oauth2.authorize.get",
56 fields(upstream_oauth_provider.id = %provider_id),
57 skip_all,
58)]
59pub(crate) async fn get(
60 mut rng: BoxRng,
61 clock: BoxClock,
62 State(metadata_cache): State<MetadataCache>,
63 mut repo: BoxRepository,
64 State(url_builder): State<UrlBuilder>,
65 State(http_client): State<reqwest::Client>,
66 cookie_jar: CookieJar,
67 Path(provider_id): Path<Ulid>,
68 Query(query): Query<OptionalPostAuthAction>,
69) -> Result<impl IntoResponse, RouteError> {
70 let provider = repo
71 .upstream_oauth_provider()
72 .lookup(provider_id)
73 .await?
74 .filter(UpstreamOAuthProvider::enabled)
75 .ok_or(RouteError::ProviderNotFound)?;
76
77 let mut lazy_metadata = LazyProviderInfos::new(&metadata_cache, &provider, &http_client);
81 lazy_metadata.maybe_discover().await?;
82
83 let redirect_uri = url_builder.upstream_oauth_callback(provider.id);
84
85 let mut data = AuthorizationRequestData::new(
86 provider.client_id.clone(),
87 provider.scope.clone(),
88 redirect_uri,
89 );
90
91 if let Some(response_mode) = provider.response_mode {
92 data = data.with_response_mode(response_mode.into());
93 }
94
95 if provider.forward_login_hint
98 && let Some(PostAuthAction::ContinueAuthorizationGrant { id }) = &query.post_auth_action
99 && let Some(login_hint) = repo
100 .oauth2_authorization_grant()
101 .lookup(*id)
102 .await?
103 .and_then(|grant| grant.login_hint)
104 {
105 data = data.with_login_hint(login_hint);
106 }
107
108 let data = if let Some(methods) = lazy_metadata.pkce_methods().await? {
109 data.with_code_challenge_methods_supported(methods)
110 } else {
111 data
112 };
113
114 let (mut url, data) = mas_oidc_client::requests::authorization_code::build_authorization_url(
116 lazy_metadata.authorization_endpoint().await?.clone(),
117 data,
118 &mut rng,
119 )?;
120
121 {
123 let mut params = url.query_pairs_mut();
125 for (key, value) in &provider.additional_authorization_parameters {
126 params.append_pair(key, value);
127 }
128 }
129
130 let session = repo
131 .upstream_oauth_session()
132 .add(
133 &mut rng,
134 &clock,
135 &provider,
136 data.state.clone(),
137 data.code_challenge_verifier,
138 data.nonce,
139 )
140 .await?;
141
142 let cookie_jar = UpstreamSessionsCookie::load(&cookie_jar)
143 .add(session.id, provider.id, data.state, query.post_auth_action)
144 .save(cookie_jar, &clock);
145
146 repo.save().await?;
147
148 Ok((cookie_jar, Redirect::temporary(url.as_str())))
149}