1use std::time::{Duration, SystemTime};
23
24use jsonwebtoken::jwk::JwkSet;
25use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
26use mz_frontegg_auth::{AppPassword, Claims};
27use reqwest::{Method, RequestBuilder};
28use serde::de::DeserializeOwned;
29use serde::{Deserialize, Serialize};
30use tokio::sync::Mutex;
31use url::Url;
32
33use crate::config::{ClientBuilder, ClientConfig};
34use crate::error::{ApiError, Error};
35
36pub mod app_password;
37pub mod role;
38pub mod user;
39
40const CREDENTIALS_AUTH_PATH: [&str; 5] = ["identity", "resources", "auth", "v1", "user"];
41const APP_PASSWORD_AUTH_PATH: [&str; 5] = ["identity", "resources", "auth", "v1", "api-token"];
42
43const REFRESH_AUTH_PATH: [&str; 7] = [
44 "identity",
45 "resources",
46 "auth",
47 "v1",
48 "api-token",
49 "token",
50 "refresh",
51];
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55struct AuthenticationResponse {
56 access_token: String,
57 expires: String,
58 expires_in: i64,
59 refresh_token: String,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64struct CredentialsAuthenticationRequest<'a> {
65 email: &'a str,
66 password: &'a str,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71struct AppPasswordAuthenticationRequest<'a> {
72 client_id: &'a str,
73 secret: &'a str,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78struct RefreshRequest<'a> {
79 refresh_token: &'a str,
80}
81
82#[derive(Debug, Clone)]
83pub(crate) struct Auth {
84 token: String,
85 refresh_at: SystemTime,
87 refresh_token: String,
88}
89
90pub enum Authentication {
97 Credentials(Credentials),
100 AppPassword(AppPassword),
103 AccessToken(String),
107}
108
109pub struct Credentials {
113 pub email: String,
115 pub password: String,
117}
118
119pub struct Client {
127 pub(crate) inner: reqwest::Client,
128 pub(crate) authentication: Authentication,
129 pub(crate) endpoint: Url,
130 pub(crate) auth: Mutex<Option<Auth>>,
131}
132
133impl Client {
134 pub fn new(config: ClientConfig) -> Client {
136 ClientBuilder::default().build(config)
137 }
138
139 pub fn builder() -> ClientBuilder {
142 ClientBuilder::default()
143 }
144
145 fn build_request<P>(&self, method: Method, path: P) -> RequestBuilder
147 where
148 P: IntoIterator,
149 P::Item: AsRef<str>,
150 {
151 let mut url = self.endpoint.clone();
152 url.path_segments_mut()
153 .expect("builder validated URL can be a base")
154 .clear()
155 .extend(path);
156 self.inner.request(method, url)
157 }
158
159 async fn send_request<T>(&self, req: RequestBuilder) -> Result<T, Error>
161 where
162 T: DeserializeOwned,
163 {
164 let token = self.auth().await?;
165 let req = req.bearer_auth(token);
166 self.send_unauthenticated_request(req).await
167 }
168
169 async fn send_unauthenticated_request<T>(&self, req: RequestBuilder) -> Result<T, Error>
170 where
171 T: DeserializeOwned,
172 {
173 #[derive(Deserialize)]
174 #[serde(rename_all = "camelCase")]
175 struct ErrorResponse {
176 #[serde(default)]
177 message: Option<String>,
178 #[serde(default)]
179 errors: Vec<String>,
180 }
181
182 let res = req.send().await?;
183 let status_code = res.status();
184 if status_code.is_success() {
185 Ok(res.json().await?)
186 } else {
187 match res.json::<ErrorResponse>().await {
188 Ok(e) => {
189 let mut messages = e.errors;
190 messages.extend(e.message);
191 Err(Error::Api(ApiError {
192 status_code,
193 messages,
194 }))
195 }
196 Err(_) => Err(Error::Api(ApiError {
197 status_code,
198 messages: vec!["unable to decode error details".into()],
199 })),
200 }
201 }
202 }
203
204 pub async fn auth(&self) -> Result<String, Error> {
207 if let Authentication::AccessToken(token) = &self.authentication {
208 return Ok(token.clone());
209 }
210
211 let mut auth = self.auth.lock().await;
212 let mut req;
213
214 match &*auth {
215 Some(auth) => {
216 if SystemTime::now() < auth.refresh_at {
217 return Ok(auth.token.clone());
218 } else {
219 req = self.build_request(Method::POST, REFRESH_AUTH_PATH);
221 let refresh_request = RefreshRequest {
222 refresh_token: auth.refresh_token.as_str(),
223 };
224 req = req.json(&refresh_request);
225 }
226 }
227 None => {
228 match &self.authentication {
229 Authentication::Credentials(credentials) => {
230 req = self.build_request(Method::POST, CREDENTIALS_AUTH_PATH);
232
233 let authentication_request = CredentialsAuthenticationRequest {
234 email: &credentials.email,
235 password: &credentials.password,
236 };
237 req = req.json(&authentication_request);
238 }
239 Authentication::AppPassword(app_password) => {
240 req = self.build_request(Method::POST, APP_PASSWORD_AUTH_PATH);
241
242 let authentication_request = AppPasswordAuthenticationRequest {
243 client_id: &app_password.client_id.to_string(),
244 secret: &app_password.secret_key.to_string(),
245 };
246 req = req.json(&authentication_request);
247 }
248 Authentication::AccessToken(_) => unreachable!("returned above"),
249 }
250 }
251 }
252
253 let res: AuthenticationResponse = self.send_unauthenticated_request(req).await?;
255
256 *auth = Some(Auth {
257 token: res.access_token.clone(),
258 refresh_at: SystemTime::now()
260 + (Duration::from_secs(res.expires_in.try_into().unwrap()) / 2),
261 refresh_token: res.refresh_token,
262 });
263 Ok(res.access_token)
264 }
265
266 async fn get_jwks(&self) -> Result<JwkSet, Error> {
268 let well_known = vec![".well-known", "jwks.json"];
269 let req = self.build_request(Method::GET, well_known);
270 let jwks: JwkSet = self.send_request(req).await?;
271 Ok(jwks)
272 }
273
274 pub async fn claims(&self) -> Result<Claims, Error> {
277 let jwks = self.get_jwks().await.map_err(|_| Error::FetchingJwks)?;
278 let jwk = jwks.keys.first().ok_or_else(|| Error::EmptyJwks)?;
279 let token = self.auth().await?;
280
281 let mut validation = Validation::new(Algorithm::RS256);
282
283 validation.validate_aud = false;
297
298 let token_data = decode::<Claims>(
299 &token,
300 &DecodingKey::from_jwk(jwk).map_err(|_| Error::ConvertingJwks)?,
301 &validation,
302 )
303 .map_err(Error::DecodingClaims)?;
304
305 Ok(token_data.claims)
306 }
307}