1 Commits

Author SHA1 Message Date
2455f3aa56 Update Rust crate rand to 0.10.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2026-02-19 00:12:45 +00:00
12 changed files with 298 additions and 596 deletions

735
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,35 +8,38 @@ edition = "2024"
[dependencies] [dependencies]
actix = "0.13.5" actix = "0.13.5"
actix-identity = "0.9.0" actix-identity = "0.9.0"
actix-web = "4.13.0" actix-web = "4.12.1"
actix-session = { version = "0.11.0", features = ["cookie-session", "redis-session"] } actix-session = { version = "0.11.0", features = ["cookie-session", "redis-session"] }
actix-remote-ip = "0.1.0" actix-remote-ip = "0.1.0"
clap = { version = "4.6.0", features = ["derive", "env"] } clap = { version = "4.5.59", features = ["derive", "env"] }
include_dir = "0.7.4" include_dir = "0.7.4"
log = "0.4.29" log = "0.4.29"
serde_json = "1.0.149" serde_json = "1.0.149"
serde_yml = "0.0.12" serde_yaml = "0.9.34"
env_logger = "0.11.9" env_logger = "0.11.8"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
bcrypt = "0.19.0" bcrypt = "0.18.0"
uuid = { version = "1.22.0", features = ["v4"] } uuid = { version = "1.20.0", features = ["v4"] }
mime_guess = "2.0.5" mime_guess = "2.0.5"
askama = "0.15.4" askama = "0.15.4"
urlencoding = "2.1.3" urlencoding = "2.1.3"
rand = "0.10.0" rand = "0.10.0"
base64 = "0.22.1" base64 = "0.22.1"
jwt-simple = { version = "0.12.14", default-features = false, features = ["pure-rust"] } jwt-simple = { version = "0.12.14", default-features = false, features = ["pure-rust"] }
sha2 = "0.11.0-rc.5" digest = "0.11.0"
lazy-regex = "3.6.0" sha2 = "0.11.0-rc.4"
lazy-regex = "3.5.1"
totp_rfc6238 = "0.6.1" totp_rfc6238 = "0.6.1"
base32 = "0.5.1" base32 = "0.5.1"
qrcode-generator = "5.0.0" qrcode-generator = "5.0.0"
webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation"] } webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation"] }
url = "2.5.8" url = "2.5.8"
light-openid = { version = "1.1.0", features = ["crypto-wrapper"] } light-openid = { version = "1.1.0", features = ["crypto-wrapper"] }
rkyv = "0.8.15" rkyv = "0.8.14"
chrono = "0.4.44" chrono = "0.4.43"
mailchecker = "6.0.20" lazy_static = "1.5.0"
mailchecker = "6.0.19"
httpdate = "1.0.3" httpdate = "1.0.3"
build-time = "0.1.3" build-time = "0.1.3"
hex = "0.4.3" hex = "0.4.3"
anyhow = "1.0.101"

View File

@@ -31,7 +31,7 @@ You can configure a list of clients (Relying Parties) in a `clients.yaml` file w
enforce_2fa_auth: true enforce_2fa_auth: true
# Optional, claims to be added to the ID token payload. # Optional, claims to be added to the ID token payload.
# The following placeholders can be set, they will the replaced when the token is created: # The following placeholders can be set, they will the replaced when the token is created:
# * {username}: username of the user # * {username}: user name of the user
# * {mail}: email address of the user # * {mail}: email address of the user
# * {first_name}: first name of the user # * {first_name}: first name of the user
# * {last_name}: last name of the user # * {last_name}: last name of the user
@@ -39,11 +39,6 @@ You can configure a list of clients (Relying Parties) in a `clients.yaml` file w
claims_id_token: claims_id_token:
groups: ["group_{user}"] groups: ["group_{user}"]
service: "auth" service: "auth"
# Optional, claims to be added to the access token payload
# The placeholders of `claims_id_token` can also be used here
claims_access_token:
groups: ["group_{user}"]
service: "auth"
# Optional, claims to be added to the user info endpoint response # Optional, claims to be added to the user info endpoint response
# The placeholders of `claims_id_token` can also be used here # The placeholders of `claims_id_token` can also be used here
claims_user_info: claims_user_info:

View File

