1use chrono::{TimeDelta, Utc};
2use http::{Method, Request, StatusCode};
3use jacquard_common::{
4 CowStr, IntoStatic,
5 cowstr::ToCowStr,
6 http_client::HttpClient,
7 session::SessionStoreError,
8 types::{
9 did::Did,
10 string::{AtStrError, Datetime},
11 },
12};
13use jacquard_identity::resolver::IdentityError;
14use serde::Serialize;
15use serde_json::Value;
16use smol_str::ToSmolStr;
17use thiserror::Error;
18
19use crate::{
20 FALLBACK_ALG,
21 atproto::atproto_client_metadata,
22 dpop::DpopExt,
23 jose::jwt::{RegisteredClaims, RegisteredClaimsAud},
24 keyset::Keyset,
25 resolver::OAuthResolver,
26 scopes::Scope,
27 session::{
28 AuthRequestData, ClientData, ClientSessionData, DpopClientData, DpopDataSource, DpopReqData,
29 },
30 types::{
31 AuthorizationCodeChallengeMethod, AuthorizationResponseType, AuthorizeOptionPrompt,
32 OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthParResponse,
33 OAuthTokenResponse, ParParameters, RefreshRequestParameters, RevocationRequestParameters,
34 TokenGrantType, TokenRequestParameters, TokenSet,
35 },
36 utils::{compare_algos, generate_dpop_key, generate_nonce, generate_pkce},
37};
38
39// https://datatracker.ietf.org/doc/html/rfc7523#section-2.2
40const CLIENT_ASSERTION_TYPE_JWT_BEARER: &str =
41 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
42
43#[derive(Error, Debug, miette::Diagnostic)]
44pub enum RequestError {
45 #[error("no {0} endpoint available")]
46 #[diagnostic(code(jacquard_oauth::request::no_endpoint), help("server does not advertise this endpoint"))]
47 NoEndpoint(CowStr<'static>),
48 #[error("token response verification failed")]
49 #[diagnostic(code(jacquard_oauth::request::token_verification))]
50 TokenVerification,
51 #[error("unsupported authentication method")]
52 #[diagnostic(
53 code(jacquard_oauth::request::unsupported_auth_method),
54 help("server must support `private_key_jwt` or `none`; configure client metadata accordingly")
55 )]
56 UnsupportedAuthMethod,
57 #[error("no refresh token available")]
58 #[diagnostic(code(jacquard_oauth::request::no_refresh_token))]
59 NoRefreshToken,
60 #[error("failed to parse DID: {0}")]
61 #[diagnostic(code(jacquard_oauth::request::invalid_did))]
62 InvalidDid(#[from] AtStrError),
63 #[error(transparent)]
64 #[diagnostic(code(jacquard_oauth::request::dpop))]
65 DpopClient(#[from] crate::dpop::Error),
66 #[error(transparent)]
67 #[diagnostic(code(jacquard_oauth::request::storage))]
68 Storage(#[from] SessionStoreError),
69
70 #[error(transparent)]
71 #[diagnostic(code(jacquard_oauth::request::resolver))]
72 ResolverError(#[from] crate::resolver::ResolverError),
73 // #[error(transparent)]
74 // OAuthSession(#[from] crate::oauth_session::Error),
75 #[error(transparent)]
76 #[diagnostic(code(jacquard_oauth::request::http_build))]
77 Http(#[from] http::Error),
78 #[error("http status: {0}")]
79 #[diagnostic(code(jacquard_oauth::request::http_status), help("see server response for details"))]
80 HttpStatus(StatusCode),
81 #[error("http status: {0}, body: {1:?}")]
82 #[diagnostic(code(jacquard_oauth::request::http_status_body), help("server returned error JSON; inspect fields like `error`, `error_description`"))]
83 HttpStatusWithBody(StatusCode, Value),
84 #[error(transparent)]
85 #[diagnostic(code(jacquard_oauth::request::identity))]
86 Identity(#[from] IdentityError),
87 #[error(transparent)]
88 #[diagnostic(code(jacquard_oauth::request::keyset))]
89 Keyset(#[from] crate::keyset::Error),
90 #[error(transparent)]
91 #[diagnostic(code(jacquard_oauth::request::serde_form))]
92 SerdeHtmlForm(#[from] serde_html_form::ser::Error),
93 #[error(transparent)]
94 #[diagnostic(code(jacquard_oauth::request::serde_json))]
95 SerdeJson(#[from] serde_json::Error),
96 #[error(transparent)]
97 #[diagnostic(code(jacquard_oauth::request::atproto))]
98 Atproto(#[from] crate::atproto::Error),
99}
100
101pub type Result<T> = core::result::Result<T, RequestError>;
102
103#[allow(dead_code)]
104pub enum OAuthRequest<'a> {
105 Token(TokenRequestParameters<'a>),
106 Refresh(RefreshRequestParameters<'a>),
107 Revocation(RevocationRequestParameters<'a>),
108 Introspection,
109 PushedAuthorizationRequest(ParParameters<'a>),
110}
111
112impl OAuthRequest<'_> {
113 pub fn name(&self) -> CowStr<'static> {
114 CowStr::new_static(match self {
115 Self::Token(_) => "token",
116 Self::Refresh(_) => "refresh",
117 Self::Revocation(_) => "revocation",
118 Self::Introspection => "introspection",
119 Self::PushedAuthorizationRequest(_) => "pushed_authorization_request",
120 })
121 }
122 pub fn expected_status(&self) -> StatusCode {
123 match self {
124 Self::Token(_) | Self::Refresh(_) => StatusCode::OK,
125 Self::PushedAuthorizationRequest(_) => StatusCode::CREATED,
126 // Unlike https://datatracker.ietf.org/doc/html/rfc7009#section-2.2, oauth-provider seems to return `204`.
127 Self::Revocation(_) => StatusCode::NO_CONTENT,
128 _ => unimplemented!(),
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::types::{OAuthAuthorizationServerMetadata, OAuthClientMetadata};
137 use http::{Response as HttpResponse, StatusCode};
138 use bytes::Bytes;
139 use jacquard_common::http_client::HttpClient;
140 use jacquard_identity::resolver::IdentityResolver;
141 use std::sync::Arc;
142 use tokio::sync::Mutex;
143
144 #[derive(Clone, Default)]
145 struct MockClient {
146 resp: Arc<Mutex<Option<HttpResponse<Vec<u8>>>>>,
147 }
148
149 impl HttpClient for MockClient {
150 type Error = std::convert::Infallible;
151 fn send_http(
152 &self,
153 _request: http::Request<Vec<u8>>,
154 ) -> impl core::future::Future<
155 Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>,
156 > + Send {
157 let resp = self.resp.clone();
158 async move { Ok(resp.lock().await.take().unwrap()) }
159 }
160 }
161
162 // IdentityResolver methods won't be called in these tests; provide stubs.
163 #[async_trait::async_trait]
164 impl IdentityResolver for MockClient {
165 fn options(&self) -> &jacquard_identity::resolver::ResolverOptions {
166 use std::sync::LazyLock;
167 static OPTS: LazyLock<jacquard_identity::resolver::ResolverOptions> =
168 LazyLock::new(|| jacquard_identity::resolver::ResolverOptions::default());
169 &OPTS
170 }
171 async fn resolve_handle(
172 &self,
173 _handle: &jacquard_common::types::string::Handle<'_>,
174 ) -> std::result::Result<
175 jacquard_common::types::string::Did<'static>,
176 jacquard_identity::resolver::IdentityError,
177 > {
178 Ok(jacquard_common::types::string::Did::new_static("did:plc:alice").unwrap())
179 }
180 async fn resolve_did_doc(
181 &self,
182 _did: &jacquard_common::types::string::Did<'_>,
183 ) -> std::result::Result<
184 jacquard_identity::resolver::DidDocResponse,
185 jacquard_identity::resolver::IdentityError,
186 > {
187 let doc = serde_json::json!({
188 "id": "did:plc:alice",
189 "service": [{
190 "id": "#pds",
191 "type": "AtprotoPersonalDataServer",
192 "serviceEndpoint": "https://pds"
193 }]
194 });
195 let buf = Bytes::from(serde_json::to_vec(&doc).unwrap());
196 Ok(jacquard_identity::resolver::DidDocResponse {
197 buffer: buf,
198 status: StatusCode::OK,
199 requested: None,
200 })
201 }
202 }
203
204 // Allow using DPoP helpers on MockClient
205 impl crate::dpop::DpopExt for MockClient {}
206 impl crate::resolver::OAuthResolver for MockClient {}
207
208 fn base_metadata() -> OAuthMetadata {
209 let mut server = OAuthAuthorizationServerMetadata::default();
210 server.issuer = CowStr::from("https://issuer");
211 server.authorization_endpoint = CowStr::from("https://issuer/authorize");
212 server.token_endpoint = CowStr::from("https://issuer/token");
213 OAuthMetadata {
214 server_metadata: server,
215 client_metadata: OAuthClientMetadata {
216 client_id: url::Url::parse("https://client").unwrap(),
217 client_uri: None,
218 redirect_uris: vec![url::Url::parse("https://client/cb").unwrap()],
219 scope: Some(CowStr::from("atproto")),
220 grant_types: None,
221 token_endpoint_auth_method: Some(CowStr::from("none")),
222 dpop_bound_access_tokens: None,
223 jwks_uri: None,
224 jwks: None,
225 token_endpoint_auth_signing_alg: None,
226 },
227 keyset: None,
228 }
229 }
230
231 #[tokio::test]
232 async fn par_missing_endpoint() {
233 let mut meta = base_metadata();
234 meta.server_metadata.require_pushed_authorization_requests = Some(true);
235 meta.server_metadata.pushed_authorization_request_endpoint = None;
236 // require_pushed_authorization_requests is true and no endpoint
237 let err = super::par(&MockClient::default(), None, None, &meta)
238 .await
239 .unwrap_err();
240 match err {
241 RequestError::NoEndpoint(name) => {
242 assert_eq!(name.as_ref(), "pushed_authorization_request");
243 }
244 other => panic!("unexpected: {other:?}"),
245 }
246 }
247
248 #[tokio::test]
249 async fn refresh_no_refresh_token() {
250 let client = MockClient::default();
251 let meta = base_metadata();
252 let mut session = ClientSessionData {
253 account_did: jacquard_common::types::string::Did::new_static("did:plc:alice").unwrap(),
254 session_id: CowStr::from("state"),
255 host_url: url::Url::parse("https://pds").unwrap(),
256 authserver_url: url::Url::parse("https://issuer").unwrap(),
257 authserver_token_endpoint: CowStr::from("https://issuer/token"),
258 authserver_revocation_endpoint: None,
259 scopes: vec![],
260 dpop_data: DpopClientData {
261 dpop_key: crate::utils::generate_key(&[CowStr::from("ES256")]).unwrap(),
262 dpop_authserver_nonce: CowStr::from(""),
263 dpop_host_nonce: CowStr::from(""),
264 },
265 token_set: crate::types::TokenSet {
266 iss: CowStr::from("https://issuer"),
267 sub: jacquard_common::types::string::Did::new_static("did:plc:alice").unwrap(),
268 aud: CowStr::from("https://pds"),
269 scope: None,
270 refresh_token: None,
271 access_token: CowStr::from("abc"),
272 token_type: crate::types::OAuthTokenType::DPoP,
273 expires_at: None,
274 },
275 };
276 let err = super::refresh(&client, session, &meta).await.unwrap_err();
277 matches!(err, RequestError::NoRefreshToken);
278 }
279
280 #[tokio::test]
281 async fn exchange_code_missing_sub() {
282 let client = MockClient::default();
283 // set mock HTTP response body: token response without `sub`
284 *client.resp.lock().await = Some(
285 HttpResponse::builder()
286 .status(StatusCode::OK)
287 .body(serde_json::to_vec(&serde_json::json!({
288 "access_token":"tok",
289 "token_type":"DPoP",
290 "expires_in": 3600
291 })).unwrap())
292 .unwrap(),
293 );
294 let meta = base_metadata();
295 let mut dpop = DpopReqData {
296 dpop_key: crate::utils::generate_key(&[CowStr::from("ES256")]).unwrap(),
297 dpop_authserver_nonce: None,
298 };
299 let err = super::exchange_code(&client, &mut dpop, "abc", "verifier", &meta)
300 .await
301 .unwrap_err();
302 matches!(err, RequestError::TokenVerification);
303 }
304}
305
306#[derive(Debug, Serialize)]
307pub struct RequestPayload<'a, T>
308where
309 T: Serialize,
310{
311 client_id: CowStr<'a>,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 client_assertion_type: Option<CowStr<'a>>,
314 #[serde(skip_serializing_if = "Option::is_none")]
315 client_assertion: Option<CowStr<'a>>,
316 #[serde(flatten)]
317 parameters: T,
318}
319
320#[derive(Debug, Clone)]
321pub struct OAuthMetadata {
322 pub server_metadata: OAuthAuthorizationServerMetadata<'static>,
323 pub client_metadata: OAuthClientMetadata<'static>,
324 pub keyset: Option<Keyset>,
325}
326
327impl OAuthMetadata {
328 pub async fn new<'r, T: HttpClient + OAuthResolver + Send + Sync>(
329 client: &T,
330 ClientData { keyset, config }: &ClientData<'r>,
331 session_data: &ClientSessionData<'r>,
332 ) -> Result<Self> {
333 Ok(OAuthMetadata {
334 server_metadata: client
335 .get_authorization_server_metadata(&session_data.authserver_url)
336 .await?,
337 client_metadata: atproto_client_metadata(config.clone(), &keyset)
338 .unwrap()
339 .into_static(),
340 keyset: keyset.clone(),
341 })
342 }
343}
344
345pub async fn par<'r, T: OAuthResolver + DpopExt + Send + Sync + 'static>(
346 client: &T,
347 login_hint: Option<CowStr<'r>>,
348 prompt: Option<AuthorizeOptionPrompt>,
349 metadata: &OAuthMetadata,
350) -> crate::request::Result<AuthRequestData<'r>> {
351 let state = generate_nonce();
352 let (code_challenge, verifier) = generate_pkce();
353
354 let Some(dpop_key) = generate_dpop_key(&metadata.server_metadata) else {
355 return Err(RequestError::TokenVerification);
356 };
357 let mut dpop_data = DpopReqData {
358 dpop_key,
359 dpop_authserver_nonce: None,
360 };
361 let parameters = ParParameters {
362 response_type: AuthorizationResponseType::Code,
363 redirect_uri: metadata.client_metadata.redirect_uris[0].to_cowstr(),
364 state: state.clone(),
365 scope: metadata.client_metadata.scope.clone(),
366 response_mode: None,
367 code_challenge,
368 code_challenge_method: AuthorizationCodeChallengeMethod::S256,
369 login_hint: login_hint,
370 prompt: prompt.map(CowStr::from),
371 };
372 if metadata
373 .server_metadata
374 .pushed_authorization_request_endpoint
375 .is_some()
376 {
377 let par_response = oauth_request::<OAuthParResponse, T, DpopReqData>(
378 &client,
379 &mut dpop_data,
380 OAuthRequest::PushedAuthorizationRequest(parameters),
381 metadata,
382 )
383 .await?;
384
385 let scopes = if let Some(scope) = &metadata.client_metadata.scope {
386 Scope::parse_multiple_reduced(&scope)
387 .expect("Failed to parse scopes")
388 .into_static()
389 } else {
390 vec![]
391 };
392 let auth_req_data = AuthRequestData {
393 state,
394 authserver_url: url::Url::parse(&metadata.server_metadata.issuer)
395 .expect("Failed to parse issuer URL"),
396 account_did: None,
397 scopes,
398 request_uri: par_response.request_uri.to_cowstr().into_static(),
399 authserver_token_endpoint: metadata.server_metadata.token_endpoint.clone(),
400 authserver_revocation_endpoint: metadata.server_metadata.revocation_endpoint.clone(),
401 pkce_verifier: verifier,
402 dpop_data,
403 };
404
405 Ok(auth_req_data)
406 } else if metadata
407 .server_metadata
408 .require_pushed_authorization_requests
409 == Some(true)
410 {
411 Err(RequestError::NoEndpoint(CowStr::new_static(
412 "pushed_authorization_request",
413 )))
414 } else {
415 todo!("use of PAR is mandatory")
416 }
417}
418
419pub async fn refresh<'r, T>(
420 client: &T,
421 mut session_data: ClientSessionData<'r>,
422 metadata: &OAuthMetadata,
423) -> Result<ClientSessionData<'r>>
424where
425 T: OAuthResolver + DpopExt + Send + Sync + 'static,
426{
427 let Some(refresh_token) = session_data.token_set.refresh_token.as_ref() else {
428 return Err(RequestError::NoRefreshToken);
429 };
430
431 // /!\ IMPORTANT /!\
432 //
433 // The "sub" MUST be a DID, whose issuer authority is indeed the server we
434 // are trying to obtain credentials from. Note that we are doing this
435 // *before* we actually try to refresh the token:
436 // 1) To avoid unnecessary refresh
437 // 2) So that the refresh is the last async operation, ensuring as few
438 // async operations happen before the result gets a chance to be stored.
439 let aud = client
440 .verify_issuer(&metadata.server_metadata, &session_data.token_set.sub)
441 .await?;
442 let iss = metadata.server_metadata.issuer.clone();
443
444 let response = oauth_request::<OAuthTokenResponse, T, DpopClientData>(
445 client,
446 &mut session_data.dpop_data,
447 OAuthRequest::Refresh(RefreshRequestParameters {
448 grant_type: TokenGrantType::RefreshToken,
449 refresh_token: refresh_token.clone(),
450 scope: None,
451 }),
452 metadata,
453 )
454 .await?;
455
456 let expires_at = response.expires_in.and_then(|expires_in| {
457 let now = Datetime::now();
458 now.as_ref()
459 .checked_add_signed(TimeDelta::seconds(expires_in))
460 .map(Datetime::new)
461 });
462
463 session_data.update_with_tokens(TokenSet {
464 iss,
465 sub: session_data.token_set.sub.clone(),
466 aud: CowStr::Owned(aud.to_smolstr()),
467 scope: response.scope.map(CowStr::Owned),
468 access_token: CowStr::Owned(response.access_token),
469 refresh_token: response.refresh_token.map(CowStr::Owned),
470 token_type: response.token_type,
471 expires_at,
472 });
473
474 Ok(session_data)
475}
476
477pub async fn exchange_code<'r, T, D>(
478 client: &T,
479 data_source: &'r mut D,
480 code: &str,
481 verifier: &str,
482 metadata: &OAuthMetadata,
483) -> Result<TokenSet<'r>>
484where
485 T: OAuthResolver + DpopExt + Send + Sync + 'static,
486 D: DpopDataSource,
487{
488 let token_response = oauth_request::<OAuthTokenResponse, T, D>(
489 client,
490 data_source,
491 OAuthRequest::Token(TokenRequestParameters {
492 grant_type: TokenGrantType::AuthorizationCode,
493 code: code.into(),
494 redirect_uri: CowStr::Owned(
495 metadata.client_metadata.redirect_uris[0]
496 .clone()
497 .to_smolstr(),
498 ), // ?
499 code_verifier: verifier.into(),
500 }),
501 metadata,
502 )
503 .await?;
504 let Some(sub) = token_response.sub else {
505 return Err(RequestError::TokenVerification);
506 };
507 let sub = Did::new_owned(sub)?;
508 let iss = metadata.server_metadata.issuer.clone();
509 // /!\ IMPORTANT /!\
510 //
511 // The token_response MUST always be valid before the "sub" it contains
512 // can be trusted (see Atproto's OAuth spec for details).
513 let aud = client
514 .verify_issuer(&metadata.server_metadata, &sub)
515 .await?;
516
517 let expires_at = token_response.expires_in.and_then(|expires_in| {
518 Datetime::now()
519 .as_ref()
520 .checked_add_signed(TimeDelta::seconds(expires_in))
521 .map(Datetime::new)
522 });
523 Ok(TokenSet {
524 iss,
525 sub,
526 aud: CowStr::Owned(aud.to_smolstr()),
527 scope: token_response.scope.map(CowStr::Owned),
528 access_token: CowStr::Owned(token_response.access_token),
529 refresh_token: token_response.refresh_token.map(CowStr::Owned),
530 token_type: token_response.token_type,
531 expires_at,
532 })
533}
534
535pub async fn revoke<'r, T, D>(
536 client: &T,
537 data_source: &'r mut D,
538 token: &str,
539 metadata: &OAuthMetadata,
540) -> Result<()>
541where
542 T: OAuthResolver + DpopExt + Send + Sync + 'static,
543 D: DpopDataSource,
544{
545 oauth_request::<(), T, D>(
546 client,
547 data_source,
548 OAuthRequest::Revocation(RevocationRequestParameters {
549 token: token.into(),
550 }),
551 metadata,
552 )
553 .await?;
554 Ok(())
555}
556
557pub async fn oauth_request<'de: 'r, 'r, O, T, D>(
558 client: &T,
559 data_source: &'r mut D,
560 request: OAuthRequest<'r>,
561 metadata: &OAuthMetadata,
562) -> Result<O>
563where
564 T: OAuthResolver + DpopExt + Send + Sync + 'static,
565 O: serde::de::DeserializeOwned,
566 D: DpopDataSource,
567{
568 let Some(url) = endpoint_for_req(&metadata.server_metadata, &request) else {
569 return Err(RequestError::NoEndpoint(request.name()));
570 };
571 let client_assertions = build_auth(
572 metadata.keyset.as_ref(),
573 &metadata.server_metadata,
574 &metadata.client_metadata,
575 )?;
576 let body = match &request {
577 OAuthRequest::Token(params) => build_oauth_req_body(client_assertions, params)?,
578 OAuthRequest::Refresh(params) => build_oauth_req_body(client_assertions, params)?,
579 OAuthRequest::Revocation(params) => build_oauth_req_body(client_assertions, params)?,
580 OAuthRequest::PushedAuthorizationRequest(params) => {
581 build_oauth_req_body(client_assertions, params)?
582 }
583 _ => unimplemented!(),
584 };
585 let req = Request::builder()
586 .uri(url.to_string())
587 .method(Method::POST)
588 .header("Content-Type", "application/x-www-form-urlencoded")
589 .body(body.into_bytes())?;
590 let res = client
591 .dpop_server_call(data_source)
592 .send(req)
593 .await
594 .map_err(RequestError::DpopClient)?;
595 if res.status() == request.expected_status() {
596 let body = res.body();
597 if body.is_empty() {
598 // since an empty body cannot be deserialized, use “null” temporarily to allow deserialization to `()`.
599 Ok(serde_json::from_slice(b"null")?)
600 } else {
601 let output: O = serde_json::from_slice(body)?;
602 Ok(output)
603 }
604 } else if res.status().is_client_error() {
605 Err(RequestError::HttpStatusWithBody(
606 res.status(),
607 serde_json::from_slice(res.body())?,
608 ))
609 } else {
610 Err(RequestError::HttpStatus(res.status()))
611 }
612}
613
614#[inline]
615fn endpoint_for_req<'a, 'r>(
616 server_metadata: &'r OAuthAuthorizationServerMetadata<'a>,
617 request: &'r OAuthRequest,
618) -> Option<&'r CowStr<'a>> {
619 match request {
620 OAuthRequest::Token(_) | OAuthRequest::Refresh(_) => Some(&server_metadata.token_endpoint),
621 OAuthRequest::Revocation(_) => server_metadata.revocation_endpoint.as_ref(),
622 OAuthRequest::Introspection => server_metadata.introspection_endpoint.as_ref(),
623 OAuthRequest::PushedAuthorizationRequest(_) => server_metadata
624 .pushed_authorization_request_endpoint
625 .as_ref(),
626 }
627}
628
629#[inline]
630fn build_oauth_req_body<'a, S>(client_assertions: ClientAuth<'a>, parameters: S) -> Result<String>
631where
632 S: Serialize,
633{
634 Ok(serde_html_form::to_string(RequestPayload {
635 client_id: client_assertions.client_id,
636 client_assertion_type: client_assertions.assertion_type,
637 client_assertion: client_assertions.assertion,
638 parameters,
639 })?)
640}
641
642#[derive(Debug, Clone, Default)]
643pub struct ClientAuth<'a> {
644 client_id: CowStr<'a>,
645 assertion_type: Option<CowStr<'a>>, // either none or `CLIENT_ASSERTION_TYPE_JWT_BEARER`
646 assertion: Option<CowStr<'a>>,
647}
648
649impl<'s> ClientAuth<'s> {
650 pub fn new_id(client_id: CowStr<'s>) -> Self {
651 Self {
652 client_id,
653 assertion_type: None,
654 assertion: None,
655 }
656 }
657}
658
659fn build_auth<'a>(
660 keyset: Option<&Keyset>,
661 server_metadata: &OAuthAuthorizationServerMetadata<'a>,
662 client_metadata: &OAuthClientMetadata<'a>,
663) -> Result<ClientAuth<'a>> {
664 let method_supported = server_metadata
665 .token_endpoint_auth_methods_supported
666 .as_ref();
667
668 let client_id = client_metadata.client_id.to_cowstr().into_static();
669 if let Some(method) = client_metadata.token_endpoint_auth_method.as_ref() {
670 match (*method).as_ref() {
671 "private_key_jwt"
672 if method_supported
673 .as_ref()
674 .is_some_and(|v| v.contains(&CowStr::new_static("private_key_jwt"))) =>
675 {
676 if let Some(keyset) = &keyset {
677 let mut algs = server_metadata
678 .token_endpoint_auth_signing_alg_values_supported
679 .clone()
680 .unwrap_or(vec![FALLBACK_ALG.into()]);
681 algs.sort_by(compare_algos);
682 let iat = Utc::now().timestamp();
683 return Ok(ClientAuth {
684 client_id: client_id.clone(),
685 assertion_type: Some(CowStr::new_static(CLIENT_ASSERTION_TYPE_JWT_BEARER)),
686 assertion: Some(
687 keyset.create_jwt(
688 &algs,
689 // https://datatracker.ietf.org/doc/html/rfc7523#section-3
690 RegisteredClaims {
691 iss: Some(client_id.clone()),
692 sub: Some(client_id),
693 aud: Some(RegisteredClaimsAud::Single(
694 server_metadata.issuer.clone(),
695 )),
696 exp: Some(iat + 60),
697 // "iat" is required and **MUST** be less than one minute
698 // https://datatracker.ietf.org/doc/html/rfc9101
699 iat: Some(iat),
700 // atproto oauth-provider requires "jti" to be present
701 jti: Some(generate_nonce()),
702 ..Default::default()
703 }
704 .into(),
705 )?,
706 ),
707 });
708 }
709 }
710 "none"
711 if method_supported
712 .as_ref()
713 .is_some_and(|v| v.contains(&CowStr::new_static("none"))) =>
714 {
715 return Ok(ClientAuth::new_id(client_id));
716 }
717 _ => {}
718 }
719 }
720
721 Err(RequestError::UnsupportedAuthMethod)
722}