aster_forge_external_auth/providers/
google.rs

1//! Google `OpenID` Connect provider driver.
2//!
3//! Google sign-in is implemented as a fixed-issuer wrapper around the generic OIDC driver. The
4//! wrapper prevents manual endpoint drift while still allowing tests to inject a loopback issuer for
5//! local mock servers.
6
7use async_trait::async_trait;
8
9use crate::Result;
10use crate::driver::{
11    ExternalAuthAuthorizationStart, ExternalAuthCallback, ExternalAuthProfile,
12    ExternalAuthProviderConfig, ExternalAuthProviderDescriptor, ExternalAuthProviderDriver,
13    ExternalAuthProviderTestResult,
14};
15use crate::types::{ExternalAuthProtocol, ExternalAuthProviderKind};
16
17use super::oidc::OidcProviderDriver;
18
19/// Google Accounts OIDC issuer advertised by the discovery document.
20const GOOGLE_ISSUER_URL: &str = "https://accounts.google.com";
21/// Minimal Google sign-in scopes. Google API / Drive access belongs to a
22/// separate OAuth authorization feature and should not be added here.
23const GOOGLE_DEFAULT_SCOPES: &str = "openid profile email";
24
25/// Dedicated Google sign-in provider backed by the generic OIDC driver.
26#[derive(Default)]
27pub struct GoogleProviderDriver;
28
29impl GoogleProviderDriver {
30    /// Creates a Google provider driver with the fixed Google Accounts issuer.
31    #[must_use]
32    pub fn new() -> Self {
33        Self
34    }
35}
36
37#[async_trait]
38impl ExternalAuthProviderDriver for GoogleProviderDriver {
39    fn kind(&self) -> ExternalAuthProviderKind {
40        ExternalAuthProviderKind::Google
41    }
42
43    fn descriptor(&self) -> ExternalAuthProviderDescriptor {
44        ExternalAuthProviderDescriptor {
45            kind: ExternalAuthProviderKind::Google,
46            protocol: ExternalAuthProtocol::Oidc,
47            display_name: "Google",
48            description: "Google OpenID Connect sign-in with fixed issuer and standard email_verified semantics.",
49            default_scopes: GOOGLE_DEFAULT_SCOPES,
50            issuer_url_required: false,
51            manual_endpoint_configuration_supported: false,
52            authorization_url_required: false,
53            token_url_required: false,
54            userinfo_url_required: false,
55            supports_discovery: true,
56            supports_pkce: true,
57            supports_email_verified_claim: true,
58        }
59    }
60
61    async fn start_authorization(
62        &self,
63        provider: &ExternalAuthProviderConfig,
64        redirect_uri: &str,
65    ) -> Result<ExternalAuthAuthorizationStart> {
66        OidcProviderDriver::new()
67            .start_authorization(&google_oidc_config(provider), redirect_uri)
68            .await
69    }
70
71    async fn exchange_callback(
72        &self,
73        provider: &ExternalAuthProviderConfig,
74        callback: ExternalAuthCallback,
75    ) -> Result<ExternalAuthProfile> {
76        OidcProviderDriver::new()
77            .exchange_callback(&google_oidc_config(provider), callback)
78            .await
79    }
80
81    async fn test_provider(
82        &self,
83        provider: &ExternalAuthProviderConfig,
84    ) -> Result<ExternalAuthProviderTestResult> {
85        let mut result = OidcProviderDriver::new()
86            .test_provider(&google_oidc_config(provider))
87            .await?;
88        result.provider = self.descriptor().display_name.to_string();
89        Ok(result)
90    }
91}
92
93/// Applies the Google preset while still allowing tests to inject a loopback issuer.
94fn google_oidc_config(provider: &ExternalAuthProviderConfig) -> ExternalAuthProviderConfig {
95    let mut provider = provider.clone();
96    provider.provider_kind = ExternalAuthProviderKind::Google;
97    provider.protocol = ExternalAuthProtocol::Oidc;
98    provider.issuer_url = provider
99        .issuer_url
100        .filter(|value| !value.trim().is_empty())
101        .or_else(|| Some(GOOGLE_ISSUER_URL.to_string()));
102    provider.authorization_url = None;
103    provider.token_url = None;
104    provider.userinfo_url = None;
105    provider.scopes = if provider.scopes.trim().is_empty() {
106        GOOGLE_DEFAULT_SCOPES.to_string()
107    } else {
108        provider.scopes.trim().to_string()
109    };
110    provider.subject_claim = provider.subject_claim.or_else(|| Some("sub".to_string()));
111    provider.display_name_claim = provider
112        .display_name_claim
113        .or_else(|| Some("name".to_string()));
114    provider.email_claim = provider.email_claim.or_else(|| Some("email".to_string()));
115    provider.email_verified_claim = provider
116        .email_verified_claim
117        .or_else(|| Some("email_verified".to_string()));
118    provider.avatar_url_claim = provider
119        .avatar_url_claim
120        .or_else(|| Some("picture".to_string()));
121    provider
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    fn provider() -> ExternalAuthProviderConfig {
129        ExternalAuthProviderConfig {
130            id: 1,
131            key: "google".to_string(),
132            provider_kind: ExternalAuthProviderKind::Google,
133            protocol: ExternalAuthProtocol::Oidc,
134            options: crate::types::ExternalAuthProviderOptions::default(),
135            issuer_url: None,
136            authorization_url: Some("https://ignored.example.com/auth".to_string()),
137            token_url: Some("https://ignored.example.com/token".to_string()),
138            userinfo_url: Some("https://ignored.example.com/userinfo".to_string()),
139            client_id: "client-id".to_string(),
140            client_secret: Some("secret".to_string()),
141            scopes: String::new(),
142            subject_claim: None,
143            username_claim: None,
144            display_name_claim: None,
145            email_claim: None,
146            email_verified_claim: None,
147            groups_claim: None,
148            avatar_url_claim: None,
149            outbound_http_user_agent: None,
150        }
151    }
152
153    #[test]
154    fn google_config_uses_fixed_defaults_and_claims() {
155        let config = google_oidc_config(&provider());
156
157        assert_eq!(config.provider_kind, ExternalAuthProviderKind::Google);
158        assert_eq!(config.protocol, ExternalAuthProtocol::Oidc);
159        assert_eq!(config.issuer_url.as_deref(), Some(GOOGLE_ISSUER_URL));
160        assert_eq!(config.authorization_url, None);
161        assert_eq!(config.token_url, None);
162        assert_eq!(config.userinfo_url, None);
163        assert_eq!(config.scopes, GOOGLE_DEFAULT_SCOPES);
164        assert_eq!(config.subject_claim.as_deref(), Some("sub"));
165        assert_eq!(config.display_name_claim.as_deref(), Some("name"));
166        assert_eq!(config.email_claim.as_deref(), Some("email"));
167        assert_eq!(
168            config.email_verified_claim.as_deref(),
169            Some("email_verified")
170        );
171        assert_eq!(config.avatar_url_claim.as_deref(), Some("picture"));
172    }
173
174    #[test]
175    fn google_config_keeps_test_issuer_override() {
176        let mut provider = provider();
177        provider.issuer_url = Some("http://127.0.0.1:3000".to_string());
178
179        let config = google_oidc_config(&provider);
180
181        assert_eq!(config.issuer_url.as_deref(), Some("http://127.0.0.1:3000"));
182    }
183}