Bläddra i källkod

Implemented simple token refresh.

Kestrel 11 månader sedan
förälder
incheckning
6c0c2aa573
2 ändrade filer med 88 tillägg och 15 borttagningar
  1. 80 12
      src/realm.rs
  2. 8 3
      src/server/oidc.rs

+ 80 - 12
src/realm.rs

@@ -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: &microrm::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: &microrm::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: &microrm::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))
     }
 }

+ 8 - 3
src/server/oidc.rs

@@ -338,9 +338,14 @@ async fn do_token<'l>(mut request: Request) -> Result<tide::Response, OIDCError<
                 None,
             ));
         };
-        let Ok((access, refresh)) = rhelper.trade_refresh_token(rtoken.as_str())
-        else {
-            todo!();
+
+        let (access, refresh) = match rhelper.trade_refresh_token(&client, rtoken.as_str()) {
+            Ok((a, r)) => (a, r),
+            Err(e) => return Err(OIDCError(
+                OIDCErrorType::InvalidRequest,
+                format!("could not trade refresh token: {e}"),
+                None,
+            ))
         };
         Ok(tide::Response::builder(200)
             .content_type(tide::http::mime::JSON)