Add code challenge support
This commit is contained in:
parent
0b64c88fc6
commit
45f125a331
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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",
|
||||||
]
|
]
|
||||||
|
@ -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"
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
70
src/data/code_challenge.rs
Normal file
70
src/data/code_challenge.rs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
@ -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
6
src/utils/crypt_utils.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use digest::Digest;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn sha256(input: &[u8]) -> Vec<u8> {
|
||||||
|
sha2::Sha256::digest(input).to_vec()
|
||||||
|
}
|
@ -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;
|
Loading…
Reference in New Issue
Block a user