synd_term/auth/
authenticator.rs1use 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}