1use crate::rauthy_error::RauthyError;
2use crate::token_set::OidcTokenSet;
3use crate::tokens::claims::RefreshToken;
4use crate::{DangerAcceptInvalidCerts, RauthyHttpsOnly, RootCertificate, VERSION};
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::borrow::Cow;
8use std::fmt::{Debug, Display, Formatter};
9use std::ops::Add;
10use std::sync::OnceLock;
11use std::time::Duration;
12use tokio::sync::watch;
13use tokio::time;
14use tracing::{debug, error, info, warn};
15
16static TX_ACCESS_TOKEN: OnceLock<watch::Sender<String>> = std::sync::OnceLock::new();
17static RX_ACCESS_TOKEN: OnceLock<watch::Receiver<String>> = std::sync::OnceLock::new();
18static TX_ID_TOKEN: OnceLock<watch::Sender<Option<String>>> = std::sync::OnceLock::new();
19static RX_ID_TOKEN: OnceLock<watch::Receiver<Option<String>>> = std::sync::OnceLock::new();
20
21#[derive(Debug, Deserialize)]
22struct DeviceCodeResponse {
23 pub device_code: String,
24 pub user_code: String,
25 pub verification_uri: String,
26 pub verification_uri_complete: Option<String>,
27 pub expires_in: u16,
28 pub interval: Option<u8>,
29}
30
31#[derive(Serialize)]
32struct DeviceGrantRequest<'a> {
33 pub client_id: &'a str,
34 pub client_secret: Option<&'a str>,
35 pub scope: Option<&'a str>,
36}
37
38#[derive(Serialize)]
39struct DeviceGrantTokenRequest {
40 pub client_id: String,
41 pub client_secret: Option<String>,
42 pub device_code: String,
43 pub grant_type: &'static str,
44}
45
46#[derive(Debug, Deserialize)]
47struct MetaResponse {
48 pub device_authorization_endpoint: String,
49 pub token_endpoint: String,
50}
51
52#[derive(Debug, Deserialize)]
53pub struct OAuth2ErrorResponse<'a> {
54 pub error: OAuth2ErrorTypeResponse,
55 pub error_description: Option<Cow<'a, str>>,
56}
57
58#[derive(Debug, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum OAuth2ErrorTypeResponse {
61 InvalidRequest,
62 InvalidClient,
63 InvalidGrant,
64 UnauthorizedClient,
65 UnsupportedGrantType,
66 InvalidScope,
67 AuthorizationPending,
69 SlowDown,
70 AccessDenied,
71 ExpiredToken,
72}
73
74pub struct DeviceCode {
75 client: reqwest::Client,
76 token_endpoint: String,
77 token_endpoint_payload: DeviceGrantTokenRequest,
78 interval: u64,
80
81 pub expires: DateTime<Utc>,
82 pub user_code: String,
83 pub verification_uri: String,
84 pub verification_uri_complete: Option<String>,
85}
86
87impl Debug for DeviceCode {
88 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
89 write!(
90 f,
91 "DeviceCode {{ token_endpoint: {}, token_endpoint_payload: <hidden>, expires: {}, \
92 user_code: {}, verification_uri: {}, verification_uri_complete: {:?} }}",
93 self.token_endpoint,
94 self.expires,
95 self.user_code,
96 self.verification_uri,
97 self.verification_uri_complete,
98 )
99 }
100}
101
102impl Display for DeviceCode {
103 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
104 write!(
105 f,
106 r#"
107 Please visit {} and enter your User Code: {}
108"#,
109 self.verification_uri, self.user_code
110 )
111 }
112}
113
114impl DeviceCode {
115 fn build_client(
116 root_certificate: Option<RootCertificate>,
117 https_only: RauthyHttpsOnly,
118 danger_insecure: DangerAcceptInvalidCerts,
119 ) -> Result<reqwest::Client, RauthyError> {
120 let builder = reqwest::Client::builder()
121 .timeout(Duration::from_secs(10))
122 .connect_timeout(Duration::from_secs(10))
123 .https_only(https_only.bool())
124 .danger_accept_invalid_certs(danger_insecure.bool())
125 .user_agent(format!("Rauthy OIDC Client v{VERSION}"));
126 if let Some(root) = root_certificate {
127 Ok(builder.add_root_certificate(root).build()?)
128 } else {
129 Ok(builder.build()?)
130 }
131 }
132
133 async fn fetch<T>(req: reqwest::RequestBuilder) -> Result<T, RauthyError>
134 where
135 T: Debug + for<'a> Deserialize<'a>,
136 {
137 let res = req.send().await?;
138 if !res.status().is_success() {
139 let status = res.status().as_u16();
140 let err = match res.text().await {
141 Ok(body) => {
142 format!("Rauthy request error - HTTP {status}: {body:?}")
143 }
144 Err(_) => {
145 format!("Rauthy request error - HTTP {status}")
146 }
147 };
148 return Err(RauthyError::Request(Cow::from(err)));
149 }
150 Ok(res.json::<T>().await?)
151 }
152
153 pub async fn request(issuer: &str, client_id: String) -> Result<Self, RauthyError> {
158 Self::request_with(
159 issuer,
160 client_id,
161 None,
162 None,
163 None,
164 RauthyHttpsOnly::Yes,
165 DangerAcceptInvalidCerts::No,
166 )
167 .await
168 }
169
170 pub async fn request_with(
173 issuer: &str,
174 client_id: String,
175 client_secret: Option<String>,
176 scope: Option<&str>,
177 root_certificate: Option<RootCertificate>,
178 https_only: RauthyHttpsOnly,
179 danger_insecure: DangerAcceptInvalidCerts,
180 ) -> Result<Self, RauthyError> {
181 let append = if issuer.ends_with('/') {
182 ".well-known/openid-configuration"
183 } else {
184 "/.well-known/openid-configuration"
185 };
186 let oidc_config_url = format!("{issuer}{append}");
187
188 let client = Self::build_client(root_certificate, https_only, danger_insecure)?;
189 let meta = Self::fetch::<MetaResponse>(client.get(oidc_config_url)).await?;
190 let device_code = Self::fetch::<DeviceCodeResponse>(
191 client
192 .post(meta.device_authorization_endpoint)
193 .form(&DeviceGrantRequest {
194 client_id: &client_id,
195 client_secret: client_secret.as_deref(),
196 scope,
197 }),
198 )
199 .await?;
200 let expires = Utc::now().add(chrono::Duration::seconds(device_code.expires_in as i64));
201 let token_endpoint_payload = DeviceGrantTokenRequest {
202 client_id,
203 client_secret,
204 device_code: device_code.device_code,
205 grant_type: "urn:ietf:params:oauth:grant-type:device_code",
206 };
207
208 Ok(DeviceCode {
209 client,
210 token_endpoint: meta.token_endpoint,
211 token_endpoint_payload,
212 interval: device_code.interval.unwrap_or(5) as u64,
213 expires,
214 user_code: device_code.user_code,
215 verification_uri: device_code.verification_uri,
216 verification_uri_complete: device_code.verification_uri_complete,
217 })
218 }
219
220 pub async fn wait_for_token(&mut self) -> Result<OidcTokenSet, RauthyError> {
223 let mut wait_for = self.interval;
224
225 loop {
226 tokio::time::sleep(Duration::from_secs(wait_for)).await;
227
228 let res = self
229 .client
230 .post(&self.token_endpoint)
231 .form(&self.token_endpoint_payload)
232 .send()
233 .await?;
234
235 if res.status().is_success() {
236 let ts = res.json::<OidcTokenSet>().await?;
237 info!("Success - received an OIDC TokenSet");
238 return Ok(ts);
239 }
240
241 let err = res.json::<OAuth2ErrorResponse>().await?;
242 match err.error {
243 OAuth2ErrorTypeResponse::AuthorizationPending => {
244 debug!("Authorization Pending - awaiting user verification");
245 }
246
247 OAuth2ErrorTypeResponse::SlowDown => {
249 warn!("Received a `slow_down` - doubling token fetch interval");
250 wait_for *= 2;
251 }
252
253 OAuth2ErrorTypeResponse::AccessDenied => {
254 return Err(RauthyError::Provider(Cow::from(format!("{err:?}"))));
255 }
256
257 OAuth2ErrorTypeResponse::ExpiredToken => {
258 return Err(RauthyError::Provider(Cow::from(format!("{err:?}"))));
259 }
260
261 _ => return Err(RauthyError::Provider(Cow::from(format!("{err:?}")))),
264 }
265 }
266 }
267
268 #[cfg(feature = "qrcode")]
269 fn qr(&self) -> Result<qrcode::QrCode, RauthyError> {
270 if let Some(uri) = &self.verification_uri_complete {
271 Ok(qrcode::QrCode::new(uri)?)
272 } else {
273 Err(RauthyError::Provider(Cow::from(
274 "did not receive a `verification_uri_complete`",
275 )))
276 }
277 }
278
279 #[cfg(feature = "qrcode")]
280 pub fn qr_string(&self) -> Result<String, RauthyError> {
281 use qrcode::render::unicode;
282
283 let code = self.qr()?;
284 let image = code
285 .render::<unicode::Dense1x2>()
286 .dark_color(unicode::Dense1x2::Light)
287 .light_color(unicode::Dense1x2::Dark)
288 .build();
289 Ok(image)
290 }
291
292 #[cfg(feature = "qrcode")]
293 pub fn qr_svg(&self) -> Result<String, RauthyError> {
294 use qrcode::render::svg;
295
296 let code = self.qr()?;
297 let image = code
298 .render()
299 .min_dimensions(200, 200)
300 .dark_color(svg::Color("#000000"))
301 .light_color(svg::Color("#ffffff"))
302 .build();
303 Ok(image)
304 }
305}
306
307#[derive(Debug, Serialize)]
308struct TokenRequest<'a> {
309 grant_type: &'a str,
311 client_id: &'a str,
312 client_secret: &'a Option<String>,
313 refresh_token: &'a str,
314}
315
316impl OidcTokenSet {
317 pub async fn access_token() -> Result<String, RauthyError> {
318 if let Some(rx) = RX_ACCESS_TOKEN.get() {
319 Ok(rx.borrow().to_string())
320 } else {
321 Err(RauthyError::Init("You must spawn a refresh handler first"))
322 }
323 }
324
325 pub async fn id_token() -> Result<Option<String>, RauthyError> {
326 if let Some(rx) = RX_ID_TOKEN.get() {
327 Ok(rx.borrow().clone())
328 } else {
329 Err(RauthyError::Init("You must spawn a refresh handler first"))
330 }
331 }
332
333 pub async fn into_refresh_handler(
334 self,
335 client_id: String,
336 client_secret: Option<String>,
337 ) -> Result<(), RauthyError> {
338 self.into_refresh_handler_with(
339 client_id,
340 client_secret,
341 None,
342 RauthyHttpsOnly::Yes,
343 DangerAcceptInvalidCerts::No,
344 )
345 .await
346 }
347
348 pub async fn into_refresh_handler_with(
349 self,
350 client_id: String,
351 client_secret: Option<String>,
352 root_certificate: Option<RootCertificate>,
353 https_only: RauthyHttpsOnly,
354 danger_insecure: DangerAcceptInvalidCerts,
355 ) -> Result<(), RauthyError> {
356 if self.refresh_token.is_none() {
357 return Err(RauthyError::Provider(Cow::from(
358 "Misconfigured client - refresh_token is missing",
359 )));
360 }
361
362 if RX_ACCESS_TOKEN.get().is_none() {
365 let (tx_access, rx_access) = watch::channel(self.access_token);
366 let (tx_id, rx_id) = watch::channel(self.id_token);
367 TX_ACCESS_TOKEN.set(tx_access).unwrap();
368 RX_ACCESS_TOKEN.set(rx_access).unwrap();
369 TX_ID_TOKEN.set(tx_id).unwrap();
370 RX_ID_TOKEN.set(rx_id).unwrap();
371 } else {
372 TX_ACCESS_TOKEN.get().unwrap().send(self.access_token)?;
373 TX_ID_TOKEN.get().unwrap().send(self.id_token)?;
374 }
375
376 let refresh_token = self.refresh_token.unwrap();
378 let refresh_claims =
379 OidcTokenSet::danger_claims_unvalidated::<RefreshToken>(&refresh_token)?;
380 let now = Utc::now().timestamp();
381
382 if refresh_claims.common.exp < now {
383 return Err(RauthyError::Token(Cow::from(
384 "The refresh_token as already expired",
385 )));
386 }
387
388 tokio::spawn(Self::refresh_handler(
389 refresh_token,
390 refresh_claims,
391 client_id,
392 client_secret,
393 root_certificate,
394 https_only,
395 danger_insecure,
396 ));
397
398 Ok(())
399 }
400
401 async fn refresh_handler(
402 mut refresh_token: String,
403 mut refresh_claims: RefreshToken,
404 client_id: String,
405 client_secret: Option<String>,
406 root_certificate: Option<RootCertificate>,
407 https_only: RauthyHttpsOnly,
408 danger_insecure: DangerAcceptInvalidCerts,
409 ) {
410 'main: loop {
411 let sleep_for = refresh_claims.common.nbf - Utc::now().timestamp() + 1;
412 if sleep_for >= 10 {
413 time::sleep(Duration::from_secs(sleep_for as u64)).await;
414 } else {
415 warn!(
416 "refresh_token nbf is < 10 seconds -> you may want to increase access_token \
417 lifetime to not end up in a busy refresh loop"
418 );
419 time::sleep(Duration::from_secs(1)).await;
422 }
423 debug!("Refreshing token now");
424
425 let token_url = format!("{}/oidc/token", refresh_claims.common.iss);
426 let payload = TokenRequest {
427 grant_type: "refresh_token",
428 client_id: &client_id,
429 client_secret: &client_secret,
430 refresh_token: &refresh_token,
431 };
432
433 let client = DeviceCode::build_client(
435 root_certificate.clone(),
436 https_only.clone(),
437 danger_insecure.clone(),
438 )
439 .unwrap();
440
441 let mut retries = 0;
442 loop {
443 match client.post(&token_url).form(&payload).send().await {
444 Ok(res) => {
445 let status = res.status();
446 if status.is_success() {
447 match res.json::<OidcTokenSet>().await {
448 Ok(ts) => {
449 TX_ACCESS_TOKEN
450 .get()
451 .unwrap()
452 .send(ts.access_token)
453 .expect("internal send to not fail");
454 TX_ID_TOKEN
455 .get()
456 .unwrap()
457 .send(ts.id_token)
458 .expect("internal send to not fail");
459
460 if let Some(rt) = ts.refresh_token {
461 info!("Rauthy token refreshed successfully");
462 refresh_token = rt;
463 refresh_claims =
464 OidcTokenSet::danger_claims_unvalidated(&refresh_token)
465 .unwrap();
466 break;
467 } else {
468 warn!(
469 "Did not receive a new refresh_token from refresh - exiting refresh handler"
470 );
471 break 'main;
472 }
473 }
474 Err(err) => {
475 error!(
476 "Error deserializing TokenSet: {:?}\nexiting refresh handler",
477 err
478 );
479 break 'main;
480 }
481 }
482 } else {
483 let body = res.text().await.unwrap_or_default();
484 error!(
485 "Error during token refresh: {}: {}\nexiting refresh handler",
486 status, body
487 );
488 break 'main;
489 }
490 }
491 Err(err) => {
492 error!("Error refreshing token: {:?}", err);
494 retries += 1;
495 if retries > 10 {
496 error!("Refresh retries exceeded - exiting refresh handler");
497 break 'main;
498 }
499 time::sleep(Duration::from_secs(10)).await;
500 }
501 }
502 }
503 }
504 }
505}