yup_oauth2/
types.rs

1use crate::error::{AuthErrorOr, Error};
2
3use serde::{Deserialize, Serialize};
4use time::OffsetDateTime;
5
6/// Represents a token returned by oauth2 servers. All tokens are Bearer tokens. Other types of
7/// tokens are not supported.
8#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
9pub struct AccessToken {
10    access_token: Option<String>,
11    expires_at: Option<OffsetDateTime>,
12}
13
14impl AccessToken {
15    /// A string representation of the access token.
16    pub fn token(&self) -> Option<&str> {
17        self.access_token.as_deref()
18    }
19
20    /// The time at which the tokens will expire, if any.
21    pub fn expiration_time(&self) -> Option<OffsetDateTime> {
22        self.expires_at
23    }
24
25    /// Determine if the access token is expired.
26    ///
27    /// This will report that the token is expired 1 minute prior to the expiration time to ensure
28    /// that when the token is actually sent to the server it's still valid.
29    pub fn is_expired(&self) -> bool {
30        // Consider the token expired if it's within 1 minute of it's expiration time.
31        self.expires_at
32            .map(|expiration_time| {
33                expiration_time - time::Duration::minutes(1) <= OffsetDateTime::now_utc()
34            })
35            .unwrap_or(false)
36    }
37}
38
39impl From<TokenInfo> for AccessToken {
40    fn from(
41        TokenInfo {
42            access_token,
43            expires_at,
44            ..
45        }: TokenInfo,
46    ) -> Self {
47        AccessToken {
48            access_token,
49            expires_at,
50        }
51    }
52}
53
54/// Represents a token as returned by OAuth2 servers.
55///
56/// It is produced by all authentication flows.
57/// It authenticates certain operations, and must be refreshed once it reached it's expiry date.
58#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
59pub struct TokenInfo {
60    /// used when authorizing calls to oauth2 enabled services.
61    pub access_token: Option<String>,
62    /// used to refresh an expired access_token.
63    pub refresh_token: Option<String>,
64    /// The time when the token expires.
65    pub expires_at: Option<OffsetDateTime>,
66    /// Optionally included by the OAuth2 server and may contain information to verify the identity
67    /// used to obtain the access token.
68    /// Specifically Google API:s include this if the additional scopes "email" and/or "profile"
69    /// are used. In that case the content is an JWT token.
70    pub id_token: Option<String>,
71}
72
73impl TokenInfo {
74    pub(crate) fn from_json(json_data: &[u8]) -> Result<TokenInfo, Error> {
75        #[derive(Deserialize)]
76        struct RawToken {
77            access_token: Option<String>,
78            refresh_token: Option<String>,
79            token_type: Option<String>,
80            expires_in: Option<i64>,
81            id_token: Option<String>,
82        }
83
84        // Serialize first to a `serde_json::Value` then to `AuthErrorOr<RawToken>` to work around this bug in
85        // serde_json: https://github.com/serde-rs/json/issues/559
86        let raw_token = serde_json::from_slice::<serde_json::Value>(json_data)?;
87        let RawToken {
88            access_token,
89            refresh_token,
90            token_type,
91            expires_in,
92            id_token,
93        } = <AuthErrorOr<RawToken>>::deserialize(raw_token)?.into_result()?;
94
95        match token_type {
96            Some(token_ty) if !token_ty.eq_ignore_ascii_case("bearer") => {
97                use std::io;
98                return Err(io::Error::new(
99                    io::ErrorKind::InvalidData,
100                    format!(
101                        r#"unknown token type returned; expected "bearer" found {}"#,
102                        token_ty
103                    ),
104                )
105                .into());
106            }
107            _ => (),
108        }
109
110        let expires_at = match expires_in {
111            Some(seconds_from_now) => {
112                Some(OffsetDateTime::now_utc() + time::Duration::seconds(seconds_from_now))
113            }
114            None if id_token.is_some() && access_token.is_none() => {
115                // If the response contains only an ID token, an expiration date may not be
116                // returned. According to the docs, the tokens are always valid for 1 hour.
117                //
118                // https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-oidc
119                Some(OffsetDateTime::now_utc() + time::Duration::HOUR)
120            }
121            None => None,
122        };
123
124        Ok(TokenInfo {
125            id_token,
126            access_token,
127            refresh_token,
128            expires_at,
129        })
130    }
131
132    /// Returns true if we are expired.
133    pub fn is_expired(&self) -> bool {
134        self.expires_at
135            .map(|expiration_time| {
136                expiration_time - time::Duration::minutes(1) <= OffsetDateTime::now_utc()
137            })
138            .unwrap_or(false)
139    }
140}
141
142/// Represents either 'installed' or 'web' applications in a json secrets file.
143/// See `ConsoleApplicationSecret` for more information
144#[derive(Deserialize, Serialize, Clone, Default, Debug)]
145pub struct ApplicationSecret {
146    /// The client ID.
147    pub client_id: String,
148    /// The client secret.
149    pub client_secret: String,
150    /// The token server endpoint URI.
151    pub token_uri: String,
152    /// The authorization server endpoint URI.
153    pub auth_uri: String,
154    /// The redirect uris.
155    pub redirect_uris: Vec<String>,
156    /// Name of the google project the credentials are associated with
157    pub project_id: Option<String>,
158    /// The service account email associated with the client.
159    pub client_email: Option<String>,
160    /// The URL of the public x509 certificate, used to verify the signature on JWTs, such
161    /// as ID tokens, signed by the authentication provider.
162    pub auth_provider_x509_cert_url: Option<String>,
163    ///  The URL of the public x509 certificate, used to verify JWTs signed by the client.
164    pub client_x509_cert_url: Option<String>,
165}
166
167/// A type to facilitate reading and writing the json secret file
168/// as returned by the [google developer console](https://code.google.com/apis/console)
169#[derive(Deserialize, Serialize, Default, Debug)]
170pub struct ConsoleApplicationSecret {
171    /// web app secret
172    pub web: Option<ApplicationSecret>,
173    /// installed app secret
174    pub installed: Option<ApplicationSecret>,
175}
176
177#[cfg(test)]
178pub mod tests {
179    use super::*;
180
181    pub const SECRET: &str =
182        "{\"installed\":{\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\
183         \"client_secret\":\"UqkDJd5RFwnHoiG5x5Rub8SI\",\"token_uri\":\"https://accounts.google.\
184         com/o/oauth2/token\",\"client_email\":\"\",\"redirect_uris\":[\"urn:ietf:wg:oauth:2.0:\
185         oob\",\"oob\"],\"client_x509_cert_url\":\"\",\"client_id\":\
186         \"14070749909-vgip2f1okm7bkvajhi9jugan6126io9v.apps.googleusercontent.com\",\
187         \"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\"}}";
188
189    #[test]
190    fn console_secret() {
191        use serde_json as json;
192        match json::from_str::<ConsoleApplicationSecret>(SECRET) {
193            Ok(s) => assert!(s.installed.is_some() && s.web.is_none()),
194            Err(err) => panic!(
195                "Encountered error parsing ConsoleApplicationSecret: {}",
196                err
197            ),
198        }
199    }
200
201    #[test]
202    fn default_expiry_for_id_token_only() {
203        // If only an ID token is present, set a default expiration date
204        let json = r#"{"id_token": "id"}"#;
205
206        let token = TokenInfo::from_json(json.as_bytes()).unwrap();
207        assert_eq!(token.id_token, Some("id".to_owned()));
208
209        let expiry = token.expires_at.unwrap();
210        assert!(expiry <= time::OffsetDateTime::now_utc() + time::Duration::HOUR);
211    }
212
213    #[test]
214    fn no_default_expiry_for_access_token() {
215        // Don't set a default expiration date if an access token is returned
216        let json = r#"{"access_token": "access", "id_token": "id"}"#;
217
218        let token = TokenInfo::from_json(json.as_bytes()).unwrap();
219        assert_eq!(token.access_token, Some("access".to_owned()));
220        assert_eq!(token.id_token, Some("id".to_owned()));
221        assert_eq!(token.expires_at, None);
222    }
223}