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