squigit_auth/auth/
credentials.rs1use std::fs;
5use std::sync::Once;
6
7use serde::Deserialize;
8
9use crate::{ProfileError, Result};
10
11use super::AuthFlowSettings;
12
13static AUTH_MISSING_CREDENTIALS_LOG_ONCE: Once = Once::new();
14
15#[derive(Clone, Debug)]
16pub enum CredentialsSource {
17 Auto,
18 RawJson(String),
19}
20
21#[derive(Deserialize, Debug)]
22struct GoogleCredentials {
23 installed: Option<OAuthConfig>,
24 web: Option<OAuthConfig>,
25}
26
27#[derive(Deserialize, Debug, Clone)]
28pub(super) struct OAuthConfig {
29 pub(super) client_id: String,
30 #[serde(rename = "client_secret")]
31 pub(super) client_secret: Option<String>,
32 pub(super) auth_uri: String,
33 pub(super) token_uri: String,
34}
35
36fn missing_credentials_message() -> String {
37 "Google authentication credentials were not provided. In release mode, credentials must be supplied explicitly (e.g. via squigit-rs runtime secrets, CredentialsSource::RawJson, or SQUIGIT_GOOGLE_CREDENTIALS_JSON / SQUIGIT_GOOGLE_CREDENTIALS_PATH).".to_string()
38}
39
40fn load_google_credentials_raw(source: &CredentialsSource) -> Result<String> {
41 match source {
42 CredentialsSource::RawJson(raw) => Ok(raw.clone()),
43 CredentialsSource::Auto => {
44 if let Ok(raw) = std::env::var("SQUIGIT_GOOGLE_CREDENTIALS_JSON") {
45 if !raw.trim().is_empty() {
46 return Ok(raw);
47 }
48 }
49
50 if let Ok(path) = std::env::var("SQUIGIT_GOOGLE_CREDENTIALS_PATH") {
51 let trimmed = path.trim();
52 if !trimmed.is_empty() {
53 return fs::read_to_string(trimmed).map_err(|err| {
54 ProfileError::Auth(format!(
55 "Failed reading SQUIGIT_GOOGLE_CREDENTIALS_PATH: {}",
56 err
57 ))
58 });
59 }
60 }
61
62 let message = missing_credentials_message();
63 AUTH_MISSING_CREDENTIALS_LOG_ONCE.call_once(|| {
64 eprintln!("[auth] {}", message.replace('\n', "\n[auth] "));
65 });
66 Err(ProfileError::MissingCredentials(message))
67 }
68 }
69}
70
71fn is_placeholder_config(config: &OAuthConfig) -> bool {
72 config.client_id.contains("replace-me") || config.client_id.trim().is_empty()
73}
74
75pub(super) fn load_google_oauth_config(settings: &AuthFlowSettings) -> Result<OAuthConfig> {
76 let raw = load_google_credentials_raw(&settings.credentials_source)?;
77 let raw = raw.trim();
78 if raw.is_empty() {
79 let message = missing_credentials_message();
80 AUTH_MISSING_CREDENTIALS_LOG_ONCE.call_once(|| {
81 eprintln!("[auth] {}", message.replace('\n', "\n[auth] "));
82 });
83 return Err(ProfileError::MissingCredentials(message));
84 }
85
86 let wrapper: GoogleCredentials = serde_json::from_str(raw).map_err(|err| {
87 ProfileError::Auth(format!("Failed to parse Google OAuth credentials: {}", err))
88 })?;
89
90 let config = wrapper.installed.or(wrapper.web).ok_or_else(|| {
91 ProfileError::Auth(
92 "Invalid credentials.json: missing 'installed' or 'web' object".to_string(),
93 )
94 })?;
95
96 if is_placeholder_config(&config) {
97 let message = missing_credentials_message();
98 AUTH_MISSING_CREDENTIALS_LOG_ONCE.call_once(|| {
99 eprintln!("[auth] {}", message.replace('\n', "\n[auth] "));
100 });
101 return Err(ProfileError::MissingCredentials(message));
102 }
103
104 Ok(config)
105}
106
107pub fn validate_google_credentials(settings: &AuthFlowSettings) -> Result<()> {
108 load_google_oauth_config(settings).map(|_| ())
109}