Add code challenge support

This commit is contained in:
Pierre HUBERT 2022-04-14 18:04:01 +02:00
parent 0b64c88fc6
commit 45f125a331
9 changed files with 117 additions and 11 deletions

2
Cargo.lock generated
View File

@ -416,6 +416,7 @@ dependencies = [
"base64", "base64",
"bcrypt", "bcrypt",
"clap", "clap",
"digest 0.10.3",
"env_logger", "env_logger",
"futures-util", "futures-util",
"include_dir", "include_dir",
@ -426,6 +427,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"sha2 0.10.2",
"urlencoding", "urlencoding",
"uuid", "uuid",
] ]

View File

@ -25,3 +25,5 @@ urlencoding = "2.1.0"
rand = "0.8.5" rand = "0.8.5"
base64 = "0.13.0" base64 = "0.13.0"
jwt-simple = "0.10.9" jwt-simple = "0.10.9"
digest = "0.10.3"
sha2 = "0.10.2"

View File

@ -3,6 +3,7 @@ use actix::Message;
use crate::constants::*; use crate::constants::*;
use crate::data::client::ClientID; use crate::data::client::ClientID;
use crate::data::code_challenge::CodeChallenge;
use crate::data::user::UserID; use crate::data::user::UserID;
use crate::utils::time::time; use crate::utils::time::time;
@ -27,7 +28,7 @@ pub struct Session {
pub refresh_token_expire_at: u64, pub refresh_token_expire_at: u64,
pub nonce: Option<String>, pub nonce: Option<String>,
pub code_challenge: Option<(String, String)>, pub code_challenge: Option<CodeChallenge>,
} }
impl Session { impl Session {

View File

@ -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::controllers::base_controller::FatalErrorPage;
use crate::data::app_config::AppConfig; use crate::data::app_config::AppConfig;
use crate::data::client::{ClientID, ClientManager}; use crate::data::client::{ClientID, ClientManager};
use crate::data::code_challenge::CodeChallenge;
use crate::data::current_user::CurrentUser; use crate::data::current_user::CurrentUser;
use crate::data::id_token::IdToken; use crate::data::id_token::IdToken;
use crate::data::jwt_signer::{JsonWebKey, JWTSigner}; use crate::data::jwt_signer::{JsonWebKey, JWTSigner};
@ -107,15 +108,16 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
return error_redirect(&query, "invalid_request", "State is empty!"); return error_redirect(&query, "invalid_request", "State is empty!");
} }
let code_challenge = match (query.0.code_challenge.clone(), query.0.code_challenge_method.clone()) { let code_challenge = match query.0.code_challenge.clone() {
(Some(chal), Some(meth)) => { Some(chal) => {
if !meth.eq("S256") { 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", 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 // Check if user is authorized to access the application
@ -173,6 +175,7 @@ pub fn error_response<D: Debug>(query: &D, error: &str, description: &str) -> Ht
pub struct TokenAuthorizationCodeQuery { pub struct TokenAuthorizationCodeQuery {
redirect_uri: String, redirect_uri: String,
code: String, code: String,
code_verifier: Option<String>,
} }
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
@ -286,8 +289,22 @@ pub async fn token(req: HttpRequest,
return Ok(error_response(&query, "invalid_request", "Authorization code expired!")); 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 { 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 // Mark session as used
@ -330,6 +347,10 @@ pub async fn token(req: HttpRequest,
return Ok(error_response(&query, "invalid_request", "Client mismatch!")); 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 = rand_str(OPEN_ID_REFRESH_TOKEN_LEN);
session.refresh_token_expire_at = OPEN_ID_REFRESH_TOKEN_TIMEOUT + time(); session.refresh_token_expire_at = OPEN_ID_REFRESH_TOKEN_TIMEOUT + time();
session.access_token = rand_str(OPEN_ID_ACCESS_TOKEN_LEN); session.access_token = rand_str(OPEN_ID_ACCESS_TOKEN_LEN);

View File

@ -0,0 +1,70 @@
use base64::URL_SAFE_NO_PAD;
use crate::utils::crypt_utils::sha256;
/// Code challenge, as specified in <https://datatracker.ietf.org/doc/rfc7636/>
///
/// See some implementation help in <https://docs.hidglobal.com/activid-as-v8.5/api/openid/leverage-pkce-auth-code-grant-flow.htm>
#[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"));
}
}

View File

@ -8,3 +8,4 @@ pub mod current_user;
pub mod openid_config; pub mod openid_config;
pub mod jwt_signer; pub mod jwt_signer;
pub mod id_token; pub mod id_token;
pub mod code_challenge;

View File

@ -1,3 +1,5 @@
extern crate core;
pub mod actors; pub mod actors;
pub mod constants; pub mod constants;
pub mod controllers; pub mod controllers;

6
src/utils/crypt_utils.rs Normal file
View File

@ -0,0 +1,6 @@
use digest::Digest;
#[inline]
pub fn sha256(input: &[u8]) -> Vec<u8> {
sha2::Sha256::digest(input).to_vec()
}

View File

@ -2,3 +2,4 @@ pub mod err;
pub mod time; pub mod time;
pub mod network_utils; pub mod network_utils;
pub mod string_utils; pub mod string_utils;
pub mod crypt_utils;