Skip to main content

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    pub fn new() -> Self {
32        Self
33    }
34}
35
36#[async_trait]
37impl ExternalAuthProviderDriver for GoogleProviderDriver {
38    fn kind(&self) -> ExternalAuthProviderKind {
39        ExternalAuthProviderKind::Google
40    }
41
42    fn descriptor(&self) -> ExternalAuthProviderDescriptor {
43        ExternalAuthProviderDescriptor {
44            kind: ExternalAuthProviderKind::Google,
45            protocol: ExternalAuthProtocol::Oidc,
46            display_name: "Google",
47            description: "Google OpenID Connect sign-in with fixed issuer and standard email_verified semantics.",
48            default_scopes: GOOGLE_DEFAULT_SCOPES,
49            issuer_url_required: false,
50            manual_endpoint_configuration_supported: false,
51            authorization_url_required: false,
52            token_url_required: false,
53            userinfo_url_required: false,
54            supports_discovery: true,
55            supports_pkce: true,
56            supports_email_verified_claim: true,
57        }
58    }
59
60    async fn start_authorization(
61        &self,
62        provider: &ExternalAuthProviderConfig,
63        redirect_uri: &str,
64    ) -> Result<ExternalAuthAuthorizationStart> {
65        OidcProviderDriver::new()
66            .start_authorization(&google_oidc_config(provider), redirect_uri)
67            .await
68    }
69
70    async fn exchange_callback(
71        &self,
72        provider: &ExternalAuthProviderConfig,
73        callback: ExternalAuthCallback,
74    ) -> Result<ExternalAuthProfile> {
75        OidcProviderDriver::new()
76            .exchange_callback(&google_oidc_config(provider), callback)
77            .await
78    }
79
80    async fn test_provider(
81        &self,
82        provider: &ExternalAuthProviderConfig,
83    ) -> Result<ExternalAuthProviderTestResult> {
84        let mut result = OidcProviderDriver::new()
85            .test_provider(&google_oidc_config(provider))
86            .await?;
87        result.provider = self.descriptor().display_name.to_string();
88        Ok(result)
89    }
90}
91
92/// Applies the Google preset while still allowing tests to inject a loopback issuer.
93fn google_oidc_config(provider: &ExternalAuthProviderConfig) -> ExternalAuthProviderConfig {
94    let mut provider = provider.clone();
95    provider.provider_kind = ExternalAuthProviderKind::Google;
96    provider.protocol = ExternalAuthProtocol::Oidc;
97    provider.issuer_url = provider
98        .issuer_url
99        .filter(|value| !value.trim().is_empty())
100        .or_else(|| Some(GOOGLE_ISSUER_URL.to_string()));
101    provider.authorization_url = None;
102    provider.token_url = None;
103    provider.userinfo_url = None;
104    provider.scopes = if provider.scopes.trim().is_empty() {
105        GOOGLE_DEFAULT_SCOPES.to_string()
106    } else {
107        provider.scopes.trim().to_string()
108    };
109    provider.subject_claim = provider.subject_claim.or_else(|| Some("sub".to_string()));
110    provider.display_name_claim = provider
111        .display_name_claim
112        .or_else(|| Some("name".to_string()));
113    provider.email_claim = provider.email_claim.or_else(|| Some("email".to_string()));
114    provider.email_verified_claim = provider
115        .email_verified_claim
116        .or_else(|| Some("email_verified".to_string()));
117    provider.avatar_url_claim = provider
118        .avatar_url_claim
119        .or_else(|| Some("picture".to_string()));
120    provider
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    fn provider() -> ExternalAuthProviderConfig {
128        ExternalAuthProviderConfig {
129            id: 1,
130            key: "google".to_string(),
131            provider_kind: ExternalAuthProviderKind::Google,
132            protocol: ExternalAuthProtocol::Oidc,
133            options: Default::default(),
134            issuer_url: None,
135            authorization_url: Some("https://ignored.example.com/auth".to_string()),
136            token_url: Some("https://ignored.example.com/token".to_string()),
137            userinfo_url: Some("https://ignored.example.com/userinfo".to_string()),
138            client_id: "client-id".to_string(),
139            client_secret: Some("secret".to_string()),
140            scopes: String::new(),
141            subject_claim: None,
142            username_claim: None,
143            display_name_claim: None,
144            email_claim: None,
145            email_verified_claim: None,
146            groups_claim: None,
147            avatar_url_claim: None,
148            outbound_http_user_agent: None,
149        }
150    }
151
152    #[test]
153    fn google_config_uses_fixed_defaults_and_claims() {
154        let config = google_oidc_config(&provider());
155
156        assert_eq!(config.provider_kind, ExternalAuthProviderKind::Google);
157        assert_eq!(config.protocol, ExternalAuthProtocol::Oidc);
158        assert_eq!(config.issuer_url.as_deref(), Some(GOOGLE_ISSUER_URL));
159        assert_eq!(config.authorization_url, None);
160        assert_eq!(config.token_url, None);
161        assert_eq!(config.userinfo_url, None);
162        assert_eq!(config.scopes, GOOGLE_DEFAULT_SCOPES);
163        assert_eq!(config.subject_claim.as_deref(), Some("sub"));
164        assert_eq!(config.display_name_claim.as_deref(), Some("name"));
165        assert_eq!(config.email_claim.as_deref(), Some("email"));
166        assert_eq!(
167            config.email_verified_claim.as_deref(),
168            Some("email_verified")
169        );
170        assert_eq!(config.avatar_url_claim.as_deref(), Some("picture"));
171    }
172
173    #[test]
174    fn google_config_keeps_test_issuer_override() {
175        let mut provider = provider();
176        provider.issuer_url = Some("http://127.0.0.1:3000".to_string());
177
178        let config = google_oidc_config(&provider);
179
180        assert_eq!(config.issuer_url.as_deref(), Some("http://127.0.0.1:3000"));
181    }
182}