Skip to main content

aetheris_server/auth/
google.rs

1use openidconnect::core::{
2    CoreClient, CoreIdTokenClaims, CoreIdTokenVerifier, CoreProviderMetadata,
3};
4use openidconnect::{
5    ClientId, ClientSecret, EndpointMaybeSet, EndpointNotSet, EndpointSet, IssuerUrl, Nonce,
6};
7use std::str::FromStr;
8use tonic::Status;
9
10pub struct GoogleOidcClient {
11    client: CoreClient<
12        EndpointSet,
13        EndpointNotSet,
14        EndpointNotSet,
15        EndpointNotSet,
16        EndpointMaybeSet,
17        EndpointMaybeSet,
18    >,
19}
20
21impl GoogleOidcClient {
22    /// Creates a new Google OIDC client by performing discovery.
23    ///
24    /// # Errors
25    ///
26    /// Returns an error if:
27    /// - `GOOGLE_CLIENT_ID` or `GOOGLE_CLIENT_SECRET` environment variables are malformed.
28    /// - The reqwest HTTP client cannot be initialized.
29    /// - OIDC discovery fails (e.g., network issues, Google's discovery endpoint is unreachable).
30    pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
31        let client_id =
32            std::env::var("GOOGLE_CLIENT_ID").map_err(|_| "GOOGLE_CLIENT_ID missing")?;
33        let client_secret =
34            std::env::var("GOOGLE_CLIENT_SECRET").map_err(|_| "GOOGLE_CLIENT_SECRET missing")?;
35
36        let http_client = reqwest::Client::builder()
37            .redirect(reqwest::redirect::Policy::none())
38            .build()?;
39
40        let provider_metadata = CoreProviderMetadata::discover_async(
41            IssuerUrl::new("https://accounts.google.com".to_string())?,
42            &|req: openidconnect::HttpRequest| {
43                let client = http_client.clone();
44                async move {
45                    let resp = client
46                        .execute(req.try_into().map_err(|e| {
47                            openidconnect::HttpClientError::Other(format!("Reqwest error: {e}"))
48                        })?)
49                        .await
50                        .map_err(|e| openidconnect::HttpClientError::Reqwest(Box::new(e)))?;
51
52                    let status = resp.status();
53                    let headers = resp.headers().clone();
54                    let body = resp
55                        .bytes()
56                        .await
57                        .map_err(|e| openidconnect::HttpClientError::Reqwest(Box::new(e)))?;
58
59                    let mut http_resp = openidconnect::HttpResponse::new(body.to_vec());
60                    *http_resp.status_mut() = status;
61                    *http_resp.headers_mut() = headers;
62                    Ok::<_, openidconnect::HttpClientError<reqwest::Error>>(http_resp)
63                }
64            },
65        )
66        .await?;
67
68        let client = CoreClient::from_provider_metadata(
69            provider_metadata,
70            ClientId::new(client_id),
71            Some(ClientSecret::new(client_secret)),
72        );
73
74        Ok(Self { client })
75    }
76
77    /// Verifies a Google ID token and returns its claims.
78    ///
79    /// # Errors
80    ///
81    /// Returns a `tonic::Status` with `Unauthenticated` if:
82    /// - The ID token is malformed and cannot be parsed.
83    /// - The ID token signature is invalid.
84    /// - The ID token claims (issuer, audience, expiry) are invalid.
85    pub fn verify_token(&self, id_token: &str, nonce: &str) -> Result<CoreIdTokenClaims, Status> {
86        let id_token = openidconnect::core::CoreIdToken::from_str(id_token)
87            .map_err(|e| Status::unauthenticated(format!("Malformed ID token: {e}")))?;
88
89        let verifier: CoreIdTokenVerifier = self.client.id_token_verifier();
90
91        let claims = id_token
92            .claims(&verifier, &Nonce::new(nonce.to_string()))
93            .map_err(|e| Status::unauthenticated(format!("Invalid ID token claims: {e}")))?;
94
95        Ok(claims.clone())
96    }
97}