Skip to main content

squigit_auth/auth/
credentials.rs

1// Copyright 2026 a7mddra
2// SPDX-License-Identifier: Apache-2.0
3
4use 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}