|
@@ -1,5 +1,5 @@
|
|
|
use std::{
|
|
|
- collections::{HashMap, HashSet},
|
|
|
+ collections::HashMap,
|
|
|
sync::{Arc, RwLock},
|
|
|
};
|
|
|
|
|
@@ -22,6 +22,18 @@ pub struct AccessTokenClaims<'l> {
|
|
|
pub roles: Vec<String>,
|
|
|
}
|
|
|
|
|
|
+#[derive(serde::Serialize, serde::Deserialize)]
|
|
|
+pub struct RefreshTokenClaims {
|
|
|
+ pub sub: String,
|
|
|
+ pub iss: String,
|
|
|
+ pub aud: String,
|
|
|
+ pub iat: u64,
|
|
|
+ pub exp: u64,
|
|
|
+ #[serde(rename = "use")]
|
|
|
+ pub use_: String,
|
|
|
+ pub scopes: Vec<String>,
|
|
|
+}
|
|
|
+
|
|
|
pub struct RealmCache {
|
|
|
config: Config,
|
|
|
db: schema::UIDCDatabase,
|
|
@@ -140,6 +152,28 @@ impl RealmHelper {
|
|
|
f(self.encoding_key_cache.write().unwrap().entry(key.id()).or_insert(ekey))
|
|
|
}
|
|
|
|
|
|
+ fn with_decoding_key<R>(&self, key: µrm::Stored<schema::Key>, f: impl FnOnce(&jsonwebtoken::DecodingKey) -> R) -> R {
|
|
|
+ // check to see if the cache will work
|
|
|
+ if let Some(v) = self.decoding_key_cache.read().unwrap().get(&key.id()) {
|
|
|
+ return f(v)
|
|
|
+ }
|
|
|
+
|
|
|
+ // parse an EncodingKey out of the stored key
|
|
|
+ let ekey = match key.key_type.as_ref() {
|
|
|
+ KeyType::HMac(_) => {
|
|
|
+ jsonwebtoken::DecodingKey::from_secret(&key.secret_data)
|
|
|
+ },
|
|
|
+ KeyType::RSA2048 | KeyType::RSA4096 => {
|
|
|
+ jsonwebtoken::DecodingKey::from_rsa_der(&key.secret_data)
|
|
|
+ },
|
|
|
+ KeyType::Ed25519 => {
|
|
|
+ jsonwebtoken::DecodingKey::from_ed_der(&key.secret_data)
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ f(self.decoding_key_cache.write().unwrap().entry(key.id()).or_insert(ekey))
|
|
|
+ }
|
|
|
+
|
|
|
pub fn generate_access_token<'a>(
|
|
|
&self,
|
|
|
client: µrm::Stored<schema::Client>,
|
|
@@ -198,16 +232,14 @@ impl RealmHelper {
|
|
|
let iat = std::time::SystemTime::now();
|
|
|
let exp = iat + std::time::Duration::from_secs(self.config.refresh_token_expiry);
|
|
|
|
|
|
- let resulting_roles = self.determine_roles(&client, &user, scopes.clone())?;
|
|
|
-
|
|
|
- let atclaims = AccessTokenClaims {
|
|
|
- sub: user.username.as_str(),
|
|
|
- iss: self.issuer.as_str(),
|
|
|
- aud: client.shortname.as_str(),
|
|
|
+ let atclaims = RefreshTokenClaims {
|
|
|
+ sub: user.username.clone(),
|
|
|
+ iss: self.issuer.clone(),
|
|
|
+ aud: client.shortname.clone(),
|
|
|
iat: iat.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
|
|
|
exp: exp.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
|
|
|
- scopes: scopes.collect(),
|
|
|
- roles: resulting_roles,
|
|
|
+ scopes: scopes.map(str::to_string).collect(),
|
|
|
+ use_: "refresh".to_string(),
|
|
|
};
|
|
|
|
|
|
// find an active key that matches the required key type
|
|
@@ -225,7 +257,7 @@ impl RealmHelper {
|
|
|
return Err(UIDCError::Abort("no matching signing key"));
|
|
|
};
|
|
|
|
|
|
- let hdr = jsonwebtoken::Header::new(match ekey.key_type.as_ref() {
|
|
|
+ let mut hdr = jsonwebtoken::Header::new(match ekey.key_type.as_ref() {
|
|
|
KeyType::Ed25519 => jsonwebtoken::Algorithm::EdDSA,
|
|
|
KeyType::RSA2048 | KeyType::RSA4096 => jsonwebtoken::Algorithm::RS256,
|
|
|
KeyType::HMac(HMacType::Sha256) => {
|
|
@@ -236,13 +268,49 @@ impl RealmHelper {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+ hdr.kid = Some(ekey.key_id.clone());
|
|
|
+
|
|
|
self.with_encoding_key(&ekey, |ekey| {
|
|
|
jsonwebtoken::encode(&hdr, &atclaims, &ekey)
|
|
|
.map_err(|_| UIDCError::Abort("failed to sign token"))
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- pub fn trade_refresh_token(&self, rtoken: &str) -> Result<(String, String), UIDCError> {
|
|
|
- todo!()
|
|
|
+ pub fn trade_refresh_token<'l>(&self, client: µrm::Stored<schema::Client>, rtoken: &'l str) -> Result<(String, String), UIDCError> {
|
|
|
+ let header = jsonwebtoken::decode_header(rtoken).map_err(|e| UIDCError::AbortString(format!("invalid JWT header: {e}")))?;
|
|
|
+ let Some(kid) = header.kid else {
|
|
|
+ return Err(UIDCError::Abort("no kid in header"));
|
|
|
+ };
|
|
|
+
|
|
|
+ let Some(key) = self.realm.keys.with(schema::Key::KeyId, kid).first().get()? else {
|
|
|
+ return Err(UIDCError::Abort("no matching key"))
|
|
|
+ };
|
|
|
+ if *key.key_state.as_ref() == schema::KeyState::Retired {
|
|
|
+ return Err(UIDCError::Abort("signing key retired"))
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
|
|
|
+ validation.set_issuer(&[self.issuer.as_str()]);
|
|
|
+ validation.set_audience(&[client.shortname.as_str()]);
|
|
|
+
|
|
|
+ let Ok(rt) = self.with_decoding_key(&key, |dkey| {
|
|
|
+ jsonwebtoken::decode::<RefreshTokenClaims>(rtoken, dkey, &validation)
|
|
|
+ }) else {
|
|
|
+ return Err(UIDCError::Abort("token validation failed"));
|
|
|
+ };
|
|
|
+
|
|
|
+ if rt.claims.use_ != "refresh" {
|
|
|
+ return Err(UIDCError::Abort("mismatching token use"))
|
|
|
+ }
|
|
|
+
|
|
|
+ let Some(user) = self.realm.users.keyed((self.realm.id(), rt.claims.sub.as_str())).get()? else {
|
|
|
+ return Err(UIDCError::Abort("user no longer exists or was renamed"))
|
|
|
+ };
|
|
|
+
|
|
|
+ let scopes = rt.claims.scopes.iter().map(String::as_str);
|
|
|
+ let access_token = self.generate_access_token(client, &user, scopes.clone())?;
|
|
|
+ let refresh_token = self.generate_refresh_token(client, &user, scopes)?;
|
|
|
+
|
|
|
+ Ok((access_token, refresh_token))
|
|
|
}
|
|
|
}
|