1use crate::configuration::tokens::insecure_validate_token_exp;
30#[cfg(feature = "tracing-config")]
31use crate::tracing_configuration::TracingConfiguration;
32use derive_builder::Builder;
33use std::{env, path::PathBuf};
34use tokio_util::sync::CancellationToken;
35
36#[cfg(feature = "python")]
37use pyo3::prelude::*;
38
39use self::{
40 secrets::{Credential, Secrets, TokenPayload},
41 settings::Settings,
42};
43
44mod error;
45mod oidc;
46mod pkce;
47#[cfg(feature = "python")]
48mod py;
49mod secrets;
50mod settings;
51mod tokens;
52
53pub use error::{LoadError, TokenError};
54#[cfg(feature = "python")]
55pub(crate) use py::*;
56pub use secrets::{DEFAULT_SECRETS_PATH, SECRETS_PATH_VAR, SECRETS_READ_ONLY_VAR};
57pub use settings::{AuthServer, DEFAULT_SETTINGS_PATH, SETTINGS_PATH_VAR};
58pub use tokens::{
59 ClientCredentials, ExternallyManaged, OAuthGrant, OAuthSession, PkceFlow, RefreshFunction,
60 RefreshToken, TokenDispatcher, TokenRefresher,
61};
62
63pub const DEFAULT_PROFILE_NAME: &str = "default";
65pub const PROFILE_NAME_VAR: &str = "QCS_PROFILE_NAME";
67fn env_or_default_profile_name() -> String {
68 env::var(PROFILE_NAME_VAR).unwrap_or_else(|_| DEFAULT_PROFILE_NAME.to_string())
69}
70
71pub const DEFAULT_API_URL: &str = "https://api.qcs.rigetti.com";
73pub const API_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_API_URL";
75fn env_or_default_api_url() -> String {
76 env::var(API_URL_VAR).unwrap_or_else(|_| DEFAULT_API_URL.to_string())
77}
78
79pub const DEFAULT_GRPC_API_URL: &str = "https://grpc.qcs.rigetti.com";
81pub const GRPC_API_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_GRPC_URL";
83fn env_or_default_grpc_url() -> String {
84 env::var(GRPC_API_URL_VAR).unwrap_or_else(|_| DEFAULT_GRPC_API_URL.to_string())
85}
86
87pub const DEFAULT_QVM_URL: &str = "http://127.0.0.1:5000";
89pub const QVM_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_QVM_URL";
91fn env_or_default_qvm_url() -> String {
92 env::var(QVM_URL_VAR).unwrap_or_else(|_| DEFAULT_QVM_URL.to_string())
93}
94
95pub const DEFAULT_QUILC_URL: &str = "tcp://127.0.0.1:5555";
97pub const QUILC_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_QUILC_URL";
99fn env_or_default_quilc_url() -> String {
100 env::var(QUILC_URL_VAR).unwrap_or_else(|_| DEFAULT_QUILC_URL.to_string())
101}
102
103#[derive(Clone, Debug, Builder)]
116#[cfg_attr(feature = "python", pyclass)]
117pub struct ClientConfiguration {
118 #[builder(private, default = "env_or_default_profile_name()")]
119 profile: String,
120
121 #[doc = "The URL for the QCS REST API."]
122 #[builder(default = "env_or_default_api_url()")]
123 api_url: String,
124
125 #[doc = "The URL for the QCS gRPC API."]
126 #[builder(default = "env_or_default_grpc_url()")]
127 grpc_api_url: String,
128
129 #[doc = "The URL of the quilc server."]
130 #[builder(default = "env_or_default_quilc_url()")]
131 quilc_url: String,
132
133 #[doc = "The URL of the QVM server."]
134 #[builder(default = "env_or_default_qvm_url()")]
135 qvm_url: String,
136
137 #[builder(default, setter(custom))]
142 pub(crate) oauth_session: Option<TokenDispatcher>,
143
144 #[builder(private, default = "ConfigSource::Builder")]
145 source: ConfigSource,
146
147 #[cfg(feature = "tracing-config")]
149 #[builder(default)]
150 tracing_configuration: Option<TracingConfiguration>,
151}
152
153impl ClientConfigurationBuilder {
154 pub fn oauth_session(&mut self, oauth_session: Option<OAuthSession>) -> &mut Self {
159 self.oauth_session = Some(oauth_session.map(Into::into));
160 self
161 }
162}
163
164struct ConfigurationContext {
166 builder: ClientConfigurationBuilder,
167 auth_server: AuthServer,
168 credential: Option<Credential>,
169}
170
171impl ConfigurationContext {
172 fn from_profile(profile_name: Option<String>) -> Result<Self, LoadError> {
173 #[cfg(feature = "tracing-config")]
174 match profile_name.as_ref() {
175 None => tracing::debug!("loading default QCS profile"),
176 Some(profile) => tracing::debug!("loading QCS profile {profile}"),
177 }
178 let settings = Settings::load()?;
179 let secrets = Secrets::load()?;
180 Self::from_sources(settings, secrets, profile_name)
181 }
182
183 fn from_sources(
184 settings: Settings,
185 mut secrets: Secrets,
186 profile_name: Option<String>,
187 ) -> Result<Self, LoadError> {
188 let Settings {
189 default_profile_name,
190 mut profiles,
191 mut auth_servers,
192 file_path: settings_path,
193 } = settings;
194 let profile_name = profile_name
195 .or_else(|| env::var(PROFILE_NAME_VAR).ok())
196 .unwrap_or(default_profile_name);
197 let profile = profiles
198 .remove(&profile_name)
199 .ok_or(LoadError::ProfileNotFound(profile_name.clone()))?;
200 let auth_server = auth_servers
201 .remove(&profile.auth_server_name)
202 .ok_or_else(|| LoadError::AuthServerNotFound(profile.auth_server_name.clone()))?;
203
204 let secrets_path = secrets.file_path;
205 let credential = secrets.credentials.remove(&profile.credentials_name);
206
207 let api_url = env::var(API_URL_VAR).unwrap_or(profile.api_url);
208 let quilc_url = env::var(QUILC_URL_VAR).unwrap_or(profile.applications.pyquil.quilc_url);
209 let qvm_url = env::var(QVM_URL_VAR).unwrap_or(profile.applications.pyquil.qvm_url);
210 let grpc_api_url = env::var(GRPC_API_URL_VAR).unwrap_or(profile.grpc_api_url);
211
212 #[cfg(feature = "tracing-config")]
213 let tracing_configuration =
214 TracingConfiguration::from_env().map_err(LoadError::TracingFilterParseError)?;
215
216 let source = match (settings_path, secrets_path) {
217 (Some(settings_path), Some(secrets_path)) => ConfigSource::File {
218 settings_path,
219 secrets_path,
220 },
221 _ => ConfigSource::Default,
222 };
223
224 let mut builder = ClientConfiguration::builder();
225 builder
226 .profile(profile_name)
227 .source(source)
228 .api_url(api_url)
229 .quilc_url(quilc_url)
230 .qvm_url(qvm_url)
231 .grpc_api_url(grpc_api_url);
232
233 #[cfg(feature = "tracing-config")]
234 {
235 builder.tracing_configuration(tracing_configuration);
236 }
237
238 Ok(Self {
239 builder,
240 auth_server,
241 credential,
242 })
243 }
244}
245
246fn credential_to_oauth_session(
247 credential: Option<Credential>,
248 auth_server: AuthServer,
249) -> Option<OAuthSession> {
250 match credential {
251 Some(Credential {
252 token_payload:
253 Some(TokenPayload {
254 access_token,
255 refresh_token,
256 ..
257 }),
258 }) => Some(OAuthSession::new(
259 OAuthGrant::RefreshToken(RefreshToken::new(refresh_token.unwrap_or_default())),
260 auth_server,
261 access_token,
262 )),
263 _ => None,
264 }
265}
266
267impl ClientConfiguration {
268 #[cfg(test)]
269 fn new(
270 settings: Settings,
271 secrets: Secrets,
272 profile_name: Option<String>,
273 ) -> Result<Self, LoadError> {
274 let ConfigurationContext {
275 mut builder,
276 auth_server,
277 credential,
278 } = ConfigurationContext::from_sources(settings, secrets, profile_name)?;
279 let oauth_session = credential_to_oauth_session(credential, auth_server);
280 Ok(builder.oauth_session(oauth_session).build()?)
281 }
282
283 pub fn load_default() -> Result<Self, LoadError> {
289 let base_config = Self::load(None)?;
290 Ok(base_config)
291 }
292
293 pub fn load_profile(profile_name: String) -> Result<Self, LoadError> {
300 Self::load(Some(profile_name))
301 }
302
303 pub async fn load_with_login(
312 cancel_token: CancellationToken,
313 profile_name: Option<String>,
314 ) -> Result<Self, LoadError> {
315 let ConfigurationContext {
316 mut builder,
317 auth_server,
318 credential,
319 } = ConfigurationContext::from_profile(profile_name)?;
320
321 if let Some(Credential {
323 token_payload:
324 Some(TokenPayload {
325 access_token,
326 refresh_token,
327 ..
328 }),
329 }) = credential
330 {
331 if let Some(access_token) = access_token {
333 if insecure_validate_token_exp(&access_token).is_ok() {
334 let refresh_token = RefreshToken::new(refresh_token.unwrap_or_default());
335 let oauth_session = OAuthSession::new(
336 OAuthGrant::RefreshToken(refresh_token),
337 auth_server,
338 Some(access_token),
339 );
340 return Ok(builder.oauth_session(Some(oauth_session)).build()?);
341 }
342 }
343
344 if let Some(refresh_token) = refresh_token {
346 if !refresh_token.is_empty() {
347 let mut refresh_token = RefreshToken::new(refresh_token);
348
349 if let Ok(access_token) = refresh_token.request_access_token(&auth_server).await
351 {
352 let oauth_session = OAuthSession::new(
353 OAuthGrant::RefreshToken(refresh_token),
354 auth_server,
355 Some(access_token),
356 );
357
358 return Ok(builder.oauth_session(Some(oauth_session)).build()?);
359 }
360 }
361 }
362 }
363
364 let pkce_flow = PkceFlow::new_login_flow(cancel_token, &auth_server).await?;
366 let access_token = pkce_flow.access_token.clone();
367 let oauth_session =
368 OAuthSession::from_pkce_flow(pkce_flow, auth_server, Some(access_token));
369
370 Ok(builder.oauth_session(Some(oauth_session)).build()?)
371 }
372
373 fn load(profile_name: Option<String>) -> Result<Self, LoadError> {
381 let ConfigurationContext {
382 mut builder,
383 auth_server,
384 credential,
385 } = ConfigurationContext::from_profile(profile_name)?;
386 let oauth_session = credential_to_oauth_session(credential, auth_server);
387 Ok(builder.oauth_session(oauth_session).build()?)
388 }
389
390 #[must_use]
392 pub fn builder() -> ClientConfigurationBuilder {
393 ClientConfigurationBuilder::default()
394 }
395
396 #[must_use]
398 pub fn profile(&self) -> &str {
399 &self.profile
400 }
401
402 #[must_use]
404 pub fn api_url(&self) -> &str {
405 &self.api_url
406 }
407
408 #[must_use]
410 pub fn grpc_api_url(&self) -> &str {
411 &self.grpc_api_url
412 }
413
414 #[must_use]
416 pub fn quilc_url(&self) -> &str {
417 &self.quilc_url
418 }
419
420 #[must_use]
422 pub fn qvm_url(&self) -> &str {
423 &self.qvm_url
424 }
425
426 #[cfg(feature = "tracing-config")]
428 #[must_use]
429 pub const fn tracing_configuration(&self) -> Option<&TracingConfiguration> {
430 self.tracing_configuration.as_ref()
431 }
432
433 #[must_use]
435 pub const fn source(&self) -> &ConfigSource {
436 &self.source
437 }
438
439 pub async fn oauth_session(&self) -> Result<OAuthSession, TokenError> {
447 Ok(self
448 .oauth_session
449 .as_ref()
450 .ok_or(TokenError::NoRefreshToken)?
451 .tokens()
452 .await)
453 }
454
455 pub async fn get_bearer_access_token(&self) -> Result<String, TokenError> {
461 let dispatcher = self
462 .oauth_session
463 .as_ref()
464 .ok_or_else(|| TokenError::NoCredentials)?;
465 match dispatcher.validate().await {
466 Ok(tokens) => Ok(tokens),
467 #[allow(unused_variables)]
468 Err(e) => {
469 #[cfg(feature = "tracing-config")]
470 tracing::debug!("Refreshing access token because current one is invalid: {e}");
471 dispatcher
472 .refresh(self.source(), self.profile())
473 .await
474 .map(|e| e.access_token().map(ToString::to_string))?
475 }
476 }
477 }
478
479 pub async fn refresh(&self) -> Result<OAuthSession, TokenError> {
485 self.oauth_session
486 .as_ref()
487 .ok_or(TokenError::NoRefreshToken)?
488 .refresh(self.source(), self.profile())
489 .await
490 }
491}
492
493#[derive(Clone, Debug)]
495pub enum ConfigSource {
496 Builder,
498 File {
500 settings_path: PathBuf,
502 secrets_path: PathBuf,
504 },
505 Default,
507}
508
509fn expand_path_from_env_or_default(
510 env_var_name: &str,
511 default: &str,
512) -> Result<PathBuf, LoadError> {
513 match env::var(env_var_name) {
514 Ok(path) => {
515 let expanded_path = shellexpand::env(&path).map_err(LoadError::from)?;
516 let path_buf: PathBuf = expanded_path.as_ref().into();
517 if !path_buf.exists() {
518 return Err(LoadError::Path {
519 path: path_buf,
520 message: format!("The given path does not exist: {path}"),
521 });
522 }
523 Ok(path_buf)
524 }
525 Err(env::VarError::NotPresent) => {
526 let expanded_path = shellexpand::tilde_with_context(default, || {
527 env::home_dir().map(|path| path.display().to_string())
528 });
529 let path_buf: PathBuf = expanded_path.as_ref().into();
530 if !path_buf.exists() {
531 return Err(LoadError::Path {
532 path: path_buf,
533 message: format!(
534 "Could not find a QCS configuration at the default path: {default}"
535 ),
536 });
537 }
538 Ok(path_buf)
539 }
540 Err(other_error) => Err(LoadError::EnvVar {
541 variable_name: env_var_name.to_string(),
542 message: other_error.to_string(),
543 }),
544 }
545}
546
547#[cfg(test)]
548mod test {
549 use jsonwebtoken::{encode, EncodingKey, Header};
550 use serde::Serialize;
551 use time::{Duration, OffsetDateTime};
552 use tokio_util::sync::CancellationToken;
553
554 use crate::configuration::{
555 expand_path_from_env_or_default, pkce::tests::PkceTestServerHarness, secrets::Secrets,
556 settings::Settings, AuthServer, ClientConfiguration, OAuthSession, RefreshToken,
557 API_URL_VAR, DEFAULT_QUILC_URL, GRPC_API_URL_VAR, QUILC_URL_VAR, QVM_URL_VAR,
558 SECRETS_PATH_VAR, SECRETS_READ_ONLY_VAR, SETTINGS_PATH_VAR,
559 };
560
561 use super::{
562 settings::QCS_DEFAULT_AUTH_ISSUER_PRODUCTION, tokens::ClientCredentials, TokenRefresher,
563 };
564
565 #[test]
566 fn expands_env_var() {
567 figment::Jail::expect_with(|jail| {
568 let dir = jail.create_dir("~/blah/blah/")?;
569 jail.create_file(dir.join("file.toml"), "")?;
570 jail.set_env("SOME_PATH", "blah/blah");
571 jail.set_env("SOME_VAR", "~/$SOME_PATH/file.toml");
572 let secrets_path = expand_path_from_env_or_default("SOME_VAR", "default").unwrap();
573 assert_eq!(secrets_path.to_str().unwrap(), "~/blah/blah/file.toml");
574
575 Ok(())
576 });
577 }
578
579 #[test]
580 fn uses_env_var_overrides() {
581 figment::Jail::expect_with(|jail| {
582 let quilc_url = "tcp://quilc:5555";
583 let qvm_url = "http://qvm:5000";
584 let grpc_url = "http://grpc:80";
585 let api_url = "http://api:80";
586
587 jail.set_env(QUILC_URL_VAR, quilc_url);
588 jail.set_env(QVM_URL_VAR, qvm_url);
589 jail.set_env(API_URL_VAR, api_url);
590 jail.set_env(GRPC_API_URL_VAR, grpc_url);
591
592 let config = ClientConfiguration::new(
593 Settings::default(),
594 Secrets::default(),
595 Some("default".to_string()),
596 )
597 .expect("Should be able to build default config.");
598
599 assert_eq!(config.quilc_url, quilc_url);
600 assert_eq!(config.qvm_url, qvm_url);
601 assert_eq!(config.grpc_api_url, grpc_url);
602
603 Ok(())
604 });
605 }
606
607 #[tokio::test]
608 async fn test_default_uses_env_var_overrides() {
609 figment::Jail::expect_with(|jail| {
610 let quilc_url = "quilc_url";
611 let qvm_url = "qvm_url";
612 let grpc_url = "grpc_url";
613 let api_url = "api_url";
614
615 jail.set_env(QUILC_URL_VAR, quilc_url);
616 jail.set_env(QVM_URL_VAR, qvm_url);
617 jail.set_env(GRPC_API_URL_VAR, grpc_url);
618 jail.set_env(API_URL_VAR, api_url);
619
620 let config = ClientConfiguration::load_default().unwrap();
621 assert_eq!(config.quilc_url, quilc_url);
622 assert_eq!(config.qvm_url, qvm_url);
623 assert_eq!(config.grpc_api_url, grpc_url);
624 assert_eq!(config.api_url, api_url);
625
626 Ok(())
627 });
628 }
629
630 #[test]
631 fn test_default_loads_settings_with_partial_profile_applications() {
632 figment::Jail::expect_with(|jail| {
633 let directory = jail.directory();
634 let settings_file_name = "settings.toml";
635 let settings_file_path = directory.join(settings_file_name);
636
637 let quilc_url_env_var = "env-var://quilc.url/after";
638
639 let settings_file_contents = r#"
640default_profile_name = "default"
641
642[profiles]
643[profiles.default]
644api_url = ""
645auth_server_name = "default"
646credentials_name = "default"
647applications = {}
648
649[auth_servers]
650[auth_servers.default]
651client_id = ""
652issuer = ""
653"#;
654 jail.create_file(settings_file_name, settings_file_contents)
655 .expect("should create test settings.toml");
656
657 jail.set_env(
658 "QCS_SETTINGS_FILE_PATH",
659 settings_file_path
660 .to_str()
661 .expect("settings file path should be a string"),
662 );
663
664 let config = ClientConfiguration::load_default().unwrap();
666 assert_eq!(config.quilc_url, DEFAULT_QUILC_URL);
667
668 jail.set_env("QCS_SETTINGS_APPLICATIONS_QUILC_URL", quilc_url_env_var);
669
670 let config = ClientConfiguration::load_default().unwrap();
672 assert_eq!(config.quilc_url, quilc_url_env_var);
673
674 Ok(())
675 });
676 }
677
678 #[test]
679 fn test_default_loads_settings_with_partial_profile_applications_pyquil() {
680 figment::Jail::expect_with(|jail| {
681 let directory = jail.directory();
682 let settings_file_name = "settings.toml";
683 let settings_file_path = directory.join(settings_file_name);
684
685 let quilc_url_settings_toml = "settings-toml://quilc.url";
686 let quilc_url_env_var = "env-var://quilc.url/after";
687
688 let settings_file_contents = format!(
689 r#"
690default_profile_name = "default"
691
692[profiles]
693[profiles.default]
694api_url = ""
695auth_server_name = "default"
696credentials_name = "default"
697applications.pyquil.quilc_url = "{quilc_url_settings_toml}"
698
699[auth_servers]
700[auth_servers.default]
701client_id = ""
702issuer = ""
703"#
704 );
705
706 jail.create_file(settings_file_name, &settings_file_contents)
707 .expect("should create test settings.toml");
708
709 jail.set_env(
710 "QCS_SETTINGS_FILE_PATH",
711 settings_file_path
712 .to_str()
713 .expect("settings file path should be a string"),
714 );
715
716 let config = ClientConfiguration::load_default().unwrap();
718 assert_eq!(config.quilc_url, quilc_url_settings_toml);
719
720 jail.set_env("QCS_SETTINGS_APPLICATIONS_QUILC_URL", quilc_url_env_var);
721
722 let config = ClientConfiguration::load_default().unwrap();
724 assert_eq!(config.quilc_url, quilc_url_env_var);
725
726 Ok(())
727 });
728 }
729
730 #[tokio::test]
731 async fn test_hydrate_access_token_on_load() {
732 let mut config = ClientConfiguration::builder().build().unwrap();
733 let access_token = "test_access_token";
734 figment::Jail::expect_with(|jail| {
735 let directory = jail.directory();
736 let settings_file_name = "settings.toml";
737 let settings_file_path = directory.join(settings_file_name);
738 let secrets_file_name = "secrets.toml";
739 let secrets_file_path = directory.join(secrets_file_name);
740
741 let settings_file_contents = r#"
742default_profile_name = "default"
743
744[profiles]
745[profiles.default]
746api_url = ""
747auth_server_name = "default"
748credentials_name = "default"
749
750[auth_servers]
751[auth_servers.default]
752client_id = ""
753issuer = ""
754"#;
755
756 let secrets_file_contents = format!(
757 r#"
758[credentials]
759[credentials.default]
760[credentials.default.token_payload]
761access_token = "{access_token}"
762expires_in = 3600
763id_token = "id_token"
764refresh_token = "refresh_token"
765scope = "offline_access openid profile email"
766token_type = "Bearer"
767"#
768 );
769
770 jail.create_file(settings_file_name, settings_file_contents)
771 .expect("should create test settings.toml");
772 jail.create_file(secrets_file_name, &secrets_file_contents)
773 .expect("should create test settings.toml");
774
775 jail.set_env(
776 "QCS_SETTINGS_FILE_PATH",
777 settings_file_path
778 .to_str()
779 .expect("settings file path should be a string"),
780 );
781 jail.set_env(
782 "QCS_SECRETS_FILE_PATH",
783 secrets_file_path
784 .to_str()
785 .expect("secrets file path should be a string"),
786 );
787
788 config = ClientConfiguration::load_default().unwrap();
789 Ok(())
790 });
791 assert_eq!(
792 config.get_access_token().await.unwrap(),
793 Some(access_token.to_string())
794 );
795 }
796
797 #[derive(Clone, Debug, Serialize)]
798 struct Claims {
799 exp: i64,
800 iss: String,
801 sub: String,
802 }
803
804 impl Default for Claims {
805 fn default() -> Self {
806 Self {
807 exp: 0,
808 iss: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
809 sub: "qcs@rigetti.com".to_string(),
810 }
811 }
812 }
813
814 impl Claims {
815 fn new_valid() -> Self {
816 Self {
817 exp: (OffsetDateTime::now_utc() + Duration::seconds(100)).unix_timestamp(),
818 ..Self::default()
819 }
820 }
821
822 fn new_expired() -> Self {
823 Self {
824 exp: (OffsetDateTime::now_utc() - Duration::seconds(100)).unix_timestamp(),
825 ..Self::default()
826 }
827 }
828
829 fn to_encoded(&self) -> String {
830 encode(&Header::default(), &self, &EncodingKey::from_secret(&[])).unwrap()
831 }
832 }
833
834 #[test]
835 fn test_valid_token() {
836 let valid_token = Claims::new_valid().to_encoded();
837 let tokens = OAuthSession::from_refresh_token(
838 RefreshToken::new(valid_token.clone()),
839 AuthServer::default(),
840 Some(valid_token.clone()),
841 );
842 assert_eq!(
843 tokens
844 .validate()
845 .expect("Token should not fail validation."),
846 valid_token
847 );
848 }
849
850 #[test]
851 fn test_expired_token() {
852 let invalid_token = Claims::new_expired().to_encoded();
853 let tokens = OAuthSession::from_refresh_token(
854 RefreshToken::new(invalid_token),
855 AuthServer::default(),
856 None,
857 );
858 assert!(tokens.validate().is_err());
859 }
860
861 #[test]
862 fn test_client_credentials_without_access_token() {
863 let tokens = OAuthSession::from_client_credentials(
864 ClientCredentials::new("client_id".to_string(), "client_secret".to_string()),
865 AuthServer::default(),
866 None,
867 );
868 assert!(tokens.validate().is_err());
869 }
870
871 #[tokio::test]
872 async fn test_session_is_present_with_empty_refresh_token_and_valid_access_token() {
873 let access_token = Claims::new_valid().to_encoded();
874 let mut config = ClientConfiguration::builder().build().unwrap();
875 figment::Jail::expect_with(|jail| {
876 let directory = jail.directory();
877 let settings_file_name = "settings.toml";
878 let settings_file_path = directory.join(settings_file_name);
879 let secrets_file_name = "secrets.toml";
880 let secrets_file_path = directory.join(secrets_file_name);
881
882 let settings_file_contents = r#"
883default_profile_name = "default"
884
885[profiles]
886[profiles.default]
887api_url = ""
888auth_server_name = "default"
889credentials_name = "default"
890
891[auth_servers]
892[auth_servers.default]
893client_id = ""
894issuer = ""
895"#;
896
897 let secrets_file_contents = format!(
899 r#"
900[credentials]
901[credentials.default]
902[credentials.default.token_payload]
903access_token = "{access_token}"
904expires_in = 3600
905id_token = "id_token"
906scope = "offline_access openid profile email"
907token_type = "Bearer"
908"#
909 );
910
911 jail.create_file(settings_file_name, settings_file_contents)
912 .expect("should create test settings.toml");
913 jail.create_file(secrets_file_name, &secrets_file_contents)
914 .expect("should create test secrets.toml");
915
916 jail.set_env(
917 "QCS_SETTINGS_FILE_PATH",
918 settings_file_path
919 .to_str()
920 .expect("settings file path should be a string"),
921 );
922 jail.set_env(
923 "QCS_SECRETS_FILE_PATH",
924 secrets_file_path
925 .to_str()
926 .expect("secrets file path should be a string"),
927 );
928
929 config = ClientConfiguration::load_default().unwrap();
930 Ok(())
931 });
932
933 assert_eq!(
934 config.get_bearer_access_token().await.unwrap(),
935 access_token.to_string()
936 );
937 }
938
939 #[test]
941 #[serial_test::serial(oauth2_test_server)]
942 fn test_pkce_flow_persists_token() {
943 let runtime = tokio::runtime::Runtime::new().expect("should create runtime");
946
947 let PkceTestServerHarness {
948 server,
949 client,
950 discovery: _,
951 redirect_port: _,
952 } = runtime.block_on(PkceTestServerHarness::new());
953
954 let client_id = client.client_id;
955 let issuer = server.issuer().to_string();
956
957 figment::Jail::expect_with(|jail| {
958 jail.set_env(SECRETS_READ_ONLY_VAR, "false");
961
962 let directory = jail.directory();
963 let settings_file_name = "settings.toml";
964 let settings_file_path = directory.join(settings_file_name);
965
966 let secrets_file_name = "secrets.toml";
967 let secrets_file_path = directory.join(secrets_file_name);
968
969 let settings_file_contents = format!(
970 r#"
971default_profile_name = "default"
972
973[profiles]
974[profiles.default]
975api_url = ""
976auth_server_name = "default"
977credentials_name = "default"
978
979[auth_servers]
980[auth_servers.default]
981client_id = "{client_id}"
982issuer = "{issuer}"
983"#
984 );
985
986 let secrets_file_contents = r#"
987[credentials]
988[credentials.default]
989[credentials.default.token_payload]
990access_token = ""
991"#;
992
993 jail.create_file(settings_file_name, &settings_file_contents)
994 .expect("should create test settings.toml");
995
996 jail.set_env(
997 SETTINGS_PATH_VAR,
998 settings_file_path
999 .to_str()
1000 .expect("settings file path should be a string"),
1001 );
1002
1003 jail.create_file(secrets_file_name, secrets_file_contents)
1004 .expect("should create test secrets.toml");
1005
1006 jail.set_env(
1007 SECRETS_PATH_VAR,
1008 secrets_file_path
1009 .to_str()
1010 .expect("secrets file path should be a string"),
1011 );
1012
1013 runtime.block_on(async {
1015 let cancel_token = CancellationToken::new();
1016 let configuration = ClientConfiguration::load_with_login(cancel_token, None)
1018 .await
1019 .expect("should load configuration");
1020 let oauth_session = configuration.refresh().await.expect("should refresh");
1021 let token = oauth_session.validate().expect("token should be valid");
1022
1023 let configuration =
1025 ClientConfiguration::load_default().expect("should load configuration");
1026
1027 let oauth_session = configuration
1028 .oauth_session()
1029 .await
1030 .expect("should get oauth session");
1031
1032 let token_payload = Secrets::load_from_path(&secrets_file_path)
1033 .expect("should load secrets")
1034 .credentials
1035 .remove("default")
1036 .expect("should get default credentials")
1037 .token_payload
1038 .expect("should get token payload");
1039
1040 assert_eq!(
1041 token,
1042 oauth_session.validate().expect("should contain token"),
1043 "session: {oauth_session:?}, token_payload: {token_payload:?}",
1044 );
1045 assert_eq!(
1046 token_payload.access_token,
1047 Some(token),
1048 "session: {oauth_session:?}, token_payload: {token_payload:?}"
1049 );
1050 assert_ne!(
1051 token_payload.refresh_token, None,
1052 "session: {oauth_session:?}, token_payload: {token_payload:?}"
1053 );
1054 });
1055
1056 Ok(())
1057 });
1058
1059 drop(server);
1060 }
1061}