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