synd_term/auth/
mod.rs

1use std::{
2    borrow::Borrow,
3    cmp::Ordering,
4    fmt,
5    ops::{Deref, Sub},
6};
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use synd_auth::jwt::google::JwtError;
11use thiserror::Error;
12use tracing::debug;
13
14use crate::{
15    application::{Cache, JwtService, LoadCacheError, PersistCacheError},
16    config,
17    types::Time,
18};
19
20pub mod authenticator;
21
22#[derive(Debug, Clone, Copy)]
23pub enum AuthenticationProvider {
24    Github,
25    Google,
26}
27
28#[derive(Debug, Error)]
29pub enum CredentialError {
30    #[error("google jwt expired")]
31    GoogleJwtExpired { refresh_token: String },
32    #[error("google jwt email not verified")]
33    GoogleJwtEmailNotVerified,
34    #[error("decode jwt: {0}")]
35    DecodeJwt(JwtError),
36    #[error("refresh jwt id token: {0}")]
37    RefreshJwt(JwtError),
38    #[error("persist credential: {0}")]
39    PersistCredential(#[from] PersistCacheError),
40    #[error("load credential: {0}")]
41    LoadCredential(#[from] LoadCacheError),
42}
43
44#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
45pub enum Credential {
46    Github {
47        access_token: String,
48    },
49    Google {
50        id_token: String,
51        refresh_token: String,
52        expired_at: DateTime<Utc>,
53    },
54}
55
56impl fmt::Debug for Credential {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        f.debug_struct("Credential").finish_non_exhaustive()
59    }
60}
61
62/// Represents expired state
63#[derive(PartialEq, Eq, Debug)]
64pub(super) struct Expired<C = Credential>(pub(super) C);
65
66/// Represents verified state
67#[derive(Debug, Clone)]
68pub(super) struct Verified<C = Credential>(C);
69
70impl Deref for Verified<Credential> {
71    type Target = Credential;
72
73    fn deref(&self) -> &Self::Target {
74        &self.0
75    }
76}
77
78impl Borrow<Credential> for &Verified<Credential> {
79    fn borrow(&self) -> &Credential {
80        &self.0
81    }
82}
83
84impl<C> Verified<C> {
85    pub(super) fn into_inner(self) -> C {
86        self.0
87    }
88}
89
90/// Represents unverified state
91#[derive(PartialEq, Eq, Debug)]
92pub struct Unverified<C = Credential>(C);
93
94impl From<Credential> for Unverified<Credential> {
95    fn from(cred: Credential) -> Self {
96        Unverified(cred)
97    }
98}
99
100pub(super) enum VerifyResult {
101    Verified(Verified<Credential>),
102    Expired(Expired<Credential>),
103}
104
105impl Unverified<Credential> {
106    pub(super) fn verify(
107        self,
108        jwt_service: &JwtService,
109        now: DateTime<Utc>,
110    ) -> Result<VerifyResult, CredentialError> {
111        let credential = self.0;
112        match &credential {
113            Credential::Github { .. } => Ok(VerifyResult::Verified(Verified(credential))),
114            Credential::Google { id_token, .. } => {
115                let claims = jwt_service
116                    .google
117                    .decode_id_token_insecure(id_token, false)
118                    .map_err(CredentialError::DecodeJwt)?;
119                if !claims.email_verified {
120                    return Err(CredentialError::GoogleJwtEmailNotVerified);
121                }
122                match claims
123                    .expired_at()
124                    .sub(config::credential::EXPIRE_MARGIN)
125                    .cmp(&now)
126                {
127                    // expired
128                    Ordering::Less | Ordering::Equal => {
129                        debug!("Google jwt expired, trying to refresh");
130
131                        Ok(VerifyResult::Expired(Expired(credential)))
132                    }
133                    // not expired
134                    Ordering::Greater => Ok(VerifyResult::Verified(Verified(credential))),
135                }
136            }
137        }
138    }
139}
140
141/// Process for restoring credential from cache
142pub(crate) struct Restore<'a> {
143    pub(crate) jwt_service: &'a JwtService,
144    pub(crate) cache: &'a Cache,
145    pub(crate) now: Time,
146    pub(crate) persist_when_refreshed: bool,
147}
148
149impl Restore<'_> {
150    pub(crate) async fn restore(self) -> Result<Verified<Credential>, CredentialError> {
151        let Restore {
152            jwt_service,
153            cache,
154            now,
155            persist_when_refreshed,
156        } = self;
157        let cred = cache.load_credential()?;
158
159        match cred.verify(jwt_service, now)? {
160            VerifyResult::Verified(cred) => Ok(cred),
161            VerifyResult::Expired(Expired(Credential::Google { refresh_token, .. })) => {
162                let cred = jwt_service.refresh_google_id_token(&refresh_token).await?;
163
164                if persist_when_refreshed {
165                    cache.persist_credential(&cred)?;
166                }
167
168                Ok(cred)
169            }
170            VerifyResult::Expired(_) => panic!("Unexpected verify result. this is bug"),
171        }
172    }
173}