tlns_google_oauth2/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::fmt::Debug;
4
5use oauth2::{
6    self,
7    basic::{
8        BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse,
9        BasicTokenResponse, BasicTokenType,
10    },
11    CsrfToken, EmptyExtraTokenFields, EndpointNotSet, EndpointSet, StandardRevocableToken,
12};
13
14pub use tlns_google_oauth2_traits::{FromGoogleScope, ToGoogleScope};
15
16#[cfg(feature = "grouped-scopes")]
17pub mod grouped_scopes;
18
19#[cfg(feature = "scopes")]
20pub mod scopes;
21
22#[cfg(not(any(feature = "grouped-scopes", feature = "scopes")))]
23compile_error!("You must enable either `grouped-scopes` or `scopes` feature to use this crate.");
24
25/// A thin wrapper around [`oauth2`] for Google OAuth2.  
26/// ```should_panic
27/// use tlns_google_oauth2::GoogleOAuth2Client;
28/// 
29/// #[pollster::main]
30/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
31///     let client = GoogleOAuth2Client::new(
32///         "client_id",
33///         "client_secret",
34///         "redirect_uri",
35///     )?;
36///     let auth_url = client.build_authorize_url(
37///         None,
38///         &[&tlns_google_oauth2::grouped_scopes::GoogleOAuth2APIv2::AuthUserinfoProfile, &tlns_google_oauth2::scopes::Scopes::AuthUserinfoEmail],
39///     );
40///     println!("Redirect to {}", auth_url.redirect_url);
41/// 
42///     let token = client.get_token("code", None).await?;
43///     println!("Token: {:?}", token);
44///     Ok(())
45/// }
46/// ```
47#[derive(Clone, Debug)]
48pub struct GoogleOAuth2Client {
49    // this is stupid
50    /// OAuth2 client
51    pub client: oauth2::Client<
52        BasicErrorResponse,
53        BasicTokenResponse,
54        BasicTokenIntrospectionResponse,
55        StandardRevocableToken,
56        BasicRevocationErrorResponse,
57        EndpointSet,
58        EndpointNotSet,
59        EndpointNotSet,
60        EndpointNotSet,
61        EndpointSet,
62    >,
63}
64
65#[derive(Clone, Debug)]
66/// Authentication stuffs
67pub struct Authentication<'a> {
68    /// URL to redirect user to
69    pub redirect_url: String,
70    /// CSRF token
71    pub csrf_token: CsrfToken,
72    /// Scopes that you used in [`crate::GoogleOAuth2Client::build_authorize_url`]
73    pub scopes: &'a [&'a dyn ToGoogleScope],
74}
75
76impl GoogleOAuth2Client {
77    /// Create new [`crate::GoogleOAuth2Client`] instance
78    pub fn new(
79        client_id: impl Into<String>,
80        client_secret: impl Into<String>,
81        redirect_uri: impl Into<String>,
82    ) -> Result<Self, oauth2::url::ParseError> {
83        let url = oauth2::RedirectUrl::new(redirect_uri.into())?;
84        Ok(Self {
85            client: oauth2::basic::BasicClient::new(oauth2::ClientId::new(client_id.into()))
86                .set_client_secret(oauth2::ClientSecret::new(client_secret.into()))
87                .set_auth_uri(
88                    oauth2::AuthUrl::new(
89                        "https://accounts.google.com/o/oauth2/v2/auth".to_string(),
90                    )
91                    .unwrap(),
92                )
93                .set_token_uri(
94                    oauth2::TokenUrl::new("https://oauth2.googleapis.com/token".to_string())
95                        .unwrap(),
96                )
97                .set_redirect_uri(url),
98        })
99    }
100
101    /// Make a authorization URL for user to authenticate
102    /// `csrf_token` will be default [`oauth2::CsrfToken::new_random`]
103    /// [`crate::grouped_scopes`] example will be
104    /// ```rust
105    /// &[&tlns_google_oauth2::grouped_scopes::GoogleOAuth2APIv2::AuthUserinfoProfile];
106    /// ```
107    /// Or like this!
108    /// ```rust
109    /// use std::str::FromStr;
110    /// use tlns_google_oauth2::FromGoogleScope;
111    /// &[&tlns_google_oauth2::scopes::Scopes::from_google_scope("https://www.googleapis.com/auth/userinfo.profile").unwrap()];
112    /// // or
113    /// &[&tlns_google_oauth2::scopes::Scopes::from_str("https://www.googleapis.com/auth/userinfo.profile").unwrap()];
114    /// ```
115    /// Or if you are using [`crate::scopes`]
116    /// ```rust
117    /// &[&tlns_google_oauth2::scopes::Scopes::AuthUserinfoProfile];
118    /// ```
119    pub fn build_authorize_url<'a>(
120        &self,
121        csrf_token: Option<fn() -> CsrfToken>,
122        scopes: &'a [&'a dyn ToGoogleScope],
123    ) -> Authentication<'a> {
124        let auth_req = self
125            .client
126            .authorize_url(csrf_token.unwrap_or(CsrfToken::new_random))
127            .add_scopes(
128                scopes
129                    .iter()
130                    .map(|e| oauth2::Scope::new(e.to_google_scope().to_string())),
131            );
132
133        let res = auth_req.url();
134        Authentication {
135            redirect_url: res.0.to_string(),
136            csrf_token: res.1,
137            scopes,
138        }
139    }
140
141    /// Get authentication tokens from provider with authenticated code from Google
142    /// Typically you will get it from the query string of the redirected URL
143    pub async fn get_token(
144        &self,
145        auth_code: impl Into<String>,
146        http_client: Option<&oauth2::reqwest::Client>,
147    ) -> Result<
148        oauth2::StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
149        oauth2::RequestTokenError<
150            oauth2::HttpClientError<oauth2::reqwest::Error>,
151            oauth2::StandardErrorResponse<oauth2::basic::BasicErrorResponseType>,
152        >,
153    > {
154        // i love you rust
155        let http_client = http_client
156            .map(|i| std::borrow::Cow::Borrowed(i))
157            .unwrap_or(std::borrow::Cow::Owned(oauth2::reqwest::Client::new()));
158        let res = self
159            .client
160            .exchange_code(oauth2::AuthorizationCode::new(auth_code.into()))
161            .request_async(http_client.as_ref())
162            .await?;
163        Ok(res)
164    }
165
166    /// Refresh your token in case it has expired
167    /// You can retrieve it from [`oauth2::StandardTokenResponse::refresh_token`]
168    pub async fn refresh_token(
169        &self,
170        refresh_token: impl Into<String>,
171        http_client: Option<&oauth2::reqwest::Client>,
172    ) -> Result<
173        oauth2::StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
174        oauth2::RequestTokenError<
175            oauth2::HttpClientError<oauth2::reqwest::Error>,
176            oauth2::StandardErrorResponse<oauth2::basic::BasicErrorResponseType>,
177        >,
178    > {
179        // i love you rust
180        let http_client = http_client
181            .map(|i| std::borrow::Cow::Borrowed(i))
182            .unwrap_or(std::borrow::Cow::Owned(oauth2::reqwest::Client::new()));
183        let res = self
184            .client
185            .exchange_refresh_token(&oauth2::RefreshToken::new(refresh_token.into()))
186            .request_async(http_client.as_ref())
187            .await?;
188        Ok(res)
189    }
190}