mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
406 session revocation (#1123)
This commit is contained in:
parent
8b6c25fac5
commit
a55c0ca68d
|
@ -432,9 +432,11 @@ impl KanidmClient {
|
|||
builder.build()
|
||||
}
|
||||
|
||||
pub async fn logout(&self) {
|
||||
pub async fn logout(&self) -> Result<(), ClientError> {
|
||||
self.perform_get_request("/v1/logout").await?;
|
||||
let mut tguard = self.bearer_token.write().await;
|
||||
*tguard = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn perform_simple_post_request<R: Serialize, T: DeserializeOwned>(
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use kanidm_proto::v1::{AccountUnixExtend, CredentialStatus, Entry, SingleStringRequest};
|
||||
use kanidm_proto::v1::{
|
||||
AccountUnixExtend, CredentialStatus, Entry, SingleStringRequest, UatStatus,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{ClientError, KanidmClient};
|
||||
|
||||
|
@ -231,4 +234,28 @@ impl KanidmClient {
|
|||
self.perform_delete_request(format!("/v1/person/{}/_radius", id).as_str())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_list_user_auth_token(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Vec<UatStatus>, ClientError> {
|
||||
self.perform_get_request(format!("/v1/account/{}/_user_auth_token", id).as_str())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_destroy_user_auth_token(
|
||||
&self,
|
||||
id: &str,
|
||||
token_id: Uuid,
|
||||
) -> Result<(), ClientError> {
|
||||
self.perform_delete_request(
|
||||
format!(
|
||||
"/v1/account/{}/_user_auth_token/{}",
|
||||
id,
|
||||
&token_id.to_string()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -321,6 +321,45 @@ pub enum UiHint {
|
|||
PosixAccount,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UatPurposeStatus {
|
||||
IdentityOnly,
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct UatStatus {
|
||||
pub account_id: Uuid,
|
||||
pub session_id: Uuid,
|
||||
#[serde(with = "time::serde::timestamp::option")]
|
||||
pub expiry: Option<time::OffsetDateTime>,
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub issued_at: time::OffsetDateTime,
|
||||
pub purpose: UatPurposeStatus,
|
||||
}
|
||||
|
||||
impl fmt::Display for UatStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "account_id: {}", self.account_id)?;
|
||||
writeln!(f, "session_id: {}", self.session_id)?;
|
||||
if let Some(exp) = self.expiry {
|
||||
writeln!(f, "expiry: {}", exp)?;
|
||||
} else {
|
||||
writeln!(f, "expiry: -")?;
|
||||
}
|
||||
writeln!(f, "issued_at: {}", self.issued_at)?;
|
||||
match &self.purpose {
|
||||
UatPurposeStatus::IdentityOnly => writeln!(f, "purpose: identity only")?,
|
||||
UatPurposeStatus::ReadOnly => writeln!(f, "purpose: read only")?,
|
||||
UatPurposeStatus::ReadWrite => writeln!(f, "purpose: read write")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UatPurpose {
|
||||
|
@ -345,10 +384,10 @@ pub enum UatPurpose {
|
|||
pub struct UserAuthToken {
|
||||
pub session_id: Uuid,
|
||||
pub auth_type: AuthType,
|
||||
// When this token should be considered expired. Interpretation
|
||||
// may depend on the client application.
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub expiry: time::OffsetDateTime,
|
||||
pub issued_at: time::OffsetDateTime,
|
||||
#[serde(with = "time::serde::timestamp::option")]
|
||||
pub expiry: Option<time::OffsetDateTime>,
|
||||
pub purpose: UatPurpose,
|
||||
pub uuid: Uuid,
|
||||
pub displayname: String,
|
||||
|
@ -363,7 +402,11 @@ impl fmt::Display for UserAuthToken {
|
|||
writeln!(f, "spn: {}", self.spn)?;
|
||||
writeln!(f, "uuid: {}", self.uuid)?;
|
||||
writeln!(f, "display: {}", self.displayname)?;
|
||||
writeln!(f, "expiry: {}", self.expiry)?;
|
||||
if let Some(exp) = self.expiry {
|
||||
writeln!(f, "expiry: {}", exp)?;
|
||||
} else {
|
||||
writeln!(f, "expiry: -")?;
|
||||
}
|
||||
match &self.purpose {
|
||||
UatPurpose::IdentityOnly => writeln!(f, "purpose: identity only")?,
|
||||
UatPurpose::ReadOnly => writeln!(f, "purpose: read only")?,
|
||||
|
|
|
@ -121,7 +121,8 @@ impl CommonOpt {
|
|||
.map(|jws: Jws<UserAuthToken>| jws.into_inner())
|
||||
{
|
||||
Ok(uat) => {
|
||||
if time::OffsetDateTime::now_utc() >= uat.expiry {
|
||||
if let Some(exp) = uat.expiry {
|
||||
if time::OffsetDateTime::now_utc() >= exp {
|
||||
error!(
|
||||
"Session has expired for {} - you may need to login again.",
|
||||
uat.spn
|
||||
|
@ -129,6 +130,7 @@ impl CommonOpt {
|
|||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Unable to read token for requested user - you may need to login again.");
|
||||
debug!(?e, "JWT Error");
|
||||
|
|
|
@ -17,8 +17,8 @@ use webauthn_authenticator_rs::u2fhid::U2FHid;
|
|||
use webauthn_authenticator_rs::WebauthnAuthenticator;
|
||||
|
||||
use crate::{
|
||||
password_prompt, AccountCredential, AccountRadius, AccountSsh, AccountValidity, PersonOpt,
|
||||
PersonPosix,
|
||||
password_prompt, AccountCredential, AccountRadius, AccountSsh, AccountUserAuthToken,
|
||||
AccountValidity, PersonOpt, PersonPosix,
|
||||
};
|
||||
|
||||
impl PersonOpt {
|
||||
|
@ -35,6 +35,10 @@ impl PersonOpt {
|
|||
PersonPosix::Set(apo) => apo.copt.debug,
|
||||
PersonPosix::SetPassword(apo) => apo.copt.debug,
|
||||
},
|
||||
PersonOpt::Session { commands } => match commands {
|
||||
AccountUserAuthToken::Status(apo) => apo.copt.debug,
|
||||
AccountUserAuthToken::Destroy { copt, .. } => copt.debug,
|
||||
},
|
||||
PersonOpt::Ssh { commands } => match commands {
|
||||
AccountSsh::List(ano) => ano.copt.debug,
|
||||
AccountSsh::Add(ano) => ano.copt.debug,
|
||||
|
@ -166,6 +170,46 @@ impl PersonOpt {
|
|||
}
|
||||
}
|
||||
}, // end PersonOpt::Posix
|
||||
PersonOpt::Session { commands } => match commands {
|
||||
AccountUserAuthToken::Status(apo) => {
|
||||
let client = apo.copt.to_client().await;
|
||||
match client
|
||||
.idm_account_list_user_auth_token(apo.aopts.account_id.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(tokens) => {
|
||||
if tokens.is_empty() {
|
||||
println!("No sessions exist");
|
||||
} else {
|
||||
for token in tokens {
|
||||
println!("token: {}", token);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error listing sessions -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
AccountUserAuthToken::Destroy {
|
||||
aopts,
|
||||
copt,
|
||||
session_id,
|
||||
} => {
|
||||
let client = copt.to_client().await;
|
||||
match client
|
||||
.idm_account_destroy_user_auth_token(aopts.account_id.as_str(), *session_id)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
println!("Success");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error destroying account session -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, // End PersonOpt::Session
|
||||
PersonOpt::Ssh { commands } => match commands {
|
||||
AccountSsh::List(aopt) => {
|
||||
let client = aopt.copt.to_client().await;
|
||||
|
|
|
@ -2,8 +2,8 @@ use kanidm_proto::messages::{AccountChangeMessage, ConsoleOutputMode, MessageSta
|
|||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
AccountSsh, AccountValidity, ServiceAccountApiToken, ServiceAccountCredential,
|
||||
ServiceAccountOpt, ServiceAccountPosix,
|
||||
AccountSsh, AccountUserAuthToken, AccountValidity, ServiceAccountApiToken,
|
||||
ServiceAccountCredential, ServiceAccountOpt, ServiceAccountPosix,
|
||||
};
|
||||
|
||||
impl ServiceAccountOpt {
|
||||
|
@ -22,6 +22,10 @@ impl ServiceAccountOpt {
|
|||
ServiceAccountPosix::Show(apo) => apo.copt.debug,
|
||||
ServiceAccountPosix::Set(apo) => apo.copt.debug,
|
||||
},
|
||||
ServiceAccountOpt::Session { commands } => match commands {
|
||||
AccountUserAuthToken::Status(apo) => apo.copt.debug,
|
||||
AccountUserAuthToken::Destroy { copt, .. } => copt.debug,
|
||||
},
|
||||
ServiceAccountOpt::Ssh { commands } => match commands {
|
||||
AccountSsh::List(ano) => ano.copt.debug,
|
||||
AccountSsh::Add(ano) => ano.copt.debug,
|
||||
|
@ -188,6 +192,46 @@ impl ServiceAccountOpt {
|
|||
}
|
||||
}
|
||||
}, // end ServiceAccountOpt::Posix
|
||||
ServiceAccountOpt::Session { commands } => match commands {
|
||||
AccountUserAuthToken::Status(apo) => {
|
||||
let client = apo.copt.to_client().await;
|
||||
match client
|
||||
.idm_account_list_user_auth_token(apo.aopts.account_id.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(tokens) => {
|
||||
if tokens.is_empty() {
|
||||
println!("No sessions exist");
|
||||
} else {
|
||||
for token in tokens {
|
||||
println!("token: {}", token);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error listing sessions -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
AccountUserAuthToken::Destroy {
|
||||
aopts,
|
||||
copt,
|
||||
session_id,
|
||||
} => {
|
||||
let client = copt.to_client().await;
|
||||
match client
|
||||
.idm_account_destroy_user_auth_token(aopts.account_id.as_str(), *session_id)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
println!("Success");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error destroying account session -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, // End ServiceAccountOpt::Session
|
||||
ServiceAccountOpt::Ssh { commands } => match commands {
|
||||
AccountSsh::List(aopt) => {
|
||||
let client = aopt.copt.to_client().await;
|
||||
|
|
|
@ -372,33 +372,66 @@ impl LoginOpt {
|
|||
|
||||
impl LogoutOpt {
|
||||
pub fn debug(&self) -> bool {
|
||||
self.debug
|
||||
self.copt.debug
|
||||
}
|
||||
|
||||
pub async fn exec(&self) {
|
||||
let username: String = if self.local_only {
|
||||
// For now we just remove this from the token store.
|
||||
|
||||
let mut _tmp_username = String::new();
|
||||
let username = match &self.username {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
_tmp_username = match prompt_for_username_get_username() {
|
||||
match &self.copt.username {
|
||||
Some(value) => value.clone(),
|
||||
None => match prompt_for_username_get_username() {
|
||||
Ok(value) => value,
|
||||
Err(msg) => {
|
||||
error!("{}", msg);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
&_tmp_username
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let client = self.copt.to_client().await;
|
||||
let token = match client.get_token().await {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
error!("Client token store is empty/corrupt");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
// Parse it for the username.
|
||||
|
||||
if let Err(e) = client.logout().await {
|
||||
error!("Failed to logout - {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Server acked the logout, lets proceed with the local cleanup now.
|
||||
let jwtu = match JwsUnverified::from_str(&token) {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
error!(?e, "Unable to parse token from str");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let uat: UserAuthToken = match jwtu.validate_embeded() {
|
||||
Ok(jwt) => jwt.into_inner(),
|
||||
Err(e) => {
|
||||
error!(?e, "Unable to verify token signature, may be corrupt");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
uat.name().to_string()
|
||||
};
|
||||
|
||||
let mut tokens = read_tokens().unwrap_or_else(|_| {
|
||||
error!("Error retrieving authentication token store");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// Remove our old one
|
||||
if tokens.remove(username).is_some() {
|
||||
if tokens.remove(&username).is_some() {
|
||||
// write them out.
|
||||
if let Err(_e) = write_tokens(&tokens) {
|
||||
error!("Error persisting authentication token store");
|
||||
|
@ -463,12 +496,16 @@ impl SessionOpt {
|
|||
let tokens: BTreeMap<_, _> = tokens
|
||||
.into_iter()
|
||||
.filter_map(|(u, (t, uat))| {
|
||||
if now >= uat.expiry {
|
||||
if let Some(exp) = uat.expiry {
|
||||
if now >= exp {
|
||||
//Expired
|
||||
None
|
||||
} else {
|
||||
Some((u, t))
|
||||
}
|
||||
} else {
|
||||
Some((u, t))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
|
@ -278,6 +278,25 @@ pub enum AccountValidity {
|
|||
BeginFrom(AccountNamedValidDateTimeOpt),
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AccountUserAuthToken {
|
||||
/// Show the status of logged in sessions associated to this account.
|
||||
#[clap(name = "status")]
|
||||
Status(AccountNamedOpt),
|
||||
/// Destroy / revoke a session for this account. Access to the
|
||||
/// session (user auth token) is NOT required, only the uuid of the session.
|
||||
#[clap(name = "destroy")]
|
||||
Destroy {
|
||||
#[clap(flatten)]
|
||||
aopts: AccountCommonOpt,
|
||||
#[clap(flatten)]
|
||||
copt: CommonOpt,
|
||||
/// The UUID of the token to destroy.
|
||||
#[clap(name = "session_id")]
|
||||
session_id: Uuid,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum PersonOpt {
|
||||
/// Manage the credentials this person uses for authentication
|
||||
|
@ -298,6 +317,12 @@ pub enum PersonOpt {
|
|||
#[clap(subcommand)]
|
||||
commands: PersonPosix,
|
||||
},
|
||||
/// Manage sessions (user auth tokens) associated to this person.
|
||||
#[clap(name = "session")]
|
||||
Session {
|
||||
#[clap(subcommand)]
|
||||
commands: AccountUserAuthToken,
|
||||
},
|
||||
/// Manage ssh public key's associated to this person
|
||||
#[clap(name = "ssh")]
|
||||
Ssh {
|
||||
|
@ -417,6 +442,12 @@ pub enum ServiceAccountOpt {
|
|||
#[clap(subcommand)]
|
||||
commands: ServiceAccountPosix,
|
||||
},
|
||||
/// Manage sessions (user auth tokens) associated to this service account.
|
||||
#[clap(name = "session")]
|
||||
Session {
|
||||
#[clap(subcommand)]
|
||||
commands: AccountUserAuthToken,
|
||||
},
|
||||
/// Manage ssh public key's associated to this person
|
||||
#[clap(name = "ssh")]
|
||||
Ssh {
|
||||
|
@ -474,14 +505,11 @@ pub struct LoginOpt {
|
|||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct LogoutOpt {
|
||||
#[clap(short, long, env = "KANIDM_DEBUG")]
|
||||
pub debug: bool,
|
||||
#[clap(short = 'H', long = "url", env = "KANIDM_URL")]
|
||||
pub addr: Option<String>,
|
||||
#[clap(parse(from_os_str), short = 'C', long = "ca", env = "KANIDM_CA_PATH")]
|
||||
pub ca_path: Option<PathBuf>,
|
||||
#[clap()]
|
||||
pub username: Option<String>,
|
||||
#[clap(flatten)]
|
||||
copt: CommonOpt,
|
||||
#[clap(short, long, hide = true)]
|
||||
/// Do not send the logout to the server - only remove the session token locally
|
||||
local_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::sync::Arc;
|
|||
|
||||
use kanidm_proto::v1::{
|
||||
ApiToken, AuthRequest, BackupCodesView, CURequest, CUSessionToken, CUStatus, CredentialStatus,
|
||||
Entry as ProtoEntry, OperationError, RadiusAuthToken, SearchRequest, SearchResponse,
|
||||
Entry as ProtoEntry, OperationError, RadiusAuthToken, SearchRequest, SearchResponse, UatStatus,
|
||||
UnixGroupToken, UnixUserToken, WhoamiResponse,
|
||||
};
|
||||
use ldap3_proto::simple::*;
|
||||
|
@ -18,6 +18,7 @@ use kanidmd_lib::prelude::*;
|
|||
use kanidmd_lib::{
|
||||
event::{OnlineBackupEvent, SearchEvent, SearchResult, WhoamiResult},
|
||||
filter::{Filter, FilterInvalid},
|
||||
idm::account::ListUserAuthTokenEvent,
|
||||
idm::credupdatesession::CredentialUpdateSessionToken,
|
||||
idm::event::{
|
||||
AuthEvent, AuthResult, CredentialStatusEvent, RadiusAuthTokenEvent, ReadBackupCodeEvent,
|
||||
|
@ -765,6 +766,38 @@ impl QueryServerReadV1 {
|
|||
idms_prox_read.service_account_list_api_token(<e)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_account_user_auth_token_get(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
uuid_or_name: String,
|
||||
eventid: Uuid,
|
||||
) -> Result<Vec<UatStatus>, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_prox_read = self.idms.proxy_read_async().await;
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
let target = idms_prox_read
|
||||
.qs_read
|
||||
.name_to_uuid(uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
admin_error!("Error resolving id to target");
|
||||
e
|
||||
})?;
|
||||
|
||||
let lte = ListUserAuthTokenEvent { ident, target };
|
||||
|
||||
idms_prox_read.account_list_user_auth_tokens(<e)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
|
|
|
@ -3,9 +3,9 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
|
||||
use kanidm_proto::v1::{
|
||||
AccountUnixExtend, CUIntentToken, CUSessionToken, CUStatus, CreateRequest, DeleteRequest,
|
||||
Entry as ProtoEntry, GroupUnixExtend, Modify as ProtoModify, ModifyList as ProtoModifyList,
|
||||
ModifyRequest, OperationError,
|
||||
AccountUnixExtend, AuthType, CUIntentToken, CUSessionToken, CUStatus, CreateRequest,
|
||||
DeleteRequest, Entry as ProtoEntry, GroupUnixExtend, Modify as ProtoModify,
|
||||
ModifyList as ProtoModifyList, ModifyRequest, OperationError,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{info, instrument, span, trace, Level};
|
||||
|
@ -17,6 +17,7 @@ use kanidmd_lib::{
|
|||
ReviveRecycledEvent,
|
||||
},
|
||||
filter::{Filter, FilterInvalid},
|
||||
idm::account::DestroySessionTokenEvent,
|
||||
idm::credupdatesession::{
|
||||
CredentialUpdateIntentToken, CredentialUpdateSessionToken, InitCredentialUpdateEvent,
|
||||
InitCredentialUpdateIntentEvent,
|
||||
|
@ -498,6 +499,91 @@ impl QueryServerWriteV1 {
|
|||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_account_user_auth_token_destroy(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
uuid_or_name: String,
|
||||
token_id: Uuid,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
})?;
|
||||
|
||||
let target = idms_prox_write
|
||||
.qs_write
|
||||
.name_to_uuid(uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Error resolving id to target");
|
||||
e
|
||||
})?;
|
||||
|
||||
let dte = DestroySessionTokenEvent {
|
||||
ident,
|
||||
target,
|
||||
token_id,
|
||||
};
|
||||
|
||||
idms_prox_write
|
||||
.account_destroy_session_token(&dte)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_logout(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
|
||||
// We specifically need a uat here to assess the auth type!
|
||||
let (ident, uat) = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| {
|
||||
idms_prox_write
|
||||
.process_uat_to_identity(&uat, ct)
|
||||
.map(|ident| (ident, uat))
|
||||
})?;
|
||||
|
||||
if uat.auth_type == AuthType::Anonymous {
|
||||
info!("Ignoring request to logout anonymous session - these sessions are not recorded");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let target = ident.get_uuid().ok_or_else(|| {
|
||||
admin_error!("Invalid identity - no uuid present");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
|
||||
let token_id = ident.get_session_id();
|
||||
|
||||
let dte = DestroySessionTokenEvent {
|
||||
ident,
|
||||
target,
|
||||
token_id,
|
||||
};
|
||||
|
||||
idms_prox_write
|
||||
.account_destroy_session_token(&dte)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
|
|
|
@ -535,6 +535,8 @@ pub fn create_https_server(
|
|||
.at("/v1/auth/valid")
|
||||
.mapped_get(&mut routemap, auth_valid);
|
||||
|
||||
appserver.at("/v1/logout").mapped_get(&mut routemap, logout);
|
||||
|
||||
let mut schema_route = appserver.at("/v1/schema");
|
||||
schema_route.at("/").mapped_get(&mut routemap, schema_get);
|
||||
schema_route
|
||||
|
@ -753,6 +755,12 @@ pub fn create_https_server(
|
|||
account_route
|
||||
.at("/:id/_ssh_pubkeys/:tag")
|
||||
.mapped_get(&mut routemap, account_get_id_ssh_pubkey_tag);
|
||||
account_route
|
||||
.at("/:id/_user_auth_token")
|
||||
.mapped_get(&mut routemap, account_get_id_user_auth_token);
|
||||
account_route
|
||||
.at("/:id/_user_auth_token/:token_id")
|
||||
.mapped_delete(&mut routemap, account_user_auth_token_delete);
|
||||
|
||||
// Credential updates, don't require the account id.
|
||||
let mut cred_route = appserver.at("/v1/credential");
|
||||
|
|
|
@ -65,6 +65,13 @@ pub async fn whoami(req: tide::Request<AppState>) -> tide::Result {
|
|||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn logout(req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req.state().qe_w_ref.handle_logout(uat, eventid).await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
// =============== REST generics ========================
|
||||
|
||||
pub async fn json_rest_event_get(
|
||||
|
@ -574,6 +581,35 @@ pub async fn account_get_id_credential_update_intent(req: tide::Request<AppState
|
|||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn account_get_id_user_auth_token(req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_r_ref
|
||||
.handle_account_user_auth_token_get(uat, uuid_or_name, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn account_user_auth_token_delete(req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let token_id = req.get_url_param_uuid("token_id")?;
|
||||
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_account_user_auth_token_destroy(uat, uuid_or_name, token_id, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn credential_update_exchange_intent(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let intent_token: CUIntentToken = req.body_json().await?;
|
||||
|
|
|
@ -366,5 +366,8 @@ async fn test_oauth2_openid_basic_flow() {
|
|||
.await
|
||||
.expect("Unable to decode OidcToken from userinfo");
|
||||
|
||||
tracing::trace!(?userinfo);
|
||||
tracing::trace!(?oidc);
|
||||
|
||||
assert!(userinfo == oidc);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
use std::time::SystemTime;
|
||||
|
||||
use kanidm_proto::v1::{
|
||||
ApiToken, CURegState, CredentialDetailType, Entry, Filter, Modify, ModifyList,
|
||||
ApiToken, CURegState, CredentialDetailType, Entry, Filter, Modify, ModifyList, UserAuthToken,
|
||||
};
|
||||
use kanidmd_lib::credential::totp::Totp;
|
||||
use tracing::debug;
|
||||
|
@ -1246,3 +1246,99 @@ async fn test_server_api_token_lifecycle() {
|
|||
|
||||
// No need to test expiry, that's validated in the server internal tests.
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_user_auth_token_lifecycle() {
|
||||
let rsclient = setup_async_test().await;
|
||||
let res = rsclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
// Not recommended in production!
|
||||
rsclient
|
||||
.idm_group_add_members("idm_admins", &["admin"])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
rsclient
|
||||
.idm_person_account_create("demo_account", "Deeeeemo")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// First, show there are no auth sessions.
|
||||
let sessions = rsclient
|
||||
.idm_account_list_user_auth_token("demo_account")
|
||||
.await
|
||||
.expect("Failed to list user auth tokens");
|
||||
assert!(sessions.is_empty());
|
||||
|
||||
// Setup the credentials for the account
|
||||
|
||||
{
|
||||
// Create an intent token for them
|
||||
let intent_token = rsclient
|
||||
.idm_person_account_credential_update_intent("demo_account")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Logout, we don't need any auth now.
|
||||
let _ = rsclient.logout();
|
||||
// Exchange the intent token
|
||||
let (session_token, _status) = rsclient
|
||||
.idm_account_credential_update_exchange(intent_token)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Setup and update the password
|
||||
let _status = rsclient
|
||||
.idm_account_credential_update_set_password(&session_token, "eicieY7ahchaoCh0eeTa")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Commit it
|
||||
rsclient
|
||||
.idm_account_credential_update_commit(&session_token)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Auth as the user.
|
||||
|
||||
let _ = rsclient.logout();
|
||||
let res = rsclient
|
||||
.auth_simple_password("demo_account", "eicieY7ahchaoCh0eeTa")
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
let token = rsclient.get_token().await.expect("No bearer token present");
|
||||
|
||||
let token_unverified =
|
||||
JwsUnverified::from_str(&token).expect("Failed to parse user auth token");
|
||||
|
||||
let token: UserAuthToken = token_unverified
|
||||
.validate_embeded()
|
||||
.map(|j| j.into_inner())
|
||||
.expect("Embedded jwk not found");
|
||||
|
||||
let sessions = rsclient
|
||||
.idm_account_list_user_auth_token("demo_account")
|
||||
.await
|
||||
.expect("Failed to list user auth tokens");
|
||||
|
||||
assert!(sessions[0].session_id == token.session_id);
|
||||
|
||||
// idm_account_destroy_user_auth_token
|
||||
rsclient
|
||||
.idm_account_destroy_user_auth_token("demo_account", token.session_id)
|
||||
.await
|
||||
.expect("Failed to destroy user auth token");
|
||||
|
||||
let tokens = rsclient
|
||||
.idm_service_account_list_api_token("demo_account")
|
||||
.await
|
||||
.expect("Failed to list user auth tokens");
|
||||
assert!(tokens.is_empty());
|
||||
|
||||
// No need to test expiry, that's validated in the server internal tests.
|
||||
}
|
||||
|
|
|
@ -6,6 +6,10 @@ if [ -z "$KANI_TMP" ]; then
|
|||
KANI_TMP=/tmp/kanidm
|
||||
fi
|
||||
|
||||
if [ -z "$KANI_CARGO_OPTS" ]; then
|
||||
KANI_CARGO_OPTS=--debug
|
||||
fi
|
||||
|
||||
CONFIG_FILE="../../examples/insecure_server.toml"
|
||||
|
||||
if [ ! -f "${CONFIG_FILE}" ]; then
|
||||
|
@ -28,4 +32,4 @@ if [ -n "${1}" ]; then
|
|||
fi
|
||||
|
||||
#shellcheck disable=SC2086
|
||||
cargo run --bin kanidmd -- ${COMMAND} -c "${CONFIG_FILE}"
|
||||
cargo run ${KANI_CARGO_OPTS} --bin kanidmd -- ${COMMAND} -c "${CONFIG_FILE}"
|
||||
|
|
|
@ -101,6 +101,7 @@ pub const JSON_IDM_SELF_ACP_READ_V1: &str = r#"{
|
|||
"account_expire",
|
||||
"account_valid_from",
|
||||
"primary_credential",
|
||||
"user_auth_token_session",
|
||||
"passkeys",
|
||||
"devicekeys"
|
||||
]
|
||||
|
@ -120,7 +121,7 @@ pub const JSON_IDM_SELF_ACP_WRITE_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"person\"]}, {\"eq\": [\"class\",\"account\"]}, \"self\"]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey", "unix_password", "passkeys", "devicekeys"
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey", "unix_password", "passkeys", "devicekeys", "user_auth_token_session"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey", "unix_password", "passkeys", "devicekeys"
|
||||
|
@ -247,6 +248,9 @@ pub const JSON_IDM_ACP_PEOPLE_MANAGE_PRIV_V1: &str = r#"{
|
|||
"displayname",
|
||||
"legalname",
|
||||
"primary_credential",
|
||||
"passkeys",
|
||||
"devicekeys",
|
||||
"user_auth_token_session",
|
||||
"ssh_publickey",
|
||||
"mail"
|
||||
],
|
||||
|
@ -434,7 +438,7 @@ pub const JSON_IDM_ACP_ACCOUNT_READ_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "mail", "gidnumber", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "mail", "gidnumber", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session", "user_auth_token_session"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -456,7 +460,7 @@ pub const JSON_IDM_ACP_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session", "user_auth_token_session"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
|
@ -591,7 +595,7 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_READ_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session", "user_auth_token_session"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -613,7 +617,7 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session", "user_auth_token_session"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
|
|
|
@ -14,6 +14,8 @@ pub use crate::constants::system_config::*;
|
|||
pub use crate::constants::uuids::*;
|
||||
pub use crate::constants::values::*;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
// Increment this as we add new schema types and values!!!
|
||||
pub const SYSTEM_INDEX_VERSION: i64 = 26;
|
||||
// On test builds, define to 60 seconds
|
||||
|
@ -45,3 +47,12 @@ pub const PW_MIN_LENGTH: usize = 10;
|
|||
|
||||
// Default
|
||||
pub const AUTH_SESSION_EXPIRY: u64 = 3600;
|
||||
|
||||
// The time that a token can be used before session
|
||||
// status is enforced. This needs to be longer than
|
||||
// replication delay/cycle.
|
||||
pub const GRACE_WINDOW: Duration = Duration::from_secs(600);
|
||||
|
||||
/// How long access tokens should last. This is NOT the length
|
||||
/// of the refresh token, which is bound to the issuing session.
|
||||
pub const OAUTH2_ACCESS_TOKEN_EXPIRY: u32 = 4 * 3600;
|
||||
|
|
|
@ -1140,6 +1140,37 @@ pub const JSON_SCHEMA_ATTR_API_TOKEN_SESSION: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"A session entry related to an issued user auth token"
|
||||
],
|
||||
"index": [
|
||||
"EQUALITY"
|
||||
],
|
||||
"unique": [
|
||||
"true"
|
||||
],
|
||||
"multivalue": [
|
||||
"true"
|
||||
],
|
||||
"attributename": [
|
||||
"user_auth_token_session"
|
||||
],
|
||||
"syntax": [
|
||||
"SESSION"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000113"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
// === classes ===
|
||||
|
||||
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
||||
|
@ -1279,7 +1310,8 @@ pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#"
|
|||
"account_expire",
|
||||
"account_valid_from",
|
||||
"mail",
|
||||
"oauth2_consent_scope_map"
|
||||
"oauth2_consent_scope_map",
|
||||
"user_auth_token_session"
|
||||
],
|
||||
"systemmust": [
|
||||
"displayname",
|
||||
|
|
|
@ -192,6 +192,8 @@ pub const _UUID_SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY: Uuid =
|
|||
pub const _UUID_SCHEMA_ATTR_API_TOKEN_SESSION: Uuid = uuid!("00000000-0000-0000-0000-ffff00000111");
|
||||
pub const _UUID_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000112");
|
||||
pub const _UUID_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000113");
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
|
|
|
@ -491,7 +491,7 @@ impl Filter<FilterInvalid> {
|
|||
// This has to have two versions to account for ro/rw traits, because RS can't
|
||||
// monomorphise on the trait to call clone_value. An option is to make a fn that
|
||||
// takes "clone_value(t, a, v) instead, but that may have a similar issue.
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
#[instrument(name = "filter::from_ro", level = "debug", skip_all)]
|
||||
pub fn from_ro(
|
||||
ev: &Identity,
|
||||
f: &ProtoFilter,
|
||||
|
@ -506,7 +506,7 @@ impl Filter<FilterInvalid> {
|
|||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
#[instrument(name = "filter::from_rw", level = "debug", skip_all)]
|
||||
pub fn from_rw(
|
||||
ev: &Identity,
|
||||
f: &ProtoFilter,
|
||||
|
@ -521,7 +521,7 @@ impl Filter<FilterInvalid> {
|
|||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
#[instrument(name = "filter::from_ldap_ro", level = "debug", skip_all)]
|
||||
pub fn from_ldap_ro(
|
||||
ev: &Identity,
|
||||
f: &LdapFilter,
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
use uuid::uuid;
|
||||
|
||||
use kanidm_proto::v1::ApiTokenPurpose;
|
||||
use kanidm_proto::v1::{ApiTokenPurpose, UatPurpose, UatPurposeStatus};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -87,6 +88,29 @@ impl TryInto<ApiTokenPurpose> for AccessScope {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&UatPurpose> for AccessScope {
|
||||
fn from(purpose: &UatPurpose) -> Self {
|
||||
match purpose {
|
||||
UatPurpose::IdentityOnly => AccessScope::IdentityOnly,
|
||||
UatPurpose::ReadOnly => AccessScope::ReadOnly,
|
||||
UatPurpose::ReadWrite { .. } => AccessScope::ReadWrite,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<UatPurposeStatus> for AccessScope {
|
||||
type Error = OperationError;
|
||||
|
||||
fn try_into(self: AccessScope) -> Result<UatPurposeStatus, OperationError> {
|
||||
match self {
|
||||
AccessScope::ReadOnly => Ok(UatPurposeStatus::ReadOnly),
|
||||
AccessScope::ReadWrite => Ok(UatPurposeStatus::ReadWrite),
|
||||
AccessScope::IdentityOnly => Ok(UatPurposeStatus::IdentityOnly),
|
||||
AccessScope::Synchronise => Err(OperationError::InvalidEntryState),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Metadata and the entry of the current Identity which is an external account/user.
|
||||
pub struct IdentUser {
|
||||
|
@ -122,11 +146,14 @@ impl From<&IdentType> for IdentityId {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// An identity that initiated an `Event`.
|
||||
/// An identity that initiated an `Event`. Contains extra details about the session
|
||||
/// and other info that can assist with server decision making.
|
||||
pub struct Identity {
|
||||
pub origin: IdentType,
|
||||
// pub(crate) source:
|
||||
// pub(crate) impersonate: bool,
|
||||
// In a way I guess these are session claims?
|
||||
pub(crate) session_id: Uuid,
|
||||
pub(crate) scope: AccessScope,
|
||||
pub(crate) limits: Limits,
|
||||
}
|
||||
|
@ -139,9 +166,10 @@ impl std::fmt::Display for Identity {
|
|||
let nv = u.entry.get_uuid2spn();
|
||||
write!(
|
||||
f,
|
||||
"User( {}, {} ) ({})",
|
||||
"User( {}, {} ) ({}, {})",
|
||||
nv.to_proto_string_clone(),
|
||||
u.entry.get_uuid().as_hyphenated(),
|
||||
self.session_id,
|
||||
self.scope
|
||||
)
|
||||
}
|
||||
|
@ -153,6 +181,7 @@ impl Identity {
|
|||
pub fn from_internal() -> Self {
|
||||
Identity {
|
||||
origin: IdentType::Internal,
|
||||
session_id: uuid!("00000000-0000-0000-0000-000000000000"),
|
||||
scope: AccessScope::ReadWrite,
|
||||
limits: Limits::unlimited(),
|
||||
}
|
||||
|
@ -164,6 +193,7 @@ impl Identity {
|
|||
) -> Self {
|
||||
Identity {
|
||||
origin: IdentType::User(IdentUser { entry }),
|
||||
session_id: uuid!("00000000-0000-0000-0000-000000000000"),
|
||||
scope: AccessScope::IdentityOnly,
|
||||
limits: Limits::unlimited(),
|
||||
}
|
||||
|
@ -173,6 +203,7 @@ impl Identity {
|
|||
pub fn from_impersonate_entry_readonly(entry: Arc<Entry<EntrySealed, EntryCommitted>>) -> Self {
|
||||
Identity {
|
||||
origin: IdentType::User(IdentUser { entry }),
|
||||
session_id: uuid!("00000000-0000-0000-0000-000000000000"),
|
||||
scope: AccessScope::ReadOnly,
|
||||
limits: Limits::unlimited(),
|
||||
}
|
||||
|
@ -184,6 +215,7 @@ impl Identity {
|
|||
) -> Self {
|
||||
Identity {
|
||||
origin: IdentType::User(IdentUser { entry }),
|
||||
session_id: uuid!("00000000-0000-0000-0000-000000000000"),
|
||||
scope: AccessScope::ReadWrite,
|
||||
limits: Limits::unlimited(),
|
||||
}
|
||||
|
@ -193,6 +225,10 @@ impl Identity {
|
|||
self.scope
|
||||
}
|
||||
|
||||
pub fn get_session_id(&self) -> Uuid {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
pub fn from_impersonate(ident: &Self) -> Self {
|
||||
// TODO #64 ?: In the future, we could change some of this data
|
||||
// to reflect the fact we are infact impersonating the action
|
||||
|
|
|
@ -2,7 +2,8 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||
use std::time::Duration;
|
||||
|
||||
use kanidm_proto::v1::{
|
||||
AuthType, BackupCodesView, CredentialStatus, OperationError, UatPurpose, UiHint, UserAuthToken,
|
||||
AuthType, BackupCodesView, CredentialStatus, OperationError, UatPurpose, UatStatus, UiHint,
|
||||
UserAuthToken,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
@ -15,8 +16,9 @@ use crate::credential::policy::CryptoPolicy;
|
|||
use crate::credential::softlock::CredSoftLockPolicy;
|
||||
use crate::credential::Credential;
|
||||
use crate::entry::{Entry, EntryCommitted, EntryReduced, EntrySealed};
|
||||
use crate::event::SearchEvent;
|
||||
use crate::idm::group::Group;
|
||||
use crate::idm::server::IdmServerProxyWriteTransaction;
|
||||
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
|
||||
use crate::modify::{ModifyInvalid, ModifyList};
|
||||
use crate::prelude::*;
|
||||
use crate::schema::SchemaTransaction;
|
||||
|
@ -195,6 +197,7 @@ impl Account {
|
|||
|
||||
// TODO: Apply policy to this expiry time.
|
||||
let expiry = OffsetDateTime::unix_epoch() + ct + Duration::from_secs(AUTH_SESSION_EXPIRY);
|
||||
let issued_at = OffsetDateTime::unix_epoch() + ct;
|
||||
// TODO: Apply priv expiry.
|
||||
let purpose = UatPurpose::ReadWrite {
|
||||
expiry: expiry.clone(),
|
||||
|
@ -203,7 +206,8 @@ impl Account {
|
|||
Some(UserAuthToken {
|
||||
session_id,
|
||||
auth_type,
|
||||
expiry,
|
||||
expiry: Some(expiry),
|
||||
issued_at,
|
||||
purpose,
|
||||
uuid: self.uuid,
|
||||
displayname: self.displayname.clone(),
|
||||
|
@ -417,13 +421,118 @@ impl Account {
|
|||
// Used in registrations only for disallowing exsiting credentials.
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn check_user_auth_token_valid(
|
||||
ct: Duration,
|
||||
uat: &UserAuthToken,
|
||||
entry: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> bool {
|
||||
// Remember, token expiry is checked by validate_and_parse_token_to_token.
|
||||
// If we wanted we could check other properties of the uat here?
|
||||
// Alternatelly, we could always store LESS in the uat because of this?
|
||||
|
||||
let within_valid_window = Account::check_within_valid_time(
|
||||
ct,
|
||||
entry.get_ava_single_datetime("account_valid_from").as_ref(),
|
||||
entry.get_ava_single_datetime("account_expire").as_ref(),
|
||||
);
|
||||
|
||||
if !within_valid_window {
|
||||
security_info!("Account has expired or is not yet valid, not allowing to proceed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Anonymous does NOT record it's sessions, so we simply check the expiry time
|
||||
// of the token. This is already done for us as noted above.
|
||||
|
||||
if uat.auth_type == AuthType::Anonymous {
|
||||
security_info!("Anonymous sessions do not have session records, session is valid.");
|
||||
true
|
||||
} else {
|
||||
// Get the sessions.
|
||||
let session_present = entry
|
||||
.get_ava_as_session_map("user_auth_token_session")
|
||||
.map(|session_map| session_map.get(&uat.session_id).is_some())
|
||||
.unwrap_or(false);
|
||||
|
||||
if session_present {
|
||||
security_info!("A valid session value exists for this token");
|
||||
true
|
||||
} else {
|
||||
let grace = uat.issued_at + GRACE_WINDOW;
|
||||
let current = time::OffsetDateTime::unix_epoch() + ct;
|
||||
trace!(%grace, %current);
|
||||
if current >= grace {
|
||||
security_info!(
|
||||
"The token grace window has passed, and no session exists. Assuming invalid."
|
||||
);
|
||||
false
|
||||
} else {
|
||||
security_info!("The token grace window is in effect. Assuming valid.");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need to also add a "to UserAuthToken" ...
|
||||
|
||||
// Need tests for conversion and the cred validations
|
||||
|
||||
pub struct DestroySessionTokenEvent {
|
||||
// Who initiated this?
|
||||
pub ident: Identity,
|
||||
// Who is it targetting?
|
||||
pub target: Uuid,
|
||||
// Which token id.
|
||||
pub token_id: Uuid,
|
||||
}
|
||||
|
||||
impl DestroySessionTokenEvent {
|
||||
#[cfg(test)]
|
||||
pub fn new_internal(target: Uuid, token_id: Uuid) -> Self {
|
||||
DestroySessionTokenEvent {
|
||||
ident: Identity::from_internal(),
|
||||
target,
|
||||
token_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||
pub fn account_destroy_session_token(
|
||||
&self,
|
||||
dte: &DestroySessionTokenEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
// Delete the attribute with uuid.
|
||||
let modlist = ModifyList::new_list(vec![Modify::Removed(
|
||||
AttrString::from("user_auth_token_session"),
|
||||
PartialValue::Refer(dte.token_id),
|
||||
)]);
|
||||
|
||||
self.qs_write
|
||||
.impersonate_modify(
|
||||
// Filter as executed
|
||||
&filter!(f_and!([
|
||||
f_eq("uuid", PartialValue::Uuid(dte.target)),
|
||||
f_eq("user_auth_token_session", PartialValue::Refer(dte.token_id))
|
||||
])),
|
||||
// Filter as intended (acp)
|
||||
&filter_all!(f_and!([
|
||||
f_eq("uuid", PartialValue::Uuid(dte.target)),
|
||||
f_eq("user_auth_token_session", PartialValue::Refer(dte.token_id))
|
||||
])),
|
||||
&modlist,
|
||||
// Provide the event to impersonate
|
||||
&dte.ident,
|
||||
)
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to destroy user auth token {:?}", e);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
pub fn service_account_into_person(
|
||||
&self,
|
||||
ident: &Identity,
|
||||
|
@ -495,6 +604,70 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ListUserAuthTokenEvent {
|
||||
// Who initiated this?
|
||||
pub ident: Identity,
|
||||
// Who is it targetting?
|
||||
pub target: Uuid,
|
||||
}
|
||||
|
||||
impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||
pub fn account_list_user_auth_tokens(
|
||||
&self,
|
||||
lte: &ListUserAuthTokenEvent,
|
||||
) -> Result<Vec<UatStatus>, OperationError> {
|
||||
// Make an event from the request
|
||||
let srch = match SearchEvent::from_target_uuid_request(
|
||||
lte.ident.clone(),
|
||||
lte.target,
|
||||
&self.qs_read,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
admin_error!("Failed to begin account list user auth tokens: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
match self.qs_read.search_ext(&srch) {
|
||||
Ok(mut entries) => {
|
||||
entries
|
||||
.pop()
|
||||
// get the first entry
|
||||
.and_then(|e| {
|
||||
let account_id = e.get_uuid();
|
||||
// From the entry, turn it into the value
|
||||
e.get_ava_as_session_map("user_auth_token_session")
|
||||
.map(|smap| {
|
||||
smap.iter()
|
||||
.map(|(u, s)| {
|
||||
s.scope
|
||||
.try_into()
|
||||
.map(|purpose| UatStatus {
|
||||
account_id,
|
||||
session_id: *u,
|
||||
expiry: s.expiry.clone(),
|
||||
issued_at: s.issued_at.clone(),
|
||||
purpose,
|
||||
})
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid user auth token {}", u);
|
||||
e
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
// No matching entry? Return none.
|
||||
Ok(Vec::new())
|
||||
})
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::constants::JSON_ANONYMOUS_V1;
|
||||
|
|
|
@ -22,9 +22,10 @@ use webauthn_rs::prelude::{
|
|||
|
||||
use crate::credential::totp::Totp;
|
||||
use crate::credential::{BackupCodes, Credential, CredentialType, Password};
|
||||
use crate::identity::IdentityId;
|
||||
use crate::idm::account::Account;
|
||||
use crate::idm::delayed::{
|
||||
BackupCodeRemoval, DelayedAction, PasswordUpgrade, WebauthnCounterIncrement,
|
||||
AuthSessionRecord, BackupCodeRemoval, DelayedAction, PasswordUpgrade, WebauthnCounterIncrement,
|
||||
};
|
||||
use crate::idm::AuthState;
|
||||
use crate::prelude::*;
|
||||
|
@ -740,10 +741,6 @@ impl AuthSession {
|
|||
) {
|
||||
CredState::Success(auth_type) => {
|
||||
security_info!("Successful cred handling");
|
||||
// TODO: put the operation id into the call to `to_userauthtoken`
|
||||
// Can't `unwrap` the uuid until full integration, because some unit tests
|
||||
// call functions that call this indirectly without opening a span first,
|
||||
// and this returns `None` when not in a span (and panics if the tree isn't initialized).
|
||||
let session_id = Uuid::new_v4();
|
||||
security_info!(
|
||||
"Starting session {} for {} {}",
|
||||
|
@ -751,11 +748,47 @@ impl AuthSession {
|
|||
self.account.spn,
|
||||
self.account.uuid
|
||||
);
|
||||
|
||||
let uat = self
|
||||
.account
|
||||
.to_userauthtoken(session_id, *time, auth_type)
|
||||
.to_userauthtoken(session_id, *time, auth_type.clone())
|
||||
.ok_or(OperationError::InvalidState)?;
|
||||
|
||||
// Queue the session info write.
|
||||
// This is dependent on the type of authentication factors
|
||||
// used. Generally we won't submit for Anonymous. Add an extra
|
||||
// safety barrier for auth types that shouldn't be here. Generally we
|
||||
// submit session info for everything else.
|
||||
match auth_type {
|
||||
AuthType::Anonymous => {
|
||||
// Skip - these sessions are not validated by session id.
|
||||
}
|
||||
AuthType::UnixPassword => {
|
||||
// Impossibru!
|
||||
admin_error!("Impossible auth type (UnixPassword) found");
|
||||
return Err(OperationError::InvalidState);
|
||||
}
|
||||
AuthType::Password
|
||||
| AuthType::GeneratedPassword
|
||||
| AuthType::PasswordMfa
|
||||
| AuthType::Passkey => {
|
||||
trace!("⚠️ Queued AuthSessionRecord for {}", self.account.uuid);
|
||||
async_tx.send(DelayedAction::AuthSessionRecord(AuthSessionRecord {
|
||||
target_uuid: self.account.uuid,
|
||||
session_id,
|
||||
label: "Auth Session".to_string(),
|
||||
expiry: uat.expiry,
|
||||
issued_at: uat.issued_at.clone(),
|
||||
issued_by: IdentityId::User(self.account.uuid),
|
||||
scope: (&uat.purpose).into(),
|
||||
}))
|
||||
.map_err(|_| {
|
||||
admin_error!("unable to queue failing authentication as the session will not validate ... ");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
}
|
||||
};
|
||||
|
||||
let jwt = Jws::new(uat);
|
||||
|
||||
// Now encrypt and prepare the token for return to the client.
|
||||
|
@ -984,6 +1017,11 @@ mod tests {
|
|||
_ => panic!(),
|
||||
};
|
||||
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::AuthSessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
||||
drop(async_tx);
|
||||
assert!(async_rx.blocking_recv().is_none());
|
||||
}
|
||||
|
@ -1225,6 +1263,11 @@ mod tests {
|
|||
Ok(AuthState::Success(_)) => {}
|
||||
_ => panic!(),
|
||||
};
|
||||
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::AuthSessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
|
||||
drop(async_tx);
|
||||
|
@ -1445,13 +1488,17 @@ mod tests {
|
|||
Ok(AuthState::Success(_)) => {}
|
||||
_ => panic!(),
|
||||
};
|
||||
}
|
||||
|
||||
// Check the async counter update was sent.
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::AuthSessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
|
||||
// Check bad challenge.
|
||||
{
|
||||
|
@ -1686,6 +1733,10 @@ mod tests {
|
|||
Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::AuthSessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
|
||||
drop(async_tx);
|
||||
|
@ -1883,6 +1934,11 @@ mod tests {
|
|||
Ok(AuthState::Success(_)) => {}
|
||||
_ => panic!(),
|
||||
};
|
||||
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::AuthSessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
|
||||
// Check good webauthn/good pw (pass)
|
||||
|
@ -1923,6 +1979,10 @@ mod tests {
|
|||
Some(DelayedAction::WebauthnCounterIncrement(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::AuthSessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
|
||||
drop(async_tx);
|
||||
|
@ -2077,6 +2137,12 @@ mod tests {
|
|||
_ => assert!(false),
|
||||
}
|
||||
|
||||
// There will be a auth session record too
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::AuthSessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
||||
// TOTP should also work:
|
||||
// check send good TOTP, should continue
|
||||
// then good pw, success
|
||||
|
@ -2108,6 +2174,12 @@ mod tests {
|
|||
};
|
||||
}
|
||||
|
||||
// There will be a auth session record too
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::AuthSessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
||||
drop(async_tx);
|
||||
assert!(async_rx.blocking_recv().is_none());
|
||||
}
|
||||
|
|
|
@ -1487,7 +1487,7 @@ mod tests {
|
|||
use crate::event::CreateEvent;
|
||||
use crate::idm::delayed::DelayedAction;
|
||||
use crate::idm::event::{AuthEvent, AuthResult};
|
||||
use crate::idm::server::IdmServer;
|
||||
use crate::idm::server::{IdmServer, IdmServerDelayed};
|
||||
use crate::idm::AuthState;
|
||||
use crate::prelude::*;
|
||||
|
||||
|
@ -1669,7 +1669,12 @@ mod tests {
|
|||
idms_prox_write.commit().expect("Failed to commit txn");
|
||||
}
|
||||
|
||||
fn check_testperson_password(idms: &IdmServer, pw: &str, ct: Duration) -> Option<String> {
|
||||
fn check_testperson_password(
|
||||
idms: &IdmServer,
|
||||
idms_delayed: &mut IdmServerDelayed,
|
||||
pw: &str,
|
||||
ct: Duration,
|
||||
) -> Option<String> {
|
||||
let mut idms_auth = idms.auth();
|
||||
|
||||
let auth_init = AuthEvent::named_init("testperson");
|
||||
|
@ -1711,13 +1716,20 @@ mod tests {
|
|||
sessionid: _,
|
||||
state: AuthState::Success(token),
|
||||
delay: _,
|
||||
}) => Some(token),
|
||||
}) => {
|
||||
// Process the auth session
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
|
||||
Some(token)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_testperson_password_totp(
|
||||
idms: &IdmServer,
|
||||
idms_delayed: &mut IdmServerDelayed,
|
||||
pw: &str,
|
||||
token: &Totp,
|
||||
ct: Duration,
|
||||
|
@ -1778,13 +1790,19 @@ mod tests {
|
|||
sessionid: _,
|
||||
state: AuthState::Success(token),
|
||||
delay: _,
|
||||
}) => Some(token),
|
||||
}) => {
|
||||
// Process the auth session
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
Some(token)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_testperson_password_backup_code(
|
||||
idms: &IdmServer,
|
||||
idms_delayed: &mut IdmServerDelayed,
|
||||
pw: &str,
|
||||
code: &str,
|
||||
ct: Duration,
|
||||
|
@ -1841,13 +1859,25 @@ mod tests {
|
|||
sessionid: _,
|
||||
state: AuthState::Success(token),
|
||||
delay: _,
|
||||
}) => Some(token),
|
||||
}) => {
|
||||
// There now should be a backup code invalidation present
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::BackupCodeRemoval(_)));
|
||||
let r = task::block_on(idms.delayed_action(ct, da));
|
||||
assert!(r.is_ok());
|
||||
|
||||
// Process the auth session
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
Some(token)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_testperson_passkey(
|
||||
idms: &IdmServer,
|
||||
idms_delayed: &mut IdmServerDelayed,
|
||||
wa: &mut WebauthnAuthenticator<SoftPasskey>,
|
||||
origin: Url,
|
||||
ct: Duration,
|
||||
|
@ -1906,7 +1936,19 @@ mod tests {
|
|||
sessionid: _,
|
||||
state: AuthState::Success(token),
|
||||
delay: _,
|
||||
}) => Some(token),
|
||||
}) => {
|
||||
// Process the webauthn update
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
|
||||
let r = task::block_on(idms.delayed_action(ct, da));
|
||||
assert!(r.is_ok());
|
||||
|
||||
// Process the auth session
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
|
||||
Some(token)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -1941,10 +1983,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_idm_credential_update_onboarding_create_new_pw() {
|
||||
run_idm_test!(|_qs: &QueryServer,
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed| {
|
||||
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||
run_idm_test!(
|
||||
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
let test_pw =
|
||||
"fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
|
||||
let (cust, _) = setup_test_session(idms, ct);
|
||||
|
@ -1974,7 +2016,7 @@ mod tests {
|
|||
commit_session(idms, ct, cust);
|
||||
|
||||
// Check it works!
|
||||
assert!(check_testperson_password(idms, test_pw, ct).is_some());
|
||||
assert!(check_testperson_password(idms, idms_delayed, test_pw, ct).is_some());
|
||||
|
||||
// Test deleting the pw
|
||||
let (cust, _) = renew_test_session(idms, ct);
|
||||
|
@ -1996,8 +2038,9 @@ mod tests {
|
|||
commit_session(idms, ct, cust);
|
||||
|
||||
// Must fail now!
|
||||
assert!(check_testperson_password(idms, test_pw, ct).is_none());
|
||||
})
|
||||
assert!(check_testperson_password(idms, idms_delayed, test_pw, ct).is_none());
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Test set of primary account password
|
||||
|
@ -2007,10 +2050,10 @@ mod tests {
|
|||
// - setup TOTP
|
||||
#[test]
|
||||
fn test_idm_credential_update_onboarding_create_new_mfa_totp_basic() {
|
||||
run_idm_test!(|_qs: &QueryServer,
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed| {
|
||||
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||
run_idm_test!(
|
||||
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
let test_pw =
|
||||
"fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
|
||||
let (cust, _) = setup_test_session(idms, ct);
|
||||
|
@ -2068,7 +2111,14 @@ mod tests {
|
|||
commit_session(idms, ct, cust);
|
||||
|
||||
// Check it works!
|
||||
assert!(check_testperson_password_totp(idms, test_pw, &totp_token, ct).is_some());
|
||||
assert!(check_testperson_password_totp(
|
||||
idms,
|
||||
idms_delayed,
|
||||
test_pw,
|
||||
&totp_token,
|
||||
ct
|
||||
)
|
||||
.is_some());
|
||||
// No need to test delete of the whole cred, we already did with pw above.
|
||||
|
||||
// If we remove TOTP, show it reverts back.
|
||||
|
@ -2089,17 +2139,18 @@ mod tests {
|
|||
commit_session(idms, ct, cust);
|
||||
|
||||
// Check it works with totp removed.
|
||||
assert!(check_testperson_password(idms, test_pw, ct).is_some());
|
||||
})
|
||||
assert!(check_testperson_password(idms, idms_delayed, test_pw, ct).is_some());
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Check sha1 totp.
|
||||
#[test]
|
||||
fn test_idm_credential_update_onboarding_create_new_mfa_totp_sha1() {
|
||||
run_idm_test!(|_qs: &QueryServer,
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed| {
|
||||
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||
run_idm_test!(
|
||||
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
let test_pw =
|
||||
"fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
|
||||
let (cust, _) = setup_test_session(idms, ct);
|
||||
|
@ -2160,9 +2211,17 @@ mod tests {
|
|||
commit_session(idms, ct, cust);
|
||||
|
||||
// Check it works!
|
||||
assert!(check_testperson_password_totp(idms, test_pw, &totp_token, ct).is_some());
|
||||
assert!(check_testperson_password_totp(
|
||||
idms,
|
||||
idms_delayed,
|
||||
test_pw,
|
||||
&totp_token,
|
||||
ct
|
||||
)
|
||||
.is_some());
|
||||
// No need to test delete, we already did with pw above.
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -2239,15 +2298,14 @@ mod tests {
|
|||
let backup_code = codes.iter().next().expect("No codes available");
|
||||
|
||||
// Check it works!
|
||||
assert!(
|
||||
check_testperson_password_backup_code(idms, test_pw, backup_code, ct).is_some()
|
||||
);
|
||||
|
||||
// There now should be a backup code invalidation present
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::BackupCodeRemoval(_)));
|
||||
let r = task::block_on(idms.delayed_action(ct, da));
|
||||
assert!(r.is_ok());
|
||||
assert!(check_testperson_password_backup_code(
|
||||
idms,
|
||||
idms_delayed,
|
||||
test_pw,
|
||||
backup_code,
|
||||
ct
|
||||
)
|
||||
.is_some());
|
||||
|
||||
// Renew to start the next steps
|
||||
let (cust, _) = renew_test_session(idms, ct);
|
||||
|
@ -2307,10 +2365,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_idm_credential_update_onboarding_cancel_inprogress_totp() {
|
||||
run_idm_test!(|_qs: &QueryServer,
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed| {
|
||||
let test_pw = "fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||
run_idm_test!(
|
||||
|_qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
let test_pw =
|
||||
"fo3EitierohF9AelaNgiem0Ei6vup4equo1Oogeevaetehah8Tobeengae3Ci0ooh0uki";
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
|
||||
let (cust, _) = setup_test_session(idms, ct);
|
||||
|
@ -2347,8 +2405,9 @@ mod tests {
|
|||
commit_session(idms, ct, cust);
|
||||
|
||||
// It's pw only, since we canceled TOTP
|
||||
assert!(check_testperson_password(idms, test_pw, ct).is_some());
|
||||
})
|
||||
assert!(check_testperson_password(idms, idms_delayed, test_pw, ct).is_some());
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Primary cred must be pw or pwmfa
|
||||
|
@ -2412,23 +2471,10 @@ mod tests {
|
|||
commit_session(idms, ct, cust);
|
||||
|
||||
// Do an auth test
|
||||
assert!(check_testperson_passkey(idms, &mut wa, origin.clone(), ct).is_some());
|
||||
|
||||
// Since it authed, it should have updated the delayed queue.
|
||||
let da = idms_delayed
|
||||
.blocking_recv()
|
||||
.expect("No queued action found!");
|
||||
|
||||
match &da {
|
||||
DelayedAction::WebauthnCounterIncrement(wci) => {
|
||||
trace!("{:?}", wci.auth_result);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
let mut idms_prox_write = idms.proxy_write(ct);
|
||||
assert!(idms_prox_write.process_delayedaction(da).is_ok());
|
||||
idms_prox_write.commit().expect("Failed to commit txn");
|
||||
assert!(
|
||||
check_testperson_passkey(idms, idms_delayed, &mut wa, origin.clone(), ct)
|
||||
.is_some()
|
||||
);
|
||||
|
||||
// Now test removing the token
|
||||
let (cust, _) = renew_test_session(idms, ct);
|
||||
|
@ -2450,7 +2496,9 @@ mod tests {
|
|||
commit_session(idms, ct, cust);
|
||||
|
||||
// Must fail now!
|
||||
assert!(check_testperson_passkey(idms, &mut wa, origin, ct).is_none());
|
||||
assert!(
|
||||
check_testperson_passkey(idms, idms_delayed, &mut wa, origin, ct).is_none()
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
use crate::identity::{AccessScope, IdentityId};
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::prelude::AuthenticationResult;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DelayedAction {
|
||||
PwUpgrade(PasswordUpgrade),
|
||||
UnixPwUpgrade(UnixPasswordUpgrade),
|
||||
WebauthnCounterIncrement(WebauthnCounterIncrement),
|
||||
BackupCodeRemoval(BackupCodeRemoval),
|
||||
Oauth2ConsentGrant(Oauth2ConsentGrant),
|
||||
AuthSessionRecord(AuthSessionRecord),
|
||||
}
|
||||
|
||||
pub struct PasswordUpgrade {
|
||||
|
@ -14,23 +20,53 @@ pub struct PasswordUpgrade {
|
|||
pub existing_password: String,
|
||||
}
|
||||
|
||||
impl fmt::Debug for PasswordUpgrade {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("PasswordUpgrade")
|
||||
.field("target_uuid", &self.target_uuid)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnixPasswordUpgrade {
|
||||
pub target_uuid: Uuid,
|
||||
pub existing_password: String,
|
||||
}
|
||||
|
||||
impl fmt::Debug for UnixPasswordUpgrade {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("UnixPasswordUpgrade")
|
||||
.field("target_uuid", &self.target_uuid)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WebauthnCounterIncrement {
|
||||
pub target_uuid: Uuid,
|
||||
pub auth_result: AuthenticationResult,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BackupCodeRemoval {
|
||||
pub target_uuid: Uuid,
|
||||
pub code_to_remove: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Oauth2ConsentGrant {
|
||||
pub target_uuid: Uuid,
|
||||
pub oauth2_rs_uuid: Uuid,
|
||||
pub scopes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthSessionRecord {
|
||||
pub target_uuid: Uuid,
|
||||
pub session_id: Uuid,
|
||||
pub label: String,
|
||||
pub expiry: Option<OffsetDateTime>,
|
||||
pub issued_at: OffsetDateTime,
|
||||
pub issued_by: IdentityId,
|
||||
pub scope: AccessScope,
|
||||
}
|
||||
|
|
|
@ -940,15 +940,24 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
let iat = ct.as_secs() as i64;
|
||||
|
||||
// TODO: Make configurable from auth policy!
|
||||
let expires_in = if code_xchg.uat.expiry > odt_ct {
|
||||
let (expiry, expires_in) = if let Some(expiry) = code_xchg.uat.expiry {
|
||||
if expiry > odt_ct {
|
||||
// Becomes a duration.
|
||||
(code_xchg.uat.expiry - odt_ct).whole_seconds() as u32
|
||||
(expiry, (expiry - odt_ct).whole_seconds() as u32)
|
||||
} else {
|
||||
security_info!(
|
||||
"User Auth Token has expired before we could publish the oauth2 response"
|
||||
);
|
||||
return Err(Oauth2Error::AccessDenied);
|
||||
}
|
||||
} else {
|
||||
security_info!("User Auth Token has no expiry, setting to refresh window");
|
||||
(
|
||||
odt_ct + Duration::from_secs(OAUTH2_ACCESS_TOKEN_EXPIRY as u64),
|
||||
OAUTH2_ACCESS_TOKEN_EXPIRY,
|
||||
)
|
||||
};
|
||||
// let expiry = odt_ct + Duration::from_secs(expires_in as u64);
|
||||
|
||||
let scope = if code_xchg.scopes.is_empty() {
|
||||
None
|
||||
|
@ -1044,7 +1053,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
scopes: code_xchg.scopes,
|
||||
session_id: code_xchg.uat.session_id,
|
||||
auth_type: code_xchg.uat.auth_type,
|
||||
expiry: code_xchg.uat.expiry,
|
||||
expiry,
|
||||
uuid: code_xchg.uat.uuid,
|
||||
iat,
|
||||
nbf: iat,
|
||||
|
@ -1870,8 +1879,10 @@ mod tests {
|
|||
// TEST_CURRENT_TIME UAT_EXPIRE TOKEN_EXPIRE
|
||||
//
|
||||
// This lets us check a variety of time based cases.
|
||||
uat.expiry = time::OffsetDateTime::unix_epoch()
|
||||
+ Duration::from_secs(TEST_CURRENT_TIME + UAT_EXPIRE - 1);
|
||||
uat.expiry = Some(
|
||||
time::OffsetDateTime::unix_epoch()
|
||||
+ Duration::from_secs(TEST_CURRENT_TIME + UAT_EXPIRE - 1),
|
||||
);
|
||||
|
||||
let idms_prox_read = idms.proxy_read();
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ use tracing::trace;
|
|||
use url::Url;
|
||||
use webauthn_rs::prelude::{Webauthn, WebauthnBuilder};
|
||||
|
||||
use super::delayed::BackupCodeRemoval;
|
||||
use super::event::ReadBackupCodeEvent;
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::softlock::CredSoftLock;
|
||||
|
@ -36,8 +35,8 @@ use crate::idm::account::Account;
|
|||
use crate::idm::authsession::AuthSession;
|
||||
use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
|
||||
use crate::idm::delayed::{
|
||||
DelayedAction, Oauth2ConsentGrant, PasswordUpgrade, UnixPasswordUpgrade,
|
||||
WebauthnCounterIncrement,
|
||||
AuthSessionRecord, BackupCodeRemoval, DelayedAction, Oauth2ConsentGrant, PasswordUpgrade,
|
||||
UnixPasswordUpgrade, WebauthnCounterIncrement,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::idm::event::PasswordChangeEvent;
|
||||
|
@ -60,6 +59,7 @@ use crate::idm::AuthState;
|
|||
use crate::ldap::{LdapBoundToken, LdapSession};
|
||||
use crate::prelude::*;
|
||||
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
|
||||
use crate::value::Session;
|
||||
|
||||
type AuthSessionMutex = Arc<Mutex<AuthSession>>;
|
||||
type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;
|
||||
|
@ -83,7 +83,6 @@ pub struct IdmServer {
|
|||
webauthn: Webauthn,
|
||||
pw_badlist_cache: Arc<CowCell<HashSet<String>>>,
|
||||
oauth2rs: Arc<Oauth2ResourceServers>,
|
||||
|
||||
uat_jwt_signer: Arc<CowCell<JwsSigner>>,
|
||||
uat_jwt_validator: Arc<CowCell<JwsValidator>>,
|
||||
token_enc_key: Arc<CowCell<Fernet>>,
|
||||
|
@ -358,14 +357,19 @@ impl IdmServerDelayed {
|
|||
let mut cx = Context::from_waker(&waker);
|
||||
match self.async_rx.poll_recv(&mut cx) {
|
||||
Poll::Pending | Poll::Ready(None) => {}
|
||||
Poll::Ready(Some(_m)) => panic!("Task queue not empty"),
|
||||
Poll::Ready(Some(m)) => {
|
||||
trace!(?m);
|
||||
panic!("Task queue not empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[cfg(test)]
|
||||
pub(crate) fn blocking_recv(&mut self) -> Option<DelayedAction> {
|
||||
self.async_rx.blocking_recv()
|
||||
}
|
||||
*/
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn try_recv(&mut self) -> Result<DelayedAction, OperationError> {
|
||||
|
@ -455,12 +459,17 @@ pub trait IdmServerTransaction<'a> {
|
|||
})
|
||||
.map(|t: Jws<UserAuthToken>| t.into_inner())?;
|
||||
|
||||
if time::OffsetDateTime::unix_epoch() + ct >= uat.expiry {
|
||||
if let Some(exp) = uat.expiry {
|
||||
if time::OffsetDateTime::unix_epoch() + ct >= exp {
|
||||
security_info!("Session expired");
|
||||
Err(OperationError::SessionExpired)
|
||||
} else {
|
||||
Ok(Token::UserAuthToken(uat))
|
||||
}
|
||||
} else {
|
||||
debug!("Session has no expiry");
|
||||
Ok(Token::UserAuthToken(uat))
|
||||
}
|
||||
} else {
|
||||
// It's a per-user key, get their validator.
|
||||
let entry = self
|
||||
|
@ -541,12 +550,17 @@ pub trait IdmServerTransaction<'a> {
|
|||
.map(|t: Jws<UserAuthToken>| t.into_inner())
|
||||
})?;
|
||||
|
||||
if time::OffsetDateTime::unix_epoch() + ct >= uat.expiry {
|
||||
if let Some(exp) = uat.expiry {
|
||||
if time::OffsetDateTime::unix_epoch() + ct >= exp {
|
||||
security_info!("Session expired");
|
||||
Err(OperationError::SessionExpired)
|
||||
} else {
|
||||
Ok(uat)
|
||||
}
|
||||
} else {
|
||||
debug!("Session has no expiry");
|
||||
Ok(uat)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_account_uuid_valid(
|
||||
|
@ -597,19 +611,14 @@ pub trait IdmServerTransaction<'a> {
|
|||
e
|
||||
})?;
|
||||
|
||||
// #59: If the account is expired, do not allow the event
|
||||
// to proceed
|
||||
let valid = Account::check_within_valid_time(
|
||||
ct,
|
||||
entry.get_ava_single_datetime("account_valid_from").as_ref(),
|
||||
entry.get_ava_single_datetime("account_expire").as_ref(),
|
||||
);
|
||||
let valid = Account::check_user_auth_token_valid(ct, uat, &entry);
|
||||
|
||||
if !valid {
|
||||
security_info!("Account has expired or is not yet valid, not allowing to proceed");
|
||||
return Err(OperationError::SessionExpired);
|
||||
}
|
||||
|
||||
// ✅ Session is valid! Start to setup for it to be used.
|
||||
|
||||
let scope = match uat.purpose {
|
||||
UatPurpose::IdentityOnly => AccessScope::IdentityOnly,
|
||||
UatPurpose::ReadOnly => AccessScope::ReadOnly,
|
||||
|
@ -623,6 +632,8 @@ pub trait IdmServerTransaction<'a> {
|
|||
}
|
||||
};
|
||||
|
||||
let limits = Limits::default();
|
||||
|
||||
// #64: Now apply claims from the uat into the Entry
|
||||
// to allow filtering.
|
||||
/*
|
||||
|
@ -635,28 +646,12 @@ pub trait IdmServerTransaction<'a> {
|
|||
AuthType::PasswordMfa => "authtype_passwordmfa",
|
||||
});
|
||||
|
||||
match &uat.auth_type {
|
||||
AuthType::Anonymous | AuthType::UnixPassword | AuthType::Password => {}
|
||||
AuthType::GeneratedPassword | AuthType::Webauthn | AuthType::PasswordMfa => {
|
||||
entry.insert_claim("authlevel_strong")
|
||||
}
|
||||
};
|
||||
|
||||
match &uat.auth_type {
|
||||
AuthType::Anonymous => {}
|
||||
AuthType::UnixPassword
|
||||
| AuthType::Password
|
||||
| AuthType::GeneratedPassword
|
||||
| AuthType::Webauthn => entry.insert_claim("authclass_single"),
|
||||
AuthType::PasswordMfa => entry.insert_claim("authclass_mfa"),
|
||||
};
|
||||
trace!(claims = ?entry.get_ava_set("claim"), "Applied claims");
|
||||
*/
|
||||
|
||||
trace!(claims = ?entry.get_ava_set("claim"), "Applied claims");
|
||||
|
||||
let limits = Limits::default();
|
||||
Ok(Identity {
|
||||
origin: IdentType::User(IdentUser { entry }),
|
||||
session_id: uat.session_id,
|
||||
scope,
|
||||
limits,
|
||||
})
|
||||
|
@ -681,6 +676,7 @@ pub trait IdmServerTransaction<'a> {
|
|||
let limits = Limits::default();
|
||||
Ok(Identity {
|
||||
origin: IdentType::User(IdentUser { entry }),
|
||||
session_id: apit.token_id,
|
||||
scope,
|
||||
limits,
|
||||
})
|
||||
|
@ -718,8 +714,11 @@ pub trait IdmServerTransaction<'a> {
|
|||
) {
|
||||
// Good to go
|
||||
let limits = Limits::default();
|
||||
let session_id = Uuid::new_v4();
|
||||
|
||||
Ok(Identity {
|
||||
origin: IdentType::User(IdentUser { entry: anon_entry }),
|
||||
session_id,
|
||||
scope: AccessScope::ReadOnly,
|
||||
limits,
|
||||
})
|
||||
|
@ -1993,6 +1992,46 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn process_authsessionrecord(
|
||||
&mut self,
|
||||
asr: &AuthSessionRecord,
|
||||
) -> Result<(), OperationError> {
|
||||
let session = Value::Session(
|
||||
asr.session_id,
|
||||
Session {
|
||||
label: asr.label.clone(),
|
||||
expiry: asr.expiry,
|
||||
// Need the other inner bits?
|
||||
// for the gracewindow.
|
||||
issued_at: asr.issued_at,
|
||||
// Who actually created this?
|
||||
issued_by: asr.issued_by.clone(),
|
||||
// What is the access scope of this session? This is
|
||||
// for auditing purposes.
|
||||
scope: asr.scope,
|
||||
},
|
||||
);
|
||||
|
||||
info!(session_id = %asr.session_id, "Persisting auth session");
|
||||
|
||||
// modify the account to put the session onto it.
|
||||
let modlist = ModifyList::new_list(vec![Modify::Present(
|
||||
AttrString::from("user_auth_token_session"),
|
||||
session,
|
||||
)]);
|
||||
|
||||
self.qs_write
|
||||
.internal_modify(
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(asr.target_uuid))),
|
||||
&modlist,
|
||||
)
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to persist user auth token {:?}", e);
|
||||
e
|
||||
})
|
||||
// Done!
|
||||
}
|
||||
|
||||
pub(crate) fn process_oauth2consentgrant(
|
||||
&mut self,
|
||||
o2cg: &Oauth2ConsentGrant,
|
||||
|
@ -2021,6 +2060,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
DelayedAction::WebauthnCounterIncrement(wci) => self.process_webauthncounterinc(&wci),
|
||||
DelayedAction::BackupCodeRemoval(bcr) => self.process_backupcoderemoval(&bcr),
|
||||
DelayedAction::Oauth2ConsentGrant(o2cg) => self.process_oauth2consentgrant(&o2cg),
|
||||
DelayedAction::AuthSessionRecord(asr) => self.process_authsessionrecord(&asr),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2110,6 +2150,8 @@ mod tests {
|
|||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::{Credential, Password};
|
||||
use crate::event::{CreateEvent, ModifyEvent};
|
||||
use crate::idm::account::DestroySessionTokenEvent;
|
||||
use crate::idm::delayed::DelayedAction;
|
||||
use crate::idm::event::{AuthEvent, AuthResult};
|
||||
use crate::idm::event::{
|
||||
PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
|
||||
|
@ -2399,15 +2441,21 @@ mod tests {
|
|||
};
|
||||
|
||||
idms_auth.commit().expect("Must not fail");
|
||||
|
||||
token
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_simple_password_auth() {
|
||||
run_idm_test!(
|
||||
|qs: &QueryServer, idms: &IdmServer, _idms_delayed: &IdmServerDelayed| {
|
||||
|qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
init_admin_w_password(qs, TEST_PASSWORD).expect("Failed to setup admin account");
|
||||
check_admin_password(idms, TEST_PASSWORD);
|
||||
|
||||
// Clear our the session record
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
idms_delayed.check_is_empty_or_panic();
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -2415,7 +2463,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_idm_simple_password_spn_auth() {
|
||||
run_idm_test!(
|
||||
|qs: &QueryServer, idms: &IdmServer, _idms_delayed: &IdmServerDelayed| {
|
||||
|qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
init_admin_w_password(qs, TEST_PASSWORD).expect("Failed to setup admin account");
|
||||
|
||||
let sid = init_admin_authsession_sid(
|
||||
|
@ -2460,6 +2508,11 @@ mod tests {
|
|||
}
|
||||
};
|
||||
|
||||
// Clear our the session record
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
idms_delayed.check_is_empty_or_panic();
|
||||
|
||||
idms_auth.commit().expect("Must not fail");
|
||||
}
|
||||
)
|
||||
|
@ -2874,12 +2927,23 @@ mod tests {
|
|||
idms_delayed.check_is_empty_or_panic();
|
||||
// Do an auth, this will trigger the action to send.
|
||||
check_admin_password(idms, "password");
|
||||
|
||||
// process it.
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
// The first task is the pw upgrade
|
||||
assert!(matches!(da, DelayedAction::PwUpgrade(_)));
|
||||
let r = task::block_on(idms.delayed_action(duration_from_epoch_now(), da));
|
||||
// The second is the auth session record
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
assert!(Ok(true) == r);
|
||||
|
||||
// Check the admin pw still matches
|
||||
check_admin_password(idms, "password");
|
||||
// Clear the next auth session record
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
|
||||
// No delayed action was queued.
|
||||
idms_delayed.check_is_empty_or_panic();
|
||||
}
|
||||
|
@ -3152,7 +3216,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_idm_account_softlocking() {
|
||||
run_idm_test!(
|
||||
|qs: &QueryServer, idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed| {
|
||||
|qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
init_admin_w_password(qs, TEST_PASSWORD).expect("Failed to setup admin account");
|
||||
|
||||
// Auth invalid, no softlock present.
|
||||
|
@ -3286,6 +3350,12 @@ mod tests {
|
|||
};
|
||||
|
||||
idms_auth.commit().expect("Must not fail");
|
||||
|
||||
// Clear the auth session record
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
idms_delayed.check_is_empty_or_panic();
|
||||
|
||||
// Auth valid after reset at, count == 0.
|
||||
// Tested in the softlock state machine.
|
||||
|
||||
|
@ -3453,13 +3523,20 @@ mod tests {
|
|||
#[test]
|
||||
fn test_idm_jwt_uat_expiry() {
|
||||
run_idm_test!(
|
||||
|qs: &QueryServer, idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed| {
|
||||
|qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
let expiry = ct + Duration::from_secs(AUTH_SESSION_EXPIRY + 1);
|
||||
// Do an authenticate
|
||||
init_admin_w_password(qs, TEST_PASSWORD).expect("Failed to setup admin account");
|
||||
let token = check_admin_password(idms, TEST_PASSWORD);
|
||||
|
||||
// Clear our the session record
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
let r = task::block_on(idms.delayed_action(duration_from_epoch_now(), da));
|
||||
assert!(Ok(true) == r);
|
||||
idms_delayed.check_is_empty_or_panic();
|
||||
|
||||
let idms_prox_read = idms.proxy_read();
|
||||
|
||||
// Check it's valid.
|
||||
|
@ -3477,6 +3554,80 @@ mod tests {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_account_session_validation() {
|
||||
run_idm_test!(
|
||||
|qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
use compact_jwt::{Jws, JwsUnverified};
|
||||
use kanidm_proto::v1::UserAuthToken;
|
||||
use std::str::FromStr;
|
||||
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
|
||||
let post_grace = ct + GRACE_WINDOW + Duration::from_secs(1);
|
||||
let expiry = ct + Duration::from_secs(AUTH_SESSION_EXPIRY + 1);
|
||||
|
||||
// Assert that our grace time is less than expiry, so we know the failure is due to
|
||||
// this.
|
||||
assert!(post_grace < expiry);
|
||||
|
||||
// Do an authenticate
|
||||
init_admin_w_password(qs, TEST_PASSWORD).expect("Failed to setup admin account");
|
||||
let token = check_admin_password(idms, TEST_PASSWORD);
|
||||
|
||||
// Process the session info.
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
let r = task::block_on(idms.delayed_action(duration_from_epoch_now(), da));
|
||||
assert!(Ok(true) == r);
|
||||
|
||||
let uat_unverified =
|
||||
JwsUnverified::from_str(&token).expect("Failed to parse apitoken");
|
||||
let uat_inner: Jws<UserAuthToken> = uat_unverified
|
||||
.validate_embeded()
|
||||
.expect("Embedded jwk not found");
|
||||
let uat_inner = uat_inner.into_inner();
|
||||
|
||||
let idms_prox_read = idms.proxy_read();
|
||||
|
||||
// Check it's valid.
|
||||
idms_prox_read
|
||||
.validate_and_parse_token_to_ident(Some(token.as_str()), ct)
|
||||
.expect("Failed to validate");
|
||||
|
||||
// If the auth session record wasn't processed, this will fail.
|
||||
idms_prox_read
|
||||
.validate_and_parse_token_to_ident(Some(token.as_str()), post_grace)
|
||||
.expect("Failed to validate");
|
||||
|
||||
drop(idms_prox_read);
|
||||
|
||||
// Mark the session as invalid now.
|
||||
let idms_prox_write = idms.proxy_write(ct.clone());
|
||||
let dte =
|
||||
DestroySessionTokenEvent::new_internal(uat_inner.uuid, uat_inner.session_id);
|
||||
assert!(idms_prox_write.account_destroy_session_token(&dte).is_ok());
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
|
||||
// Now check again with the session destroyed.
|
||||
let idms_prox_read = idms.proxy_read();
|
||||
|
||||
// Now, within gracewindow, it's still valid.
|
||||
idms_prox_read
|
||||
.validate_and_parse_token_to_ident(Some(token.as_str()), ct)
|
||||
.expect("Failed to validate");
|
||||
|
||||
// post grace, it's not valid.
|
||||
match idms_prox_read
|
||||
.validate_and_parse_token_to_ident(Some(token.as_str()), post_grace)
|
||||
{
|
||||
Err(OperationError::SessionExpired) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_uat_claim_insertion() {
|
||||
run_idm_test!(|_qs: &QueryServer,
|
||||
|
@ -3584,11 +3735,17 @@ mod tests {
|
|||
#[test]
|
||||
fn test_idm_jwt_uat_token_key_reload() {
|
||||
run_idm_test!(
|
||||
|qs: &QueryServer, idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed| {
|
||||
|qs: &QueryServer, idms: &IdmServer, idms_delayed: &mut IdmServerDelayed| {
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
|
||||
init_admin_w_password(qs, TEST_PASSWORD).expect("Failed to setup admin account");
|
||||
let token = check_admin_password(idms, TEST_PASSWORD);
|
||||
|
||||
// Clear the session record
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
idms_delayed.check_is_empty_or_panic();
|
||||
|
||||
let idms_prox_read = idms.proxy_read();
|
||||
|
||||
// Check it's valid.
|
||||
|
@ -3619,6 +3776,11 @@ mod tests {
|
|||
// Check the old token is invalid, due to reload.
|
||||
let new_token = check_admin_password(idms, TEST_PASSWORD);
|
||||
|
||||
// Clear the session record
|
||||
let da = idms_delayed.try_recv().expect("invalid");
|
||||
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
|
||||
idms_delayed.check_is_empty_or_panic();
|
||||
|
||||
let idms_prox_read = idms.proxy_read();
|
||||
assert!(idms_prox_read
|
||||
.validate_and_parse_token_to_ident(Some(token.as_str()), ct)
|
||||
|
|
|
@ -26,8 +26,6 @@ use crate::value::Session;
|
|||
|
||||
// revoke
|
||||
|
||||
const GRACE_WINDOW: Duration = Duration::from_secs(600);
|
||||
|
||||
macro_rules! try_from_entry {
|
||||
($value:expr) => {{
|
||||
// Check the classes
|
||||
|
@ -324,7 +322,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
admin_error!("Failed to begin ssh key read: {:?}", e);
|
||||
admin_error!("Failed to begin service account api token list: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
@ -376,7 +374,7 @@ mod tests {
|
|||
use compact_jwt::{Jws, JwsUnverified};
|
||||
use kanidm_proto::v1::ApiToken;
|
||||
|
||||
use super::{DestroyApiTokenEvent, GenerateApiTokenEvent, GRACE_WINDOW};
|
||||
use super::{DestroyApiTokenEvent, GenerateApiTokenEvent};
|
||||
// use crate::prelude::*;
|
||||
use crate::event::CreateEvent;
|
||||
use crate::idm::server::IdmServerTransaction;
|
||||
|
|
|
@ -2655,6 +2655,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY,
|
||||
JSON_SCHEMA_ATTR_API_TOKEN_SESSION,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP,
|
||||
JSON_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION,
|
||||
JSON_SCHEMA_CLASS_PERSON,
|
||||
JSON_SCHEMA_CLASS_ORGPERSON,
|
||||
JSON_SCHEMA_CLASS_GROUP,
|
||||
|
|
|
@ -291,7 +291,7 @@ impl KaniHttpServer {
|
|||
}
|
||||
|
||||
pub async fn close_connection(&self) {
|
||||
self.client.logout().await;
|
||||
assert!(self.client.logout().await.is_ok());
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
|
|
Loading…
Reference in a new issue