Compare commits
15 Commits
d5ad1dbf2e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c8c50660c | |||
| 0482e1f0cc | |||
| 6fcfacfab2 | |||
| ddaf74739d | |||
| c538f01148 | |||
| 1c2cd39305 | |||
| 316e2bfaa5 | |||
| 1a92085d70 | |||
| 2268e7cbff | |||
| 4801ed7cf9 | |||
|
|
7391a4f488 | ||
| cc72ff64d2 | |||
| 3482c53acf | |||
| 84c3415ad7 | |||
| cc2c3d7626 |
735
Cargo.lock
generated
735
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@@ -8,38 +8,35 @@ edition = "2024"
|
||||
[dependencies]
|
||||
actix = "0.13.5"
|
||||
actix-identity = "0.9.0"
|
||||
actix-web = "4.12.1"
|
||||
actix-web = "4.13.0"
|
||||
actix-session = { version = "0.11.0", features = ["cookie-session", "redis-session"] }
|
||||
actix-remote-ip = "0.1.0"
|
||||
clap = { version = "4.5.59", features = ["derive", "env"] }
|
||||
clap = { version = "4.6.0", features = ["derive", "env"] }
|
||||
include_dir = "0.7.4"
|
||||
log = "0.4.29"
|
||||
serde_json = "1.0.149"
|
||||
serde_yaml = "0.9.34"
|
||||
env_logger = "0.11.8"
|
||||
serde_yml = "0.0.12"
|
||||
env_logger = "0.11.9"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
bcrypt = "0.18.0"
|
||||
uuid = { version = "1.20.0", features = ["v4"] }
|
||||
bcrypt = "0.19.0"
|
||||
uuid = { version = "1.22.0", features = ["v4"] }
|
||||
mime_guess = "2.0.5"
|
||||
askama = "0.15.4"
|
||||
urlencoding = "2.1.3"
|
||||
rand = "0.9.0"
|
||||
rand = "0.10.0"
|
||||
base64 = "0.22.1"
|
||||
jwt-simple = { version = "0.12.14", default-features = false, features = ["pure-rust"] }
|
||||
digest = "0.11.0"
|
||||
sha2 = "0.11.0-rc.5"
|
||||
lazy-regex = "3.5.1"
|
||||
lazy-regex = "3.6.0"
|
||||
totp_rfc6238 = "0.6.1"
|
||||
base32 = "0.5.1"
|
||||
qrcode-generator = "5.0.0"
|
||||
webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation"] }
|
||||
url = "2.5.8"
|
||||
light-openid = { version = "1.1.0", features = ["crypto-wrapper"] }
|
||||
rkyv = "0.8.14"
|
||||
chrono = "0.4.43"
|
||||
lazy_static = "1.5.0"
|
||||
mailchecker = "6.0.19"
|
||||
rkyv = "0.8.15"
|
||||
chrono = "0.4.44"
|
||||
mailchecker = "6.0.20"
|
||||
httpdate = "1.0.3"
|
||||
build-time = "0.1.3"
|
||||
hex = "0.4.3"
|
||||
anyhow = "1.0.101"
|
||||
hex = "0.4.3"
|
||||
@@ -31,7 +31,7 @@ You can configure a list of clients (Relying Parties) in a `clients.yaml` file w
|
||||
enforce_2fa_auth: true
|
||||
# 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:
|
||||
# * {username}: user name of the user
|
||||
# * {username}: username of the user
|
||||
# * {mail}: email address of the user
|
||||
# * {first_name}: first name of the user
|
||||
# * {last_name}: last name of the user
|
||||
@@ -39,6 +39,11 @@ You can configure a list of clients (Relying Parties) in a `clients.yaml` file w
|
||||
claims_id_token:
|
||||
groups: ["group_{user}"]
|
||||
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
|
||||
# The placeholders of `claims_id_token` can also be used here
|
||||
claims_user_info:
|
||||
|
||||
@@ -4,10 +4,10 @@ use actix::{Actor, AsyncContext, Context, Handler};
|
||||
use crate::constants::*;
|
||||
use crate::data::access_token::AccessToken;
|
||||
use crate::data::app_config::AppConfig;
|
||||
use crate::data::client::ClientID;
|
||||
use crate::data::client::{Client, ClientID};
|
||||
use crate::data::code_challenge::CodeChallenge;
|
||||
use crate::data::jwt_signer::JWTSigner;
|
||||
use crate::data::user::UserID;
|
||||
use crate::data::user::{User, UserID};
|
||||
use crate::utils::err::Res;
|
||||
use crate::utils::string_utils::rand_str;
|
||||
use crate::utils::time_utils::time;
|
||||
@@ -17,17 +17,20 @@ pub struct SessionID(pub String);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Session {
|
||||
pub session_id: SessionID,
|
||||
pub client: ClientID,
|
||||
pub user: UserID,
|
||||
pub auth_time: u64,
|
||||
pub redirect_uri: String,
|
||||
|
||||
pub session_id: SessionID,
|
||||
pub session_expire_at: u64,
|
||||
|
||||
pub authorization_code: String,
|
||||
pub authorization_code_expire_at: u64,
|
||||
|
||||
pub access_token: Option<String>,
|
||||
pub access_token_expire_at: u64,
|
||||
|
||||
pub refresh_token: String,
|
||||
pub refresh_token_expire_at: u64,
|
||||
|
||||
@@ -37,19 +40,23 @@ pub struct Session {
|
||||
|
||||
impl Session {
|
||||
pub fn is_expired(&self) -> bool {
|
||||
self.authorization_code_expire_at < time()
|
||||
&& self.access_token_expire_at < time()
|
||||
&& self.refresh_token_expire_at < time()
|
||||
self.session_expire_at < time()
|
||||
|| (self.authorization_code_expire_at < time()
|
||||
&& self.access_token_expire_at < time()
|
||||
&& self.refresh_token_expire_at < time())
|
||||
}
|
||||
|
||||
pub fn regenerate_access_and_refresh_tokens(
|
||||
&mut self,
|
||||
app_config: &AppConfig,
|
||||
jwt_signer: &JWTSigner,
|
||||
user: &User,
|
||||
client: &Client,
|
||||
) -> Res {
|
||||
let access_token = AccessToken {
|
||||
issuer: app_config.website_origin.to_string(),
|
||||
subject_identifier: self.user.clone().0,
|
||||
user,
|
||||
client,
|
||||
issued_at: time(),
|
||||
exp_time: time() + OPEN_ID_ACCESS_TOKEN_TIMEOUT,
|
||||
rand_val: rand_str(OPEN_ID_ACCESS_TOKEN_LEN),
|
||||
|
||||
@@ -64,7 +64,8 @@ pub const USERINFO_URI: &str = "/openid/userinfo";
|
||||
|
||||
/// Open ID constants
|
||||
pub const OPEN_ID_SESSION_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
|
||||
pub const OPEN_ID_SESSION_LEN: usize = 40;
|
||||
pub const OPEN_ID_SESSION_ID_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_TIMEOUT: u64 = 300;
|
||||
pub const OPEN_ID_ACCESS_TOKEN_LEN: usize = 50;
|
||||
|
||||
@@ -219,11 +219,12 @@ pub async fn authorize(
|
||||
(_, "code") => {
|
||||
// Save all authentication information in memory
|
||||
let session = Session {
|
||||
session_id: SessionID(rand_str(OPEN_ID_SESSION_LEN)),
|
||||
client: client.id.clone(),
|
||||
user: user.uid.clone(),
|
||||
auth_time: SessionIdentity(Some(&id)).auth_time(),
|
||||
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_expire_at: time() + OPEN_ID_AUTHORIZATION_CODE_TIMEOUT,
|
||||
access_token: None,
|
||||
@@ -500,13 +501,7 @@ pub async fn token(
|
||||
));
|
||||
}
|
||||
|
||||
session.regenerate_access_and_refresh_tokens(AppConfig::get(), &jwt_signer)?;
|
||||
|
||||
sessions
|
||||
.send(openid_sessions_actor::UpdateSession(session.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get user information
|
||||
let user: Option<User> = users
|
||||
.send(users_actor::GetUserRequest(session.user.clone()))
|
||||
.await
|
||||
@@ -517,6 +512,18 @@ pub async fn token(
|
||||
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
|
||||
let id_token = IdToken {
|
||||
issuer: AppConfig::get().website_origin.to_string(),
|
||||
@@ -573,8 +580,24 @@ pub async fn token(
|
||||
));
|
||||
}
|
||||
|
||||
session.regenerate_access_and_refresh_tokens(AppConfig::get(), &jwt_signer)?;
|
||||
// Get user information
|
||||
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
|
||||
.send(openid_sessions_actor::UpdateSession(session.clone()))
|
||||
.await
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::data::client::{AdditionalClaims, Client};
|
||||
use crate::data::user::User;
|
||||
use jwt_simple::claims::JWTClaims;
|
||||
use jwt_simple::prelude::Duration;
|
||||
|
||||
pub struct AccessToken {
|
||||
pub struct AccessToken<'a> {
|
||||
pub issuer: String,
|
||||
pub subject_identifier: String,
|
||||
pub user: &'a User,
|
||||
pub client: &'a Client,
|
||||
pub issued_at: u64,
|
||||
pub exp_time: u64,
|
||||
pub rand_val: String,
|
||||
@@ -13,21 +16,26 @@ pub struct AccessToken {
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct CustomAccessTokenClaims {
|
||||
rand_val: String,
|
||||
/// Additional claims
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(flatten)]
|
||||
pub additional_claims: Option<AdditionalClaims>,
|
||||
}
|
||||
|
||||
impl AccessToken {
|
||||
impl<'a> AccessToken<'a> {
|
||||
pub fn to_jwt_claims(self) -> JWTClaims<CustomAccessTokenClaims> {
|
||||
JWTClaims {
|
||||
issued_at: Some(Duration::from_secs(self.issued_at)),
|
||||
expires_at: Some(Duration::from_secs(self.exp_time)),
|
||||
invalid_before: None,
|
||||
issuer: Some(self.issuer),
|
||||
subject: Some(self.subject_identifier),
|
||||
subject: Some(self.user.uid.0.to_string()),
|
||||
audiences: None,
|
||||
jwt_id: None,
|
||||
nonce: self.nonce,
|
||||
custom: CustomAccessTokenClaims {
|
||||
rand_val: self.rand_val,
|
||||
additional_claims: self.client.claims_access_token(self.user),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::Parser;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::constants::{
|
||||
APP_NAME, CLIENTS_LIST_FILE, OIDC_PROVIDER_CB_URI, PROVIDERS_LIST_FILE, USERS_LIST_FILE,
|
||||
@@ -93,23 +93,21 @@ pub struct AppConfig {
|
||||
redis_password: String,
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
static ARGS: OnceLock<AppConfig> = OnceLock::new();
|
||||
|
||||
impl AppConfig {
|
||||
/// Get parsed command line arguments
|
||||
pub fn get() -> &'static AppConfig {
|
||||
&ARGS
|
||||
ARGS.get_or_init(|| {
|
||||
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 {
|
||||
|
||||
@@ -42,6 +42,9 @@ pub struct Client {
|
||||
/// Additional claims to return with the id token
|
||||
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
|
||||
claims_user_info: Option<AdditionalClaims>,
|
||||
}
|
||||
@@ -117,6 +120,11 @@ impl Client {
|
||||
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
|
||||
pub fn claims_user_info(&self, user: &User) -> Option<AdditionalClaims> {
|
||||
self.process_additional_claims(user, &self.claims_user_info)
|
||||
|
||||
@@ -36,7 +36,7 @@ where
|
||||
file_path: path.as_ref().to_path_buf(),
|
||||
list: match Self::file_format(path.as_ref()) {
|
||||
FileFormat::Json => serde_json::from_str(&file_content)?,
|
||||
FileFormat::Yaml => serde_yaml::from_str(&file_content)?,
|
||||
FileFormat::Yaml => serde_yml::from_str(&file_content)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -47,7 +47,7 @@ where
|
||||
&self.file_path,
|
||||
match Self::file_format(self.file_path.as_ref()) {
|
||||
FileFormat::Json => serde_json::to_string(&self.list)?,
|
||||
FileFormat::Yaml => serde_yaml::to_string(&self.list)?,
|
||||
FileFormat::Yaml => serde_yml::to_string(&self.list)?,
|
||||
},
|
||||
)?)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use base32::Alphabet;
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use totp_rfc6238::{HashAlgorithm, TotpGenerator};
|
||||
|
||||
use crate::data::app_config::AppConfig;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use digest::Digest;
|
||||
use sha2::Digest;
|
||||
|
||||
#[inline]
|
||||
pub fn sha256(input: &[u8]) -> Vec<u8> {
|
||||
|
||||
Reference in New Issue
Block a user