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#[derive(PartialEq, Eq, Debug)]
64pub(super) struct Expired<C = Credential>(pub(super) C);
65
66#[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#[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 Ordering::Less | Ordering::Equal => {
129 debug!("Google jwt expired, trying to refresh");
130
131 Ok(VerifyResult::Expired(Expired(credential)))
132 }
133 Ordering::Greater => Ok(VerifyResult::Verified(Verified(credential))),
135 }
136 }
137 }
138 }
139}
140
141pub(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}