|
| 1 | +use std::sync::{Arc, Mutex}; |
| 2 | + |
| 3 | +use chrono::{DateTime, FixedOffset, TimeDelta, Utc}; |
| 4 | +use jsonwebtoken::jwk::{Jwk, JwkSet}; |
| 5 | +use thiserror::Error; |
| 6 | +use url::Url; |
| 7 | + |
| 8 | +use crate::{ResponseExt, WorkOsError, WorkOsResult, user_management::GetJwksError}; |
| 9 | + |
| 10 | +type Entry = (JwkSet, DateTime<FixedOffset>); |
| 11 | + |
| 12 | +/// An error returned from [`RemoteJwkSet::find`]. |
| 13 | +#[derive(Debug, Error)] |
| 14 | +pub enum FindJwkError { |
| 15 | + /// Get JWKS error. |
| 16 | + #[error(transparent)] |
| 17 | + GetJwks(WorkOsError<GetJwksError>), |
| 18 | + |
| 19 | + /// Poison error. |
| 20 | + #[error("poison error: {0}")] |
| 21 | + Poison(String), |
| 22 | +} |
| 23 | + |
| 24 | +impl From<FindJwkError> for WorkOsError<FindJwkError> { |
| 25 | + fn from(value: FindJwkError) -> Self { |
| 26 | + Self::Operation(value) |
| 27 | + } |
| 28 | +} |
| 29 | + |
| 30 | +/// Remote JSON Web Key Set (JWKS). |
| 31 | +#[derive(Clone)] |
| 32 | +pub struct RemoteJwkSet { |
| 33 | + client: reqwest::Client, |
| 34 | + url: Url, |
| 35 | + jwks: Arc<Mutex<Option<Entry>>>, |
| 36 | +} |
| 37 | + |
| 38 | +impl RemoteJwkSet { |
| 39 | + pub(crate) fn new(client: reqwest::Client, url: Url) -> Self { |
| 40 | + RemoteJwkSet { |
| 41 | + client, |
| 42 | + url, |
| 43 | + jwks: Arc::new(Mutex::new(None)), |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + /// Find the key in the set that matches the given key id, if any. |
| 48 | + pub async fn find(&self, kid: &str) -> WorkOsResult<Option<Jwk>, FindJwkError> { |
| 49 | + { |
| 50 | + let jwks = self |
| 51 | + .jwks |
| 52 | + .lock() |
| 53 | + .map_err(|err| FindJwkError::Poison(err.to_string()))?; |
| 54 | + |
| 55 | + if let Some((jwks, expires_at)) = jwks.as_ref() |
| 56 | + && *expires_at > Utc::now().fixed_offset() |
| 57 | + { |
| 58 | + return Ok(jwks.find(kid).cloned()); |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + let new_jwks = self |
| 63 | + .client |
| 64 | + .get(self.url.as_str()) |
| 65 | + .send() |
| 66 | + .await? |
| 67 | + .handle_unauthorized_or_generic_error() |
| 68 | + .await? |
| 69 | + .json::<JwkSet>() |
| 70 | + .await?; |
| 71 | + |
| 72 | + let key = new_jwks.find(kid).cloned(); |
| 73 | + |
| 74 | + // TODO: Consider using the expiry of keys instead? |
| 75 | + let mut jwks = self |
| 76 | + .jwks |
| 77 | + .lock() |
| 78 | + .map_err(|err| FindJwkError::Poison(err.to_string()))?; |
| 79 | + |
| 80 | + *jwks = Some((new_jwks, Utc::now().fixed_offset() + TimeDelta::minutes(5))); |
| 81 | + |
| 82 | + Ok(key) |
| 83 | + } |
| 84 | +} |
0 commit comments