synd_term/auth/
authenticator.rs

1use std::ops::Add;
2use synd_auth::{
3    device_flow::{provider, DeviceAuthorizationResponse, DeviceFlow},
4    jwt,
5};
6
7use crate::{
8    auth::{AuthenticationProvider, Credential, CredentialError, Verified},
9    config,
10    types::Time,
11};
12
13#[derive(Clone)]
14pub struct DeviceFlows {
15    pub github: DeviceFlow<provider::Github>,
16    pub google: DeviceFlow<provider::Google>,
17}
18
19#[derive(Clone)]
20pub struct JwtService {
21    pub google: jwt::google::JwtService,
22}
23
24impl JwtService {
25    pub fn new() -> Self {
26        Self {
27            google: jwt::google::JwtService::default(),
28        }
29    }
30
31    #[must_use]
32    pub fn with_google_jwt_service(self, google: jwt::google::JwtService) -> Self {
33        Self { google }
34    }
35
36    pub(crate) async fn refresh_google_id_token(
37        &self,
38        refresh_token: &str,
39    ) -> Result<Verified<Credential>, CredentialError> {
40        let id_token = self
41            .google
42            .refresh_id_token(refresh_token)
43            .await
44            .map_err(CredentialError::RefreshJwt)?;
45        let expired_at = self
46            .google
47            .decode_id_token_insecure(&id_token, false)
48            .map_err(CredentialError::DecodeJwt)?
49            .expired_at();
50        let credential = Credential::Google {
51            id_token,
52            refresh_token: refresh_token.to_owned(),
53            expired_at,
54        };
55        Ok(Verified(credential))
56    }
57}
58
59#[derive(Clone)]
60pub struct Authenticator {
61    pub device_flows: DeviceFlows,
62    pub jwt_service: JwtService,
63}
64
65impl Authenticator {
66    pub fn new() -> Self {
67        Self {
68            device_flows: DeviceFlows {
69                github: DeviceFlow::new(provider::Github::default()),
70                google: DeviceFlow::new(provider::Google::default()),
71            },
72            jwt_service: JwtService::new(),
73        }
74    }
75
76    #[must_use]
77    pub fn with_device_flows(self, device_flows: DeviceFlows) -> Self {
78        Self {
79            device_flows,
80            ..self
81        }
82    }
83
84    #[must_use]
85    pub fn with_jwt_service(self, jwt_service: JwtService) -> Self {
86        Self {
87            jwt_service,
88            ..self
89        }
90    }
91
92    pub(crate) async fn init_device_flow(
93        &self,
94        provider: AuthenticationProvider,
95    ) -> anyhow::Result<DeviceAuthorizationResponse> {
96        match provider {
97            AuthenticationProvider::Github => {
98                self.device_flows.github.device_authorize_request().await
99            }
100
101            AuthenticationProvider::Google => {
102                self.device_flows.google.device_authorize_request().await
103            }
104        }
105    }
106
107    pub(crate) async fn poll_device_flow_access_token(
108        &self,
109        now: Time,
110        provider: AuthenticationProvider,
111        response: DeviceAuthorizationResponse,
112    ) -> anyhow::Result<Verified<Credential>> {
113        match provider {
114            AuthenticationProvider::Github => {
115                let token_response = self
116                    .device_flows
117                    .github
118                    .poll_device_access_token(response.device_code, response.interval)
119                    .await?;
120
121                Ok(Verified(Credential::Github {
122                    access_token: token_response.access_token,
123                }))
124            }
125            AuthenticationProvider::Google => {
126                let token_response = self
127                    .device_flows
128                    .google
129                    .poll_device_access_token(response.device_code, response.interval)
130                    .await?;
131
132                let id_token = token_response.id_token.expect("id token not found");
133                let expired_at = self
134                    .jwt_service
135                    .google
136                    .decode_id_token_insecure(&id_token, false)
137                    .ok()
138                    .map_or(now.add(config::credential::FALLBACK_EXPIRE), |claims| {
139                        claims.expired_at()
140                    });
141                Ok(Verified(Credential::Google {
142                    id_token,
143                    refresh_token: token_response
144                        .refresh_token
145                        .expect("refresh token not found"),
146                    expired_at,
147                }))
148            }
149        }
150    }
151}