1use std::{collections::HashMap, fmt, sync::Arc};
2
3use chrono::Utc;
4use http::StatusCode;
5use securitydept_oidc_client::{
6 OidcClient, OidcCodeCallbackResult, OidcCodeCallbackSearchParams,
7 OidcCodeFlowAuthorizationRequest, OidcRefreshTokenResult, PendingOauthStore,
8 auth_state::{
9 OidcExtractedPrincipal, extract_issuer_from_refresh_result,
10 extract_principal_from_code_callback, extract_principal_from_refresh_result,
11 },
12};
13use securitydept_utils::{
14 error::{ErrorPresentation, ToErrorPresentation, UserRecovery},
15 http::ToHttpStatus,
16};
17use serde::Deserialize;
18use serde_json::{Value, json};
19use snafu::Snafu;
20use url::Url;
21
22use super::{
23 capabilities::{
24 MetadataDelivery, MetadataDeliveryKind, PostAuthRedirectPolicy, RefreshMaterialProtection,
25 },
26 metadata_redemption::{
27 MetadataRedemptionId, PendingAuthStateMetadataRedemptionConfig,
28 PendingAuthStateMetadataRedemptionPayload, PendingAuthStateMetadataRedemptionStore,
29 },
30 redirect::BackendOidcModeRedirectUriResolver,
31 refresh_material::{
32 AeadRefreshMaterialProtector, PassthroughRefreshMaterialProtector,
33 RefreshMaterialProtector, SealedRefreshMaterial,
34 },
35 transport::{
36 BackendOidcModeCallbackReturns, BackendOidcModeRefreshPayload,
37 BackendOidcModeRefreshReturns,
38 },
39};
40use crate::{
41 backend_oidc_mode::config::ResolvedBackendOidcModeConfig,
42 models::{
43 AuthStateDelta, AuthStateMetadataDelta, AuthStateMetadataSnapshot, AuthStateSnapshot,
44 AuthTokenDelta, AuthTokenSnapshot, AuthenticatedPrincipal, AuthenticationSource,
45 AuthenticationSourceKind, CurrentAuthStateMetadataSnapshotPartial,
46 },
47};
48
49const PENDING_POST_AUTH_REDIRECT_URI_KEY: &str = "post_auth_redirect_uri";
50
51#[derive(Debug, Clone)]
57pub struct BackendOidcModeCodeCallbackResult {
58 pub post_auth_redirect_uri: Option<Url>,
60 pub auth_state_snapshot: AuthStateSnapshot,
61 pub response_body: BackendOidcModeCallbackReturns,
62}
63
64#[derive(Debug, Clone)]
66pub struct BackendOidcModeTokenRefreshResult {
67 pub post_auth_redirect_uri: Option<Url>,
69 pub auth_state_delta: AuthStateDelta,
70 pub response_body: BackendOidcModeRefreshReturns,
71}
72
73#[derive(Debug, Clone, Default)]
79pub struct BackendOidcModeAuthStateOptions {
80 pub source_provider_id: Option<String>,
81 pub source_attributes: HashMap<String, Value>,
82 pub metadata_attributes: HashMap<String, Value>,
83}
84
85#[derive(Debug, Clone, Deserialize)]
105pub struct BackendOidcModeRuntimeConfig<MC>
106where
107 MC: PendingAuthStateMetadataRedemptionConfig,
108{
109 #[serde(default, bound(deserialize = ""))]
110 pub refresh_material_protection: RefreshMaterialProtection,
111
112 #[serde(
113 default,
114 bound(deserialize = "MC: PendingAuthStateMetadataRedemptionConfig")
115 )]
116 pub metadata_delivery: MetadataDelivery<MC>,
117
118 #[serde(default)]
119 pub post_auth_redirect: PostAuthRedirectPolicy,
120}
121
122impl<MC> Default for BackendOidcModeRuntimeConfig<MC>
123where
124 MC: PendingAuthStateMetadataRedemptionConfig,
125{
126 fn default() -> Self {
127 Self {
128 refresh_material_protection: RefreshMaterialProtection::default(),
129 metadata_delivery: MetadataDelivery::default(),
130 post_auth_redirect: PostAuthRedirectPolicy::default(),
131 }
132 }
133}
134
135#[derive(Clone)]
145pub struct BackendOidcModeRuntime<MS>
146where
147 MS: PendingAuthStateMetadataRedemptionStore,
148{
149 refresh_material_protector: Arc<dyn RefreshMaterialProtector>,
150 redirect_uri_resolver: Option<BackendOidcModeRedirectUriResolver>,
151 metadata_redemption_store: Option<MS>,
152 metadata_delivery_kind: MetadataDeliveryKind,
153}
154
155impl<MS> fmt::Debug for BackendOidcModeRuntime<MS>
156where
157 MS: PendingAuthStateMetadataRedemptionStore,
158{
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 f.write_str("BackendOidcModeRuntime { ... }")
161 }
162}
163
164#[derive(Debug, Snafu)]
170pub enum BackendOidcModeRuntimeError {
171 #[snafu(display("oidc: {source}"), context(false))]
172 Oidc {
173 source: securitydept_oidc_client::OidcError,
174 },
175
176 #[snafu(display("refresh_material: {source}"), context(false))]
177 RefreshMaterial {
178 source: super::refresh_material::RefreshMaterialError,
179 },
180
181 #[snafu(display("redirect_uri: {source}"), context(false))]
182 RedirectUri {
183 source: super::redirect::BackendOidcModeRedirectUriError,
184 },
185
186 #[snafu(display("metadata_store: {source}"), context(false))]
187 MetadataStore {
188 source: super::metadata_redemption::PendingAuthStateMetadataRedemptionStoreError,
189 },
190
191 #[snafu(display("config: {message}"))]
192 Config { message: String },
193}
194
195pub type BackendOidcModeRuntimeResult<T> = Result<T, BackendOidcModeRuntimeError>;
196
197impl ToErrorPresentation for BackendOidcModeRuntimeError {
198 fn to_error_presentation(&self) -> ErrorPresentation {
199 match self {
200 Self::Oidc { source } => source.to_error_presentation(),
201 Self::Config { .. } => ErrorPresentation::new(
202 "backend_oidc_mode_config_invalid",
203 "Backend-oidc mode runtime is misconfigured.",
204 UserRecovery::ContactSupport,
205 ),
206 Self::RefreshMaterial { .. } => ErrorPresentation::new(
207 "backend_oidc_mode_refresh_material_invalid",
208 "The sign-in state is no longer valid. Sign in again.",
209 UserRecovery::Reauthenticate,
210 ),
211 Self::RedirectUri { .. } => ErrorPresentation::new(
212 "backend_oidc_mode_redirect_uri_invalid",
213 "The redirect URL is invalid.",
214 UserRecovery::RestartFlow,
215 ),
216 Self::MetadataStore { .. } => ErrorPresentation::new(
217 "backend_oidc_mode_metadata_unavailable",
218 "Authentication metadata is temporarily unavailable.",
219 UserRecovery::Retry,
220 ),
221 }
222 }
223}
224
225impl ToHttpStatus for BackendOidcModeRuntimeError {
226 fn to_http_status(&self) -> StatusCode {
227 match self {
228 Self::Oidc { source } => source.to_http_status(),
229 Self::Config { .. }
230 | Self::RefreshMaterial { .. }
231 | Self::RedirectUri { .. }
232 | Self::MetadataStore { .. } => StatusCode::INTERNAL_SERVER_ERROR,
233 }
234 }
235}
236
237impl<MC> BackendOidcModeRuntimeConfig<MC>
242where
243 MC: PendingAuthStateMetadataRedemptionConfig,
244{
245 pub fn validate(&self) -> BackendOidcModeRuntimeResult<()> {
251 if let PostAuthRedirectPolicy::Resolved { ref config } = self.post_auth_redirect {
252 config.validate_as_uri_reference()?;
253 }
254
255 Ok(())
256 }
257}
258
259impl<MS> BackendOidcModeRuntime<MS>
264where
265 MS: PendingAuthStateMetadataRedemptionStore,
266{
267 pub fn from_config(
269 config: BackendOidcModeRuntimeConfig<MS::Config>,
270 ) -> BackendOidcModeRuntimeResult<Self> {
271 let refresh_material_protector: Arc<dyn RefreshMaterialProtector> = match &config
274 .refresh_material_protection
275 {
276 RefreshMaterialProtection::Sealed { master_key } => {
277 Arc::new(AeadRefreshMaterialProtector::from_master_key(master_key)?)
278 }
279 RefreshMaterialProtection::Passthrough => Arc::new(PassthroughRefreshMaterialProtector),
280 };
281
282 let metadata_delivery_kind = config.metadata_delivery.kind();
285 let metadata_redemption_store = match &config.metadata_delivery {
286 MetadataDelivery::Redemption { config } => Some(MS::from_config(config)?),
287 MetadataDelivery::None => None,
288 };
289
290 let redirect_uri_resolver = match config.post_auth_redirect {
293 PostAuthRedirectPolicy::Resolved { ref config } => {
294 config.validate_as_uri_reference()?;
295 Some(BackendOidcModeRedirectUriResolver::from_config(
296 config.clone(),
297 ))
298 }
299 PostAuthRedirectPolicy::CallerValidated => None,
300 };
301
302 Ok(Self {
303 refresh_material_protector,
304 redirect_uri_resolver,
305 metadata_redemption_store,
306 metadata_delivery_kind,
307 })
308 }
309
310 pub async fn from_resolved_config<PS>(
331 resolved: Option<&ResolvedBackendOidcModeConfig<PS::Config, MS::Config>>,
332 ) -> BackendOidcModeRuntimeResult<(Self, Option<Arc<OidcClient<PS>>>)>
333 where
334 PS: PendingOauthStore,
335 PS::Config: Clone,
336 MS::Config: Clone,
337 {
338 let (runtime_config, oidc_client) = match resolved {
339 Some(r) => {
340 let client = OidcClient::<PS>::from_config(r.oidc_client.clone()).await?;
341 (r.runtime.clone(), Some(Arc::new(client)))
342 }
343 None => (BackendOidcModeRuntimeConfig::default(), None),
344 };
345
346 let runtime = Self::from_config(runtime_config)?;
347 Ok((runtime, oidc_client))
348 }
349
350 pub fn seal_refresh_token(
355 &self,
356 refresh_token: &str,
357 ) -> BackendOidcModeRuntimeResult<SealedRefreshMaterial> {
358 self.refresh_material_protector
359 .seal(refresh_token)
360 .map_err(Into::into)
361 }
362
363 pub fn unseal_refresh_token(
364 &self,
365 material: &SealedRefreshMaterial,
366 ) -> BackendOidcModeRuntimeResult<String> {
367 self.refresh_material_protector
368 .unseal(material)
369 .map_err(Into::into)
370 }
371
372 fn resolve_post_auth_redirect_uri(
373 &self,
374 requested: Option<&str>,
375 external_base_url: &Url,
376 ) -> BackendOidcModeRuntimeResult<Option<Url>> {
377 match &self.redirect_uri_resolver {
378 Some(resolver) => resolver
379 .resolve_redirect_uri(requested, external_base_url)
380 .map(Some)
381 .map_err(Into::into),
382 None => Ok(None), }
384 }
385
386 pub fn auth_state_snapshot_from_code_callback(
391 &self,
392 result: &OidcCodeCallbackResult,
393 options: &BackendOidcModeAuthStateOptions,
394 ) -> BackendOidcModeRuntimeResult<AuthStateSnapshot> {
395 let mut kind_history = Vec::new();
396 push_kind_history(
397 &mut kind_history,
398 &AuthenticationSourceKind::OidcAuthorizationCode,
399 );
400
401 let extracted = extract_principal_from_code_callback(result);
402
403 Ok(AuthStateSnapshot {
404 tokens: AuthTokenSnapshot {
405 access_token: result.access_token.clone(),
406 id_token: result.id_token.clone(),
407 refresh_material: seal_optional_refresh_material(
408 self,
409 result.refresh_token.as_deref(),
410 )?,
411 access_token_expires_at: result.access_token_expiration,
412 },
413 metadata: AuthStateMetadataSnapshot {
414 principal: Some(into_authenticated_principal(extracted)),
415 source: AuthenticationSource {
416 kind: AuthenticationSourceKind::OidcAuthorizationCode,
417 provider_id: options.source_provider_id.clone(),
418 issuer: Some(result.id_token_claims.issuer().url().to_string()),
419 kind_history,
420 attributes: options.source_attributes.clone(),
421 },
422 attributes: options.metadata_attributes.clone(),
423 },
424 })
425 }
426
427 pub fn auth_state_metadata_delta_from_refresh_result(
428 current_metadata: Option<&CurrentAuthStateMetadataSnapshotPartial>,
429 result: &OidcRefreshTokenResult,
430 ) -> AuthStateMetadataDelta {
431 let principal =
432 extract_principal_from_refresh_result(result).map(into_authenticated_principal);
433
434 AuthStateMetadataDelta {
435 principal,
436 source: Some(refreshed_source(
437 current_metadata.and_then(|m| m.source.as_ref()),
438 result,
439 )),
440 ..Default::default()
441 }
442 }
443
444 pub async fn authorize_code_flow<PS>(
455 &self,
456 oidc_client: &OidcClient<PS>,
457 external_base_url: &Url,
458 requested_post_auth_redirect_uri: Option<&str>,
459 redirect_url_override: Option<&str>,
460 ) -> BackendOidcModeRuntimeResult<OidcCodeFlowAuthorizationRequest>
461 where
462 PS: PendingOauthStore,
463 {
464 if requested_post_auth_redirect_uri.is_some() {
470 let _ = self.resolve_post_auth_redirect_uri(
471 requested_post_auth_redirect_uri,
472 external_base_url,
473 )?;
474 }
475
476 let extra_data = requested_post_auth_redirect_uri.map(|uri| {
477 json!({
478 PENDING_POST_AUTH_REDIRECT_URI_KEY: uri,
479 })
480 });
481
482 let request = oidc_client
483 .handle_code_authorize_with_redirect_override_and_extra_data(
484 external_base_url,
485 redirect_url_override,
486 extra_data,
487 )
488 .await?;
489
490 Ok(request)
491 }
492
493 pub async fn handle_code_callback<PS>(
495 &self,
496 oidc_client: &OidcClient<PS>,
497 search_params: OidcCodeCallbackSearchParams,
498 external_base_url: &Url,
499 auth_state_options: &BackendOidcModeAuthStateOptions,
500 redirect_url_override: Option<&str>,
501 ) -> BackendOidcModeRuntimeResult<BackendOidcModeCodeCallbackResult>
502 where
503 PS: PendingOauthStore,
504 {
505 let result = oidc_client
506 .handle_code_callback_with_redirect_override(
507 search_params,
508 external_base_url,
509 redirect_url_override,
510 )
511 .await?;
512
513 let post_auth_redirect_uri = if self.redirect_uri_resolver.is_some() {
515 self.resolve_post_auth_redirect_uri(
516 callback_post_auth_redirect_uri(&result).as_deref(),
517 external_base_url,
518 )?
519 } else {
520 None
521 };
522
523 let auth_state_snapshot =
524 self.auth_state_snapshot_from_code_callback(&result, auth_state_options)?;
525
526 let metadata_redemption_id = self.issue_metadata_snapshot(&auth_state_snapshot)?;
528
529 let response_body = BackendOidcModeCallbackReturns::from_snapshot(
530 &auth_state_snapshot.tokens,
531 metadata_redemption_id,
532 );
533
534 Ok(BackendOidcModeCodeCallbackResult {
535 post_auth_redirect_uri,
536 auth_state_snapshot,
537 response_body,
538 })
539 }
540
541 pub async fn handle_token_refresh<PS>(
543 &self,
544 oidc_client: &OidcClient<PS>,
545 payload: &BackendOidcModeRefreshPayload,
546 external_base_url: &Url,
547 ) -> BackendOidcModeRuntimeResult<BackendOidcModeTokenRefreshResult>
548 where
549 PS: PendingOauthStore,
550 {
551 let post_auth_redirect_uri = if self.redirect_uri_resolver.is_some() {
553 self.resolve_post_auth_redirect_uri(
554 payload.post_auth_redirect_uri.as_deref(),
555 external_base_url,
556 )?
557 } else {
558 None
559 };
560
561 let refresh_token = self.unseal_refresh_token(&payload.refresh_material)?;
563
564 let refresh_result = oidc_client
565 .handle_token_refresh(refresh_token, payload.id_token.clone())
566 .await?;
567
568 let refresh_material_delta = refresh_result
570 .refresh_token
571 .as_deref()
572 .map(|value| self.seal_refresh_token(value))
573 .transpose()?;
574
575 let token_delta = AuthTokenDelta {
576 access_token: refresh_result.access_token.clone(),
577 id_token: refresh_result.id_token.clone(),
578 refresh_material: refresh_material_delta,
579 access_token_expires_at: refresh_result.access_token_expiration,
580 };
581
582 let metadata_delta = Self::auth_state_metadata_delta_from_refresh_result(
583 payload.current_metadata_snapshot.as_ref(),
584 &refresh_result,
585 );
586
587 let metadata_redemption_id = self.issue_metadata_delta(&metadata_delta)?;
589
590 let response_body =
591 BackendOidcModeRefreshReturns::from_delta(&token_delta, metadata_redemption_id);
592
593 Ok(BackendOidcModeTokenRefreshResult {
594 post_auth_redirect_uri,
595 auth_state_delta: AuthStateDelta {
596 tokens: token_delta,
597 metadata: metadata_delta,
598 },
599 response_body,
600 })
601 }
602
603 pub async fn handle_code_callback_inline<PS>(
616 &self,
617 oidc_client: &OidcClient<PS>,
618 search_params: OidcCodeCallbackSearchParams,
619 external_base_url: &Url,
620 auth_state_options: &BackendOidcModeAuthStateOptions,
621 redirect_url_override: Option<&str>,
622 ) -> BackendOidcModeRuntimeResult<BackendOidcModeCodeCallbackResult>
623 where
624 PS: PendingOauthStore,
625 {
626 let result = oidc_client
627 .handle_code_callback_with_redirect_override(
628 search_params,
629 external_base_url,
630 redirect_url_override,
631 )
632 .await?;
633
634 let auth_state_snapshot =
635 self.auth_state_snapshot_from_code_callback(&result, auth_state_options)?;
636
637 let response_body = BackendOidcModeCallbackReturns::from_snapshot_with_inline_metadata(
639 &auth_state_snapshot.tokens,
640 auth_state_snapshot.metadata.clone(),
641 );
642
643 Ok(BackendOidcModeCodeCallbackResult {
644 post_auth_redirect_uri: None,
645 auth_state_snapshot,
646 response_body,
647 })
648 }
649
650 pub async fn handle_token_refresh_inline<PS>(
663 &self,
664 oidc_client: &OidcClient<PS>,
665 payload: &BackendOidcModeRefreshPayload,
666 ) -> BackendOidcModeRuntimeResult<BackendOidcModeTokenRefreshResult>
667 where
668 PS: PendingOauthStore,
669 {
670 let refresh_token = self.unseal_refresh_token(&payload.refresh_material)?;
672
673 let refresh_result = oidc_client
674 .handle_token_refresh(refresh_token, payload.id_token.clone())
675 .await?;
676
677 let refresh_material_delta = refresh_result
679 .refresh_token
680 .as_deref()
681 .map(|value| self.seal_refresh_token(value))
682 .transpose()?;
683
684 let token_delta = AuthTokenDelta {
685 access_token: refresh_result.access_token.clone(),
686 id_token: refresh_result.id_token.clone(),
687 refresh_material: refresh_material_delta,
688 access_token_expires_at: refresh_result.access_token_expiration,
689 };
690
691 let metadata_delta = Self::auth_state_metadata_delta_from_refresh_result(
692 payload.current_metadata_snapshot.as_ref(),
693 &refresh_result,
694 );
695
696 let response_body = BackendOidcModeRefreshReturns::from_delta_with_inline_metadata(
698 &token_delta,
699 metadata_delta.clone(),
700 );
701
702 Ok(BackendOidcModeTokenRefreshResult {
703 post_auth_redirect_uri: None,
704 auth_state_delta: AuthStateDelta {
705 tokens: token_delta,
706 metadata: metadata_delta,
707 },
708 response_body,
709 })
710 }
711
712 pub async fn redeem_metadata(
714 &self,
715 payload: &super::transport::BackendOidcModeMetadataRedemptionRequest,
716 ) -> BackendOidcModeRuntimeResult<
717 Option<super::transport::BackendOidcModeMetadataRedemptionResponse>,
718 > {
719 let store = self.metadata_redemption_store.as_ref().ok_or_else(|| {
720 BackendOidcModeRuntimeError::Config {
721 message: "metadata redemption is not enabled in this configuration".to_string(),
722 }
723 })?;
724
725 let metadata = store.redeem(&payload.metadata_redemption_id, Utc::now())?;
726
727 Ok(metadata
728 .map(|m| super::transport::BackendOidcModeMetadataRedemptionResponse { metadata: m }))
729 }
730
731 fn issue_metadata_snapshot(
736 &self,
737 snapshot: &AuthStateSnapshot,
738 ) -> BackendOidcModeRuntimeResult<Option<MetadataRedemptionId>> {
739 match (
740 &self.metadata_delivery_kind,
741 &self.metadata_redemption_store,
742 ) {
743 (MetadataDeliveryKind::Redemption, Some(store)) => {
744 let ticket = store.issue(
745 PendingAuthStateMetadataRedemptionPayload::Snapshot(snapshot.metadata.clone()),
746 Utc::now(),
747 )?;
748 Ok(Some(ticket.id))
749 }
750 _ => Ok(None),
751 }
752 }
753
754 fn issue_metadata_delta(
755 &self,
756 delta: &AuthStateMetadataDelta,
757 ) -> BackendOidcModeRuntimeResult<Option<MetadataRedemptionId>> {
758 match (
759 &self.metadata_delivery_kind,
760 &self.metadata_redemption_store,
761 ) {
762 (MetadataDeliveryKind::Redemption, Some(store)) if delta.is_empty() => {
763 let ticket = store.issue(
764 PendingAuthStateMetadataRedemptionPayload::Delta(delta.clone()),
765 Utc::now(),
766 )?;
767 Ok(Some(ticket.id))
768 }
769 _ => Ok(None),
770 }
771 }
772}
773
774fn refreshed_source(
779 current_source: Option<&crate::models::CurrentAuthenticationSourcePartial>,
780 result: &OidcRefreshTokenResult,
781) -> AuthenticationSource {
782 let mut source = AuthenticationSource {
783 kind: AuthenticationSourceKind::RefreshToken,
784 provider_id: current_source.and_then(|s| s.provider_id.clone()),
785 issuer: current_source.and_then(|s| s.issuer.clone()),
786 kind_history: current_source
787 .and_then(|s| s.kind_history.as_ref())
788 .cloned()
789 .unwrap_or_default(),
790 attributes: current_source
791 .map(|s| s.attributes.clone())
792 .unwrap_or_default(),
793 };
794 push_kind_history(
795 &mut source.kind_history,
796 &AuthenticationSourceKind::RefreshToken,
797 );
798
799 if let Some(issuer) = extract_issuer_from_refresh_result(result) {
800 source.issuer = Some(issuer);
801 }
802
803 source
804}
805
806fn into_authenticated_principal(extracted: OidcExtractedPrincipal) -> AuthenticatedPrincipal {
807 extracted
808}
809
810fn push_kind_history(history: &mut Vec<AuthenticationSourceKind>, kind: &AuthenticationSourceKind) {
811 if history.last() != Some(kind) {
812 history.push(kind.clone());
813 }
814}
815
816fn seal_optional_refresh_material<MS>(
817 runtime: &BackendOidcModeRuntime<MS>,
818 refresh_token: Option<&str>,
819) -> Result<Option<SealedRefreshMaterial>, BackendOidcModeRuntimeError>
820where
821 MS: PendingAuthStateMetadataRedemptionStore,
822{
823 refresh_token
824 .map(|value| runtime.seal_refresh_token(value))
825 .transpose()
826}
827
828fn callback_post_auth_redirect_uri(result: &OidcCodeCallbackResult) -> Option<String> {
829 result
830 .pending_extra_data
831 .as_ref()
832 .and_then(|value| value.get(PENDING_POST_AUTH_REDIRECT_URI_KEY))
833 .and_then(|value| value.as_str())
834 .map(ToOwned::to_owned)
835}
836
837#[cfg(test)]
838mod tests {
839 use super::{push_kind_history, refreshed_source};
840 use crate::models::{
841 AuthStateMetadataDelta, AuthenticationSourceKind, CurrentAuthenticationSourcePartial,
842 };
843
844 #[test]
845 fn kind_history_appends_new_kinds() {
846 let mut history = Vec::new();
847
848 push_kind_history(
849 &mut history,
850 &AuthenticationSourceKind::OidcAuthorizationCode,
851 );
852 push_kind_history(&mut history, &AuthenticationSourceKind::RefreshToken);
853
854 assert_eq!(
855 history,
856 vec![
857 AuthenticationSourceKind::OidcAuthorizationCode,
858 AuthenticationSourceKind::RefreshToken
859 ]
860 );
861 }
862
863 #[test]
864 fn kind_history_merges_same_top_kind() {
865 let mut history = vec![AuthenticationSourceKind::RefreshToken];
866
867 push_kind_history(&mut history, &AuthenticationSourceKind::RefreshToken);
868
869 assert_eq!(history, vec![AuthenticationSourceKind::RefreshToken]);
870 }
871
872 #[test]
873 fn metadata_delta_is_generated_without_previous_snapshot() {
874 let delta: AuthStateMetadataDelta = AuthStateMetadataDelta {
875 source: Some(refreshed_source(None, &mock_refresh_result())),
876 ..Default::default()
877 };
878
879 assert_eq!(
880 delta.source.as_ref().map(|source| &source.kind),
881 Some(&AuthenticationSourceKind::RefreshToken)
882 );
883 assert_eq!(
884 delta.source.as_ref().map(|source| &source.kind_history),
885 Some(&vec![AuthenticationSourceKind::RefreshToken])
886 );
887 }
888
889 #[test]
890 fn refreshed_source_preserves_partial_source_fields() {
891 let source = refreshed_source(
892 Some(&CurrentAuthenticationSourcePartial {
893 provider_id: Some("primary".to_string()),
894 issuer: Some("https://issuer.example.com".to_string()),
895 kind_history: Some(vec![AuthenticationSourceKind::OidcAuthorizationCode]),
896 ..Default::default()
897 }),
898 &mock_refresh_result(),
899 );
900
901 assert_eq!(source.provider_id.as_deref(), Some("primary"));
902 assert_eq!(source.issuer.as_deref(), Some("https://issuer.example.com"));
903 assert_eq!(
904 source.kind_history,
905 vec![
906 AuthenticationSourceKind::OidcAuthorizationCode,
907 AuthenticationSourceKind::RefreshToken
908 ]
909 );
910 }
911
912 fn mock_refresh_result() -> securitydept_oidc_client::OidcRefreshTokenResult {
913 securitydept_oidc_client::OidcRefreshTokenResult {
914 access_token: "access-token".to_string(),
915 access_token_expiration: None,
916 id_token: None,
917 refresh_token: None,
918 id_token_claims: None,
919 user_info_claims: None,
920 claims_check_result: None,
921 }
922 }
923}