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