From 45f125a33139855c86ee1eafffa0475bc5597c0c Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Thu, 14 Apr 2022 18:04:01 +0200 Subject: [PATCH] Add code challenge support --- Cargo.lock | 2 + Cargo.toml | 4 +- src/actors/openid_sessions_actor.rs | 3 +- src/controllers/openid_controller.rs | 35 +++++++++++--- src/data/code_challenge.rs | 70 ++++++++++++++++++++++++++++ src/data/mod.rs | 3 +- src/lib.rs | 2 + src/utils/crypt_utils.rs | 6 +++ src/utils/mod.rs | 3 +- 9 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 src/data/code_challenge.rs create mode 100644 src/utils/crypt_utils.rs diff --git a/Cargo.lock b/Cargo.lock index 6c3a603..2cada7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -416,6 +416,7 @@ dependencies = [ "base64", "bcrypt", "clap", + "digest 0.10.3", "env_logger", "futures-util", "include_dir", @@ -426,6 +427,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sha2 0.10.2", "urlencoding", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index abda5ac..79c62d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,4 +24,6 @@ futures-util = "0.3.21" urlencoding = "2.1.0" rand = "0.8.5" base64 = "0.13.0" -jwt-simple = "0.10.9" \ No newline at end of file +jwt-simple = "0.10.9" +digest = "0.10.3" +sha2 = "0.10.2" \ No newline at end of file diff --git a/src/actors/openid_sessions_actor.rs b/src/actors/openid_sessions_actor.rs index b5aa546..30226e7 100644 --- a/src/actors/openid_sessions_actor.rs +++ b/src/actors/openid_sessions_actor.rs @@ -3,6 +3,7 @@ use actix::Message; use crate::constants::*; use crate::data::client::ClientID; +use crate::data::code_challenge::CodeChallenge; use crate::data::user::UserID; use crate::utils::time::time; @@ -27,7 +28,7 @@ pub struct Session { pub refresh_token_expire_at: u64, pub nonce: Option, - pub code_challenge: Option<(String, String)>, + pub code_challenge: Option, } impl Session { diff --git a/src/controllers/openid_controller.rs b/src/controllers/openid_controller.rs index cfbbeb1..cf6327d 100644 --- a/src/controllers/openid_controller.rs +++ b/src/controllers/openid_controller.rs @@ -12,6 +12,7 @@ use crate::constants::{AUTHORIZE_URI, CERT_URI, OPEN_ID_ACCESS_TOKEN_LEN, OPEN_I use crate::controllers::base_controller::FatalErrorPage; use crate::data::app_config::AppConfig; use crate::data::client::{ClientID, ClientManager}; +use crate::data::code_challenge::CodeChallenge; use crate::data::current_user::CurrentUser; use crate::data::id_token::IdToken; use crate::data::jwt_signer::{JsonWebKey, JWTSigner}; @@ -107,15 +108,16 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query { - if !meth.eq("S256") { + let code_challenge = match query.0.code_challenge.clone() { + Some(chal) => { + let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain"); + if !meth.eq("S256") && !meth.eq("plain") { return error_redirect(&query, "invalid_request", - "Only S256 code challenge is supported!"); + "Only S256 and plain code challenge methods are supported!"); } - Some((chal, meth)) + Some(CodeChallenge { code_challenge: chal, code_challenge_method: meth.to_string() }) } - (_, _) => None + _ => None }; // Check if user is authorized to access the application @@ -173,6 +175,7 @@ pub fn error_response(query: &D, error: &str, description: &str) -> Ht pub struct TokenAuthorizationCodeQuery { redirect_uri: String, code: String, + code_verifier: Option, } #[derive(Debug, serde::Deserialize)] @@ -286,8 +289,22 @@ pub async fn token(req: HttpRequest, return Ok(error_response(&query, "invalid_request", "Authorization code expired!")); } + // Check code challenge, if needed + if let Some(chall) = &session.code_challenge { + let code_verifier = match &q.code_verifier { + None => { + return Ok(error_response(&query, "access_denied", "Code verifier missing")); + } + Some(s) => s + }; + + if !chall.verify_code(code_verifier) { + return Ok(error_response(&query, "invalid_grant", "Invalid code verifier")); + } + } + if session.authorization_code_used { - return Ok(error_response(&query, "invalid_request", "Authorization already used!")); + return Ok(error_response(&query, "invalid_request", "Authorization code already used!")); } // Mark session as used @@ -330,6 +347,10 @@ pub async fn token(req: HttpRequest, return Ok(error_response(&query, "invalid_request", "Client mismatch!")); } + if session.refresh_token_expire_at < time() { + return Ok(error_response(&query, "access_denied", "Refresh token has expired!")); + } + session.refresh_token = rand_str(OPEN_ID_REFRESH_TOKEN_LEN); session.refresh_token_expire_at = OPEN_ID_REFRESH_TOKEN_TIMEOUT + time(); session.access_token = rand_str(OPEN_ID_ACCESS_TOKEN_LEN); diff --git a/src/data/code_challenge.rs b/src/data/code_challenge.rs new file mode 100644 index 0000000..464d2ce --- /dev/null +++ b/src/data/code_challenge.rs @@ -0,0 +1,70 @@ +use base64::URL_SAFE_NO_PAD; + +use crate::utils::crypt_utils::sha256; + +/// Code challenge, as specified in +/// +/// See some implementation help in +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct CodeChallenge { + pub code_challenge: String, + pub code_challenge_method: String, +} + +impl CodeChallenge { + pub fn verify_code(&self, code_verifer: &str) -> bool { + match self.code_challenge_method.as_str() { + "plain" => code_verifer.eq(&self.code_challenge), + "S256" => { + let encoded = base64::encode_config( + sha256(code_verifer.as_bytes()), + URL_SAFE_NO_PAD, + ); + + encoded.eq(&self.code_challenge) + } + s => { + log::error!("Unknown code challenge method: {}", s); + false + } + } + } +} + +#[cfg(test)] +mod test { + use crate::data::code_challenge::CodeChallenge; + + #[test] + fn test_plain() { + let chal = CodeChallenge { + code_challenge_method: "plain".to_string(), + code_challenge: "text1".to_string(), + }; + + assert_eq!(true, chal.verify_code("text1")); + assert_eq!(false, chal.verify_code("text2")); + } + + #[test] + fn test_s256() { + let chal = CodeChallenge { + code_challenge_method: "S256".to_string(), + code_challenge: "uSOvC48D8TMh6RgW-36XppMlMgys-6KAE_wEIev9W2g".to_string(), + }; + + assert_eq!(true, chal.verify_code("HIwht3lCHfnsruA+7Sq8NP2mPj5cBZe0Ewf23eK9UQhK4TdCIt3SK7Fr/giCdnfjxYQILOPG2D562emggAa2lA==")); + assert_eq!(false, chal.verify_code("text1")); + } + + #[test] + fn test_s256_2() { + let chal = CodeChallenge { + code_challenge_method: "S256".to_string(), + code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(), + }; + + assert_eq!(true, chal.verify_code("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")); + assert_eq!(false, chal.verify_code("text1")); + } +} \ No newline at end of file diff --git a/src/data/mod.rs b/src/data/mod.rs index 66ff2f6..7fcc00d 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -7,4 +7,5 @@ pub mod remote_ip; pub mod current_user; pub mod openid_config; pub mod jwt_signer; -pub mod id_token; \ No newline at end of file +pub mod id_token; +pub mod code_challenge; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 6a7e3e4..0ae0db4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +extern crate core; + pub mod actors; pub mod constants; pub mod controllers; diff --git a/src/utils/crypt_utils.rs b/src/utils/crypt_utils.rs new file mode 100644 index 0000000..14b2885 --- /dev/null +++ b/src/utils/crypt_utils.rs @@ -0,0 +1,6 @@ +use digest::Digest; + +#[inline] +pub fn sha256(input: &[u8]) -> Vec { + sha2::Sha256::digest(input).to_vec() +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs index fd21bed..dcc3635 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod err; pub mod time; pub mod network_utils; -pub mod string_utils; \ No newline at end of file +pub mod string_utils; +pub mod crypt_utils; \ No newline at end of file