@@ -4,10 +4,10 @@ use actix::{Actor, AsyncContext, Context, Handler};
use crate::constants::*; use crate::constants::*;
use crate::data::access_token::AccessToken; use crate::data::access_token::AccessToken;
use crate::data::app_config::AppConfig; use crate::data::app_config::AppConfig;
use crate::data::client::{Client, ClientID}; use crate::data::client::ClientID;
use crate::data::code_challenge::CodeChallenge; use crate::data::code_challenge::CodeChallenge;
use crate::data::jwt_signer::JWTSigner; use crate::data::jwt_signer::JWTSigner;
use crate::data::user::{User, UserID}; use crate::data::user::UserID;
use crate::utils::err::Res; use crate::utils::err::Res;
use crate::utils::string_utils::rand_str; use crate::utils::string_utils::rand_str;
use crate::utils::time_utils::time; use crate::utils::time_utils::time;
@@ -17,20 +17,17 @@ pub struct SessionID(pub String);
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Session { pub struct Session {
pub session_id: SessionID,
pub client: ClientID, pub client: ClientID,
pub user: UserID, pub user: UserID,
pub auth_time: u64, pub auth_time: u64,
pub redirect_uri: String, pub redirect_uri: String,
pub session_id: SessionID,
pub session_expire_at: u64,
pub authorization_code: String, pub authorization_code: String,
pub authorization_code_expire_at: u64, pub authorization_code_expire_at: u64,
pub access_token: Option<String>, pub access_token: Option<String>,
pub access_token_expire_at: u64, pub access_token_expire_at: u64,
pub refresh_token: String, pub refresh_token: String,
pub refresh_token_expire_at: u64, pub refresh_token_expire_at: u64,
@@ -40,23 +37,19 @@ pub struct Session {
impl Session { impl Session {
pub fn is_expired(&self) -> bool { pub fn is_expired(&self) -> bool {
self.session_expire_at < time() self.authorization_code_expire_at < time()
|| (self.authorization_code_expire_at < time() && self.access_token_expire_at < time()
&& self.access_token_expire_at < time() && self.refresh_token_expire_at < time()
&& self.refresh_token_expire_at < time())
} }
pub fn regenerate_access_and_refresh_tokens( pub fn regenerate_access_and_refresh_tokens(
&mut self, &mut self,
app_config: &AppConfig, app_config: &AppConfig,
jwt_signer: &JWTSigner, jwt_signer: &JWTSigner,
user: &User,
client: &Client,
) -> Res { ) -> Res {
let access_token = AccessToken { let access_token = AccessToken {
issuer: app_config.website_origin.to_string(), issuer: app_config.website_origin.to_string(),
user, subject_identifier: self.user.clone().0,
client,
issued_at: time(), issued_at: time(),
exp_time: time() + OPEN_ID_ACCESS_TOKEN_TIMEOUT, exp_time: time() + OPEN_ID_ACCESS_TOKEN_TIMEOUT,
rand_val: rand_str(OPEN_ID_ACCESS_TOKEN_LEN), rand_val: rand_str(OPEN_ID_ACCESS_TOKEN_LEN),

View File

@@ -64,8 +64,7 @@ pub const USERINFO_URI: &str = "/openid/userinfo";
/// Open ID constants /// Open ID constants
pub const OPEN_ID_SESSION_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); pub const OPEN_ID_SESSION_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
pub const OPEN_ID_SESSION_ID_LEN: usize = 40; pub const OPEN_ID_SESSION_LEN: usize = 40;
pub const OPEN_ID_SESSION_MAX_DURATION: Duration = Duration::from_secs(3600 * 24 * 7);
pub const OPEN_ID_AUTHORIZATION_CODE_LEN: usize = 120; pub const OPEN_ID_AUTHORIZATION_CODE_LEN: usize = 120;
pub const OPEN_ID_AUTHORIZATION_CODE_TIMEOUT: u64 = 300; pub const OPEN_ID_AUTHORIZATION_CODE_TIMEOUT: u64 = 300;
pub const OPEN_ID_ACCESS_TOKEN_LEN: usize = 50; pub const OPEN_ID_ACCESS_TOKEN_LEN: usize = 50;

View File

@@ -219,12 +219,11 @@ pub async fn authorize(
(_, "code") => { (_, "code") => {
// Save all authentication information in memory // Save all authentication information in memory
let session = Session { let session = Session {
session_id: SessionID(rand_str(OPEN_ID_SESSION_LEN)),
client: client.id.clone(), client: client.id.clone(),
user: user.uid.clone(), user: user.uid.clone(),
auth_time: SessionIdentity(Some(&id)).auth_time(), auth_time: SessionIdentity(Some(&id)).auth_time(),
redirect_uri, redirect_uri,
session_id: SessionID(rand_str(OPEN_ID_SESSION_ID_LEN)),
session_expire_at: time() + OPEN_ID_SESSION_MAX_DURATION.as_secs(),
authorization_code: rand_str(OPEN_ID_AUTHORIZATION_CODE_LEN), authorization_code: rand_str(OPEN_ID_AUTHORIZATION_CODE_LEN),
authorization_code_expire_at: time() + OPEN_ID_AUTHORIZATION_CODE_TIMEOUT, authorization_code_expire_at: time() + OPEN_ID_AUTHORIZATION_CODE_TIMEOUT,
access_token: None, access_token: None,
@@ -501,7 +500,13 @@ pub async fn token(
)); ));
} }
// Get user information session.regenerate_access_and_refresh_tokens(AppConfig::get(), &jwt_signer)?;
sessions
.send(openid_sessions_actor::UpdateSession(session.clone()))
.await
.unwrap();
let user: Option<User> = users let user: Option<User> = users
.send(users_actor::GetUserRequest(session.user.clone())) .send(users_actor::GetUserRequest(session.user.clone()))
.await .await
@@ -512,18 +517,6 @@ pub async fn token(
Some(u) => u, Some(u) => u,
}; };
// Refresh access and refresh tokens
session.regenerate_access_and_refresh_tokens(
AppConfig::get(),
&jwt_signer,
&user,
&client,
)?;
sessions
.send(openid_sessions_actor::UpdateSession(session.clone()))
.await
.unwrap();
// Generate id token // Generate id token
let id_token = IdToken { let id_token = IdToken {
issuer: AppConfig::get().website_origin.to_string(), issuer: AppConfig::get().website_origin.to_string(),
@@ -580,24 +573,8 @@ pub async fn token(
)); ));
} }
// Get user information session.regenerate_access_and_refresh_tokens(AppConfig::get(), &jwt_signer)?;
let user: Option<User> = users
.send(users_actor::GetUserRequest(session.user.clone()))
.await
.unwrap()
.0;
let user = match user {
None => return Ok(error_response(&query, "invalid_request", "User not found!")),
Some(u) => u,
};
// Regenerate user session
session.regenerate_access_and_refresh_tokens(
AppConfig::get(),
&jwt_signer,
&user,
&client,
)?;
sessions sessions
.send(openid_sessions_actor::UpdateSession(session.clone())) .send(openid_sessions_actor::UpdateSession(session.clone()))
.await .await

View File

@@ -1,12 +1,9 @@
use crate::data::client::{AdditionalClaims, Client};
use crate::data::user::User;
use jwt_simple::claims::JWTClaims; use jwt_simple::claims::JWTClaims;
use jwt_simple::prelude::Duration; use jwt_simple::prelude::Duration;
pub struct AccessToken<'a> { pub struct AccessToken {
pub issuer: String, pub issuer: String,
pub user: &'a User, pub subject_identifier: String,
pub client: &'a Client,
pub issued_at: u64, pub issued_at: u64,
pub exp_time: u64, pub exp_time: u64,
pub rand_val: String, pub rand_val: String,
@@ -16,26 +13,21 @@ pub struct AccessToken<'a> {
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
pub struct CustomAccessTokenClaims { pub struct CustomAccessTokenClaims {
rand_val: String, rand_val: String,
/// Additional claims
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub additional_claims: Option<AdditionalClaims>,
} }
impl<'a> AccessToken<'a> { impl AccessToken {
pub fn to_jwt_claims(self) -> JWTClaims<CustomAccessTokenClaims> { pub fn to_jwt_claims(self) -> JWTClaims<CustomAccessTokenClaims> {
JWTClaims { JWTClaims {
issued_at: Some(Duration::from_secs(self.issued_at)), issued_at: Some(Duration::from_secs(self.issued_at)),
expires_at: Some(Duration::from_secs(self.exp_time)), expires_at: Some(Duration::from_secs(self.exp_time)),
invalid_before: None, invalid_before: None,
issuer: Some(self.issuer), issuer: Some(self.issuer),
subject: Some(self.user.uid.0.to_string()), subject: Some(self.subject_identifier),
audiences: None, audiences: None,
jwt_id: None, jwt_id: None,
nonce: self.nonce, nonce: self.nonce,
custom: CustomAccessTokenClaims { custom: CustomAccessTokenClaims {
rand_val: self.rand_val, rand_val: self.rand_val,
additional_claims: self.client.claims_access_token(self.user),
}, },
} }
} }

View File

@@ -1,6 +1,6 @@
use clap::Parser;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use clap::Parser;
use crate::constants::{ use crate::constants::{
APP_NAME, CLIENTS_LIST_FILE, OIDC_PROVIDER_CB_URI, PROVIDERS_LIST_FILE, USERS_LIST_FILE, APP_NAME, CLIENTS_LIST_FILE, OIDC_PROVIDER_CB_URI, PROVIDERS_LIST_FILE, USERS_LIST_FILE,
@@ -93,21 +93,23 @@ pub struct AppConfig {
redis_password: String, redis_password: String,
} }
static ARGS: OnceLock<AppConfig> = OnceLock::new(); lazy_static::lazy_static! {
static ref ARGS: AppConfig = {
let mut config = AppConfig::parse();
// In debug mode only, use dummy token
if cfg!(debug_assertions) && config.token_key.is_empty() {
config.token_key = String::from_utf8_lossy(&[32; 64]).to_string();
}
config
};
}
impl AppConfig { impl AppConfig {
/// Get parsed command line arguments /// Get parsed command line arguments
pub fn get() -> &'static AppConfig { pub fn get() -> &'static AppConfig {
ARGS.get_or_init(|| { &ARGS
let mut config = AppConfig::parse();
// In debug mode only, use dummy token
if cfg!(debug_assertions) && config.token_key.is_empty() {
config.token_key = String::from_utf8_lossy(&[32; 64]).to_string();
}
config
})
} }
pub fn secure_cookie(&self) -> bool { pub fn secure_cookie(&self) -> bool {

View File

@@ -42,9 +42,6 @@ pub struct Client {
/// Additional claims to return with the id token /// Additional claims to return with the id token
claims_id_token: Option<AdditionalClaims>, claims_id_token: Option<AdditionalClaims>,
/// Additional claims to return with the access token
claims_access_token: Option<AdditionalClaims>,
/// Additional claims to return through the user info endpoint /// Additional claims to return through the user info endpoint
claims_user_info: Option<AdditionalClaims>, claims_user_info: Option<AdditionalClaims>,
} }
@@ -120,11 +117,6 @@ impl Client {
self.process_additional_claims(user, &self.claims_id_token) self.process_additional_claims(user, &self.claims_id_token)
} }
/// Get additional claims for access_token for a successful authentication
pub fn claims_access_token(&self, user: &User) -> Option<AdditionalClaims> {
self.process_additional_claims(user, &self.claims_access_token)
}
/// Get additional claims for user info for a successful authentication /// Get additional claims for user info for a successful authentication
pub fn claims_user_info(&self, user: &User) -> Option<AdditionalClaims> { pub fn claims_user_info(&self, user: &User) -> Option<AdditionalClaims> {
self.process_additional_claims(user, &self.claims_user_info) self.process_additional_claims(user, &self.claims_user_info)

View File

@@ -36,7 +36,7 @@ where
file_path: path.as_ref().to_path_buf(), file_path: path.as_ref().to_path_buf(),
list: match Self::file_format(path.as_ref()) { list: match Self::file_format(path.as_ref()) {
FileFormat::Json => serde_json::from_str(&file_content)?, FileFormat::Json => serde_json::from_str(&file_content)?,
FileFormat::Yaml => serde_yml::from_str(&file_content)?, FileFormat::Yaml => serde_yaml::from_str(&file_content)?,
}, },
}) })
} }
@@ -47,7 +47,7 @@ where
&self.file_path, &self.file_path,
match Self::file_format(self.file_path.as_ref()) { match Self::file_format(self.file_path.as_ref()) {
FileFormat::Json => serde_json::to_string(&self.list)?, FileFormat::Json => serde_json::to_string(&self.list)?,
FileFormat::Yaml => serde_yml::to_string(&self.list)?, FileFormat::Yaml => serde_yaml::to_string(&self.list)?,
}, },
)?) )?)
} }

View File

@@ -1,5 +1,5 @@
use base32::Alphabet; use base32::Alphabet;
use rand::RngExt; use rand::Rng;
use totp_rfc6238::{HashAlgorithm, TotpGenerator}; use totp_rfc6238::{HashAlgorithm, TotpGenerator};
use crate::data::app_config::AppConfig; use crate::data::app_config::AppConfig;

View File

@@ -1,4 +1,4 @@
use sha2::Digest; use digest::Digest;
#[inline] #[inline]
pub fn sha256(input: &[u8]) -> Vec<u8> { pub fn sha256(input: &[u8]) -> Vec<u8> {