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",
|
||||
"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",
|
||||
]
|
||||
|
@ -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"
|
||||
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::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<String>,
|
||||
pub code_challenge: Option<(String, String)>,
|
||||
pub code_challenge: Option<CodeChallenge>,
|
||||
}
|
||||
|
||||
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::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<Author
|
||||
return error_redirect(&query, "invalid_request", "State is empty!");
|
||||
}
|
||||
|
||||
let code_challenge = match (query.0.code_challenge.clone(), query.0.code_challenge_method.clone()) {
|
||||
(Some(chal), Some(meth)) => {
|
||||
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<D: Debug>(query: &D, error: &str, description: &str) -> Ht
|
||||
pub struct TokenAuthorizationCodeQuery {
|
||||
redirect_uri: String,
|
||||
code: String,
|
||||
code_verifier: Option<String>,
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
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"));
|
||||
}
|
||||
}
|
@ -7,4 +7,5 @@ pub mod remote_ip;
|
||||
pub mod current_user;
|
||||
pub mod openid_config;
|
||||
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 constants;
|
||||
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()
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
pub mod err;
|
||||
pub mod time;
|
||||
pub mod network_utils;
|
||||
pub mod string_utils;
|
||||
pub mod string_utils;
|
||||
pub mod crypt_utils;
|
Loading…
Reference in New Issue
Block a user