1use std::{collections::HashSet, num::NonZeroU32};
20
21use base64ct::{Base64UrlUnpadded, Encoding};
22use chrono::{DateTime, Utc};
23use http::header::CONTENT_TYPE;
24use language_tags::LanguageTag;
25use mas_http::{CatchHttpCodesLayer, FormUrlencodedRequestLayer, JsonResponseLayer};
26use mas_iana::oauth::{OAuthAuthorizationEndpointResponseType, PkceCodeChallengeMethod};
27use mas_jose::claims::{self, TokenHash};
28use oauth2_types::{
29 pkce,
30 prelude::CodeChallengeMethodExt,
31 requests::{
32 AccessTokenRequest, AccessTokenResponse, AuthorizationCodeGrant, AuthorizationRequest,
33 Display, Prompt, PushedAuthorizationResponse,
34 },
35 scope::Scope,
36};
37use rand::{
38 distributions::{Alphanumeric, DistString},
39 Rng,
40};
41use serde::Serialize;
42use serde_with::skip_serializing_none;
43use tower::{Layer, Service, ServiceExt};
44use url::Url;
45
46use super::jose::JwtVerificationData;
47use crate::{
48 error::{
49 AuthorizationError, IdTokenError, PushedAuthorizationError, TokenAuthorizationCodeError,
50 },
51 http_service::HttpService,
52 requests::{jose::verify_id_token, token::request_access_token},
53 types::{
54 client_credentials::ClientCredentials,
55 scope::{ScopeExt, ScopeToken},
56 IdToken,
57 },
58 utils::{http_all_error_status_codes, http_error_mapper},
59};
60
61#[derive(Debug, Clone)]
63pub struct AuthorizationRequestData {
64 pub client_id: String,
66
67 pub scope: Scope,
72
73 pub redirect_uri: Url,
77
78 pub code_challenge_methods_supported: Option<Vec<PkceCodeChallengeMethod>>,
83
84 pub display: Option<Display>,
87
88 pub prompt: Option<Vec<Prompt>>,
93
94 pub max_age: Option<NonZeroU32>,
97
98 pub ui_locales: Option<Vec<LanguageTag>>,
100
101 pub id_token_hint: Option<String>,
105
106 pub login_hint: Option<String>,
109
110 pub acr_values: Option<HashSet<String>>,
112}
113
114impl AuthorizationRequestData {
115 #[must_use]
118 pub fn new(client_id: String, scope: Scope, redirect_uri: Url) -> Self {
119 Self {
120 client_id,
121 scope,
122 redirect_uri,
123 code_challenge_methods_supported: None,
124 display: None,
125 prompt: None,
126 max_age: None,
127 ui_locales: None,
128 id_token_hint: None,
129 login_hint: None,
130 acr_values: None,
131 }
132 }
133
134 #[must_use]
137 pub fn with_code_challenge_methods_supported(
138 mut self,
139 code_challenge_methods_supported: Vec<PkceCodeChallengeMethod>,
140 ) -> Self {
141 self.code_challenge_methods_supported = Some(code_challenge_methods_supported);
142 self
143 }
144
145 #[must_use]
147 pub fn with_display(mut self, display: Display) -> Self {
148 self.display = Some(display);
149 self
150 }
151
152 #[must_use]
154 pub fn with_prompt(mut self, prompt: Vec<Prompt>) -> Self {
155 self.prompt = Some(prompt);
156 self
157 }
158
159 #[must_use]
161 pub fn with_max_age(mut self, max_age: NonZeroU32) -> Self {
162 self.max_age = Some(max_age);
163 self
164 }
165
166 #[must_use]
168 pub fn with_ui_locales(mut self, ui_locales: Vec<LanguageTag>) -> Self {
169 self.ui_locales = Some(ui_locales);
170 self
171 }
172
173 #[must_use]
175 pub fn with_id_token_hint(mut self, id_token_hint: String) -> Self {
176 self.id_token_hint = Some(id_token_hint);
177 self
178 }
179
180 #[must_use]
182 pub fn with_login_hint(mut self, login_hint: String) -> Self {
183 self.login_hint = Some(login_hint);
184 self
185 }
186
187 #[must_use]
189 pub fn with_acr_values(mut self, acr_values: HashSet<String>) -> Self {
190 self.acr_values = Some(acr_values);
191 self
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct AuthorizationValidationData {
199 pub state: String,
201
202 pub nonce: String,
204
205 pub redirect_uri: Url,
207
208 pub code_challenge_verifier: Option<String>,
210}
211
212#[skip_serializing_none]
213#[derive(Clone, Serialize)]
214struct FullAuthorizationRequest {
215 #[serde(flatten)]
216 inner: AuthorizationRequest,
217 #[serde(flatten)]
218 pkce: Option<pkce::AuthorizationRequest>,
219}
220
221fn build_authorization_request(
223 authorization_data: AuthorizationRequestData,
224 rng: &mut impl Rng,
225) -> Result<(FullAuthorizationRequest, AuthorizationValidationData), AuthorizationError> {
226 let AuthorizationRequestData {
227 client_id,
228 mut scope,
229 redirect_uri,
230 code_challenge_methods_supported,
231 display,
232 prompt,
233 max_age,
234 ui_locales,
235 id_token_hint,
236 login_hint,
237 acr_values,
238 } = authorization_data;
239
240 let state = Alphanumeric.sample_string(rng, 16);
242 let nonce = Alphanumeric.sample_string(rng, 16);
243
244 let (pkce, code_challenge_verifier) = if code_challenge_methods_supported
246 .iter()
247 .any(|methods| methods.contains(&PkceCodeChallengeMethod::S256))
248 {
249 let mut verifier = [0u8; 32];
250 rng.fill(&mut verifier);
251
252 let method = PkceCodeChallengeMethod::S256;
253 let verifier = Base64UrlUnpadded::encode_string(&verifier);
254 let code_challenge = method.compute_challenge(&verifier)?.into();
255
256 let pkce = pkce::AuthorizationRequest {
257 code_challenge_method: method,
258 code_challenge,
259 };
260
261 (Some(pkce), Some(verifier))
262 } else {
263 (None, None)
264 };
265
266 scope.insert_token(ScopeToken::Openid);
267
268 let auth_request = FullAuthorizationRequest {
269 inner: AuthorizationRequest {
270 response_type: OAuthAuthorizationEndpointResponseType::Code.into(),
271 client_id,
272 redirect_uri: Some(redirect_uri.clone()),
273 scope,
274 state: Some(state.clone()),
275 response_mode: None,
276 nonce: Some(nonce.clone()),
277 display,
278 prompt,
279 max_age,
280 ui_locales,
281 id_token_hint,
282 login_hint,
283 acr_values,
284 request: None,
285 request_uri: None,
286 registration: None,
287 },
288 pkce,
289 };
290
291 let auth_data = AuthorizationValidationData {
292 state,
293 nonce,
294 redirect_uri,
295 code_challenge_verifier,
296 };
297
298 Ok((auth_request, auth_data))
299}
300
301#[allow(clippy::too_many_lines)]
332pub fn build_authorization_url(
333 authorization_endpoint: Url,
334 authorization_data: AuthorizationRequestData,
335 rng: &mut impl Rng,
336) -> Result<(Url, AuthorizationValidationData), AuthorizationError> {
337 tracing::debug!(
338 scope = ?authorization_data.scope,
339 "Authorizing..."
340 );
341
342 let (authorization_request, validation_data) =
343 build_authorization_request(authorization_data, rng)?;
344
345 let authorization_query = serde_urlencoded::to_string(authorization_request)?;
346
347 let mut authorization_url = authorization_endpoint;
348
349 let mut full_query = authorization_url
351 .query()
352 .map(ToOwned::to_owned)
353 .unwrap_or_default();
354 if !full_query.is_empty() {
355 full_query.push('&');
356 }
357 full_query.push_str(&authorization_query);
358
359 authorization_url.set_query(Some(&full_query));
360
361 Ok((authorization_url, validation_data))
362}
363
364#[tracing::instrument(skip_all, fields(par_endpoint))]
407pub async fn build_par_authorization_url(
408 http_service: &HttpService,
409 client_credentials: ClientCredentials,
410 par_endpoint: &Url,
411 authorization_endpoint: Url,
412 authorization_data: AuthorizationRequestData,
413 now: DateTime<Utc>,
414 rng: &mut impl Rng,
415) -> Result<(Url, AuthorizationValidationData), AuthorizationError> {
416 tracing::debug!(
417 scope = ?authorization_data.scope,
418 "Authorizing with a PAR..."
419 );
420
421 let client_id = client_credentials.client_id().to_owned();
422
423 let (authorization_request, validation_data) =
424 build_authorization_request(authorization_data, rng)?;
425
426 let par_request = http::Request::post(par_endpoint.as_str())
427 .header(CONTENT_TYPE, mime::APPLICATION_WWW_FORM_URLENCODED.as_ref())
428 .body(authorization_request)
429 .map_err(PushedAuthorizationError::from)?;
430
431 let par_request = client_credentials
432 .apply_to_request(par_request, now, rng)
433 .map_err(PushedAuthorizationError::from)?;
434
435 let service = (
436 FormUrlencodedRequestLayer::default(),
437 JsonResponseLayer::<PushedAuthorizationResponse>::default(),
438 CatchHttpCodesLayer::new(http_all_error_status_codes(), http_error_mapper),
439 )
440 .layer(http_service.clone());
441
442 let par_response = service
443 .ready_oneshot()
444 .await
445 .map_err(PushedAuthorizationError::from)?
446 .call(par_request)
447 .await
448 .map_err(PushedAuthorizationError::from)?
449 .into_body();
450
451 let authorization_query = serde_urlencoded::to_string([
452 ("request_uri", par_response.request_uri.as_str()),
453 ("client_id", &client_id),
454 ])?;
455
456 let mut authorization_url = authorization_endpoint;
457
458 let mut full_query = authorization_url
460 .query()
461 .map(ToOwned::to_owned)
462 .unwrap_or_default();
463 if !full_query.is_empty() {
464 full_query.push('&');
465 }
466 full_query.push_str(&authorization_query);
467
468 authorization_url.set_query(Some(&full_query));
469
470 Ok((authorization_url, validation_data))
471}
472
473#[allow(clippy::too_many_arguments)]
511#[tracing::instrument(skip_all, fields(token_endpoint))]
512pub async fn access_token_with_authorization_code(
513 http_service: &HttpService,
514 client_credentials: ClientCredentials,
515 token_endpoint: &Url,
516 code: String,
517 validation_data: AuthorizationValidationData,
518 id_token_verification_data: Option<JwtVerificationData<'_>>,
519 now: DateTime<Utc>,
520 rng: &mut impl Rng,
521) -> Result<(AccessTokenResponse, Option<IdToken<'static>>), TokenAuthorizationCodeError> {
522 tracing::debug!("Exchanging authorization code for access token...");
523
524 let token_response = request_access_token(
525 http_service,
526 client_credentials,
527 token_endpoint,
528 AccessTokenRequest::AuthorizationCode(AuthorizationCodeGrant {
529 code: code.clone(),
530 redirect_uri: Some(validation_data.redirect_uri),
531 code_verifier: validation_data.code_challenge_verifier,
532 }),
533 now,
534 rng,
535 )
536 .await?;
537
538 let id_token = if let Some(verification_data) = id_token_verification_data {
539 let signing_alg = verification_data.signing_algorithm;
540
541 let id_token = token_response
542 .id_token
543 .as_deref()
544 .ok_or(IdTokenError::MissingIdToken)?;
545
546 let id_token = verify_id_token(id_token, verification_data, None, now)?;
547
548 let mut claims = id_token.payload().clone();
549
550 claims::AT_HASH
552 .extract_optional_with_options(
553 &mut claims,
554 TokenHash::new(signing_alg, &token_response.access_token),
555 )
556 .map_err(IdTokenError::from)?;
557
558 claims::C_HASH
560 .extract_optional_with_options(&mut claims, TokenHash::new(signing_alg, &code))
561 .map_err(IdTokenError::from)?;
562
563 claims::NONCE
565 .extract_required_with_options(&mut claims, validation_data.nonce.as_str())
566 .map_err(IdTokenError::from)?;
567
568 Some(id_token.into_owned())
569 } else {
570 None
571 };
572
573 Ok((token_response, id_token))
574}