mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Authentication shortcut to get a RW session (#1993)
* auth: Add a privileged flag to AuthStep::Init2 step to request a rw session The privileged flag is defined as Option<bool> for compatibility with existing clients.
This commit is contained in:
parent
47e953bfd2
commit
9dda8b1ad3
|
@ -955,6 +955,7 @@ impl KanidmClient {
|
|||
step: AuthStep::Init2 {
|
||||
username: ident.to_string(),
|
||||
issue: AuthIssueSession::Token,
|
||||
privileged: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -919,6 +919,8 @@ pub enum AuthStep {
|
|||
Init2 {
|
||||
username: String,
|
||||
issue: AuthIssueSession,
|
||||
#[serde(default)]
|
||||
privileged: bool,
|
||||
},
|
||||
// We want to talk to you like this.
|
||||
Begin(AuthMech),
|
||||
|
|
|
@ -77,7 +77,9 @@ impl fmt::Display for AuthType {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
enum AuthIntent {
|
||||
InitialAuth,
|
||||
InitialAuth {
|
||||
privileged: bool,
|
||||
},
|
||||
Reauth {
|
||||
session_id: Uuid,
|
||||
session_expiry: Option<OffsetDateTime>,
|
||||
|
@ -751,6 +753,7 @@ impl AuthSession {
|
|||
pub fn new(
|
||||
account: Account,
|
||||
issue: AuthIssueSession,
|
||||
privileged: bool,
|
||||
webauthn: &Webauthn,
|
||||
ct: Duration,
|
||||
source: Source,
|
||||
|
@ -808,7 +811,7 @@ impl AuthSession {
|
|||
account,
|
||||
state,
|
||||
issue,
|
||||
intent: AuthIntent::InitialAuth,
|
||||
intent: AuthIntent::InitialAuth { privileged },
|
||||
source,
|
||||
};
|
||||
// Get the set of mechanisms that can proceed. This is tied
|
||||
|
@ -1109,7 +1112,7 @@ impl AuthSession {
|
|||
) -> Result<UserAuthToken, OperationError> {
|
||||
security_debug!("Successful cred handling");
|
||||
match self.intent {
|
||||
AuthIntent::InitialAuth => {
|
||||
AuthIntent::InitialAuth { privileged } => {
|
||||
let session_id = Uuid::new_v4();
|
||||
// We need to actually work this out better, and then
|
||||
// pass it to to_userauthtoken
|
||||
|
@ -1117,13 +1120,18 @@ impl AuthSession {
|
|||
AuthType::UnixPassword | AuthType::Anonymous => SessionScope::ReadOnly,
|
||||
AuthType::GeneratedPassword => SessionScope::ReadWrite,
|
||||
AuthType::Password | AuthType::PasswordMfa | AuthType::Passkey => {
|
||||
if privileged {
|
||||
SessionScope::ReadWrite
|
||||
} else {
|
||||
SessionScope::PrivilegeCapable
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
security_info!(
|
||||
"Issuing {:?} session {} for {} {}",
|
||||
"Issuing {:?} session ({:?}) {} for {} {}",
|
||||
self.issue,
|
||||
scope,
|
||||
session_id,
|
||||
self.account.spn,
|
||||
self.account.uuid
|
||||
|
@ -1229,11 +1237,14 @@ impl AuthSession {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub use std::collections::BTreeSet as Set;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use compact_jwt::JwsSigner;
|
||||
use compact_jwt::{Jws, JwsSigner, JwsUnverified};
|
||||
use hashbrown::HashSet;
|
||||
use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthIssueSession, AuthMech};
|
||||
use kanidm_proto::v1::{
|
||||
AuthAllowed, AuthCredential, AuthIssueSession, AuthMech, UatPurpose, UserAuthToken,
|
||||
};
|
||||
use tokio::sync::mpsc::unbounded_channel as unbounded;
|
||||
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
|
||||
use webauthn_authenticator_rs::WebauthnAuthenticator;
|
||||
|
@ -1282,6 +1293,7 @@ mod tests {
|
|||
let (session, state) = AuthSession::new(
|
||||
anon_account,
|
||||
AuthIssueSession::Token,
|
||||
false,
|
||||
&webauthn,
|
||||
duration_from_epoch_now(),
|
||||
Source::Internal,
|
||||
|
@ -1311,11 +1323,13 @@ mod tests {
|
|||
(
|
||||
$audit:expr,
|
||||
$account:expr,
|
||||
$webauthn:expr
|
||||
$webauthn:expr,
|
||||
$privileged:expr
|
||||
) => {{
|
||||
let (session, state) = AuthSession::new(
|
||||
$account.clone(),
|
||||
AuthIssueSession::Token,
|
||||
$privileged,
|
||||
$webauthn,
|
||||
duration_from_epoch_now(),
|
||||
Source::Internal,
|
||||
|
@ -1344,9 +1358,7 @@ mod tests {
|
|||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_authsession_simple_password_mech() {
|
||||
sketching::test_init();
|
||||
fn start_session_simple_password_mech(privileged: bool) -> UserAuthToken {
|
||||
let webauthn = create_webauthn();
|
||||
// create the ent
|
||||
let mut account = entry_to_account!(E_ADMIN_V1.clone());
|
||||
|
@ -1360,7 +1372,7 @@ mod tests {
|
|||
|
||||
// now check
|
||||
let (mut session, pw_badlist_cache) =
|
||||
start_password_session!(&mut audit, account, &webauthn);
|
||||
start_password_session!(&mut audit, account, &webauthn, false);
|
||||
|
||||
let attempt = AuthCredential::Password("bad_password".to_string());
|
||||
let jws_signer = create_jwt_signer();
|
||||
|
@ -1385,10 +1397,10 @@ mod tests {
|
|||
// === Now begin a new session, and use a good pw.
|
||||
|
||||
let (mut session, pw_badlist_cache) =
|
||||
start_password_session!(&mut audit, account, &webauthn);
|
||||
start_password_session!(&mut audit, account, &webauthn, privileged);
|
||||
|
||||
let attempt = AuthCredential::Password("test_password".to_string());
|
||||
match session.validate_creds(
|
||||
let uat: UserAuthToken = match session.validate_creds(
|
||||
&attempt,
|
||||
Duration::from_secs(0),
|
||||
&async_tx,
|
||||
|
@ -1397,7 +1409,12 @@ mod tests {
|
|||
&jws_signer,
|
||||
&AccountPolicy::from_pw_badlist_cache(pw_badlist_cache),
|
||||
) {
|
||||
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
|
||||
Ok(AuthState::Success(jwt, AuthIssueSession::Token)) => {
|
||||
let uat = JwsUnverified::from_str(&jwt).expect("Failed to parse jwt");
|
||||
let uat: Jws<UserAuthToken> =
|
||||
uat.validate_embeded().expect("Embedded uat not found");
|
||||
uat.into_inner()
|
||||
}
|
||||
_ => panic!(),
|
||||
};
|
||||
|
||||
|
@ -1410,6 +1427,34 @@ mod tests {
|
|||
assert!(async_rx.blocking_recv().is_none());
|
||||
drop(audit_tx);
|
||||
assert!(audit_rx.blocking_recv().is_none());
|
||||
|
||||
uat
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_authsession_simple_password_mech() {
|
||||
sketching::test_init();
|
||||
let uat = start_session_simple_password_mech(false);
|
||||
match uat.purpose {
|
||||
UatPurpose::ReadOnly => panic!("Unexpected UatPurpose::ReadOnly"),
|
||||
UatPurpose::ReadWrite { expiry } => {
|
||||
// Long lived RO session capable of reauth
|
||||
assert!(expiry.is_none())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_authsession_simple_password_mech_priv_shortcut() {
|
||||
sketching::test_init();
|
||||
let uat = start_session_simple_password_mech(true);
|
||||
match uat.purpose {
|
||||
UatPurpose::ReadOnly => panic!("Unexpected UatPurpose::ReadOnly"),
|
||||
UatPurpose::ReadWrite { expiry } => {
|
||||
// Short lived RW session
|
||||
assert!(expiry.is_some())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1429,7 +1474,7 @@ mod tests {
|
|||
|
||||
// now check, even though the password is correct, Auth should be denied since it is in badlist
|
||||
let (mut session, pw_badlist_cache) =
|
||||
start_password_session!(&mut audit, account, &webauthn);
|
||||
start_password_session!(&mut audit, account, &webauthn, false);
|
||||
|
||||
let attempt = AuthCredential::Password("list@no3IBTyqHu$bad".to_string());
|
||||
match session.validate_creds(
|
||||
|
@ -1464,6 +1509,7 @@ mod tests {
|
|||
let (session, state) = AuthSession::new(
|
||||
$account.clone(),
|
||||
AuthIssueSession::Token,
|
||||
false,
|
||||
$webauthn,
|
||||
duration_from_epoch_now(),
|
||||
Source::Internal,
|
||||
|
@ -1790,6 +1836,7 @@ mod tests {
|
|||
let (session, state) = AuthSession::new(
|
||||
$account.clone(),
|
||||
AuthIssueSession::Token,
|
||||
false,
|
||||
$webauthn,
|
||||
duration_from_epoch_now(),
|
||||
Source::Internal,
|
||||
|
|
|
@ -280,6 +280,7 @@ impl LdapTokenAuthEvent {
|
|||
pub struct AuthEventStepInit {
|
||||
pub username: String,
|
||||
pub issue: AuthIssueSession,
|
||||
pub privileged: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -311,14 +312,23 @@ impl AuthEventStep {
|
|||
Ok(AuthEventStep::Init(AuthEventStepInit {
|
||||
username,
|
||||
issue: AuthIssueSession::Token,
|
||||
privileged: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
AuthStep::Init2 { username, issue } => {
|
||||
AuthStep::Init2 {
|
||||
username,
|
||||
issue,
|
||||
privileged,
|
||||
} => {
|
||||
if username.trim().is_empty() {
|
||||
Err(OperationError::EmptyRequest)
|
||||
} else {
|
||||
Ok(AuthEventStep::Init(AuthEventStepInit { username, issue }))
|
||||
Ok(AuthEventStep::Init(AuthEventStepInit {
|
||||
username,
|
||||
issue,
|
||||
privileged,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,6 +352,7 @@ impl AuthEventStep {
|
|||
AuthEventStep::Init(AuthEventStepInit {
|
||||
username: "anonymous".to_string(),
|
||||
issue: AuthIssueSession::Token,
|
||||
privileged: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -350,6 +361,7 @@ impl AuthEventStep {
|
|||
AuthEventStep::Init(AuthEventStepInit {
|
||||
username: name.to_string(),
|
||||
issue: AuthIssueSession::Token,
|
||||
privileged: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1006,6 +1006,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
security_info!(
|
||||
username = %init.username,
|
||||
issue = ?init.issue,
|
||||
privileged = ?init.privileged,
|
||||
uuid = %euuid,
|
||||
"Initiating Authentication Session",
|
||||
);
|
||||
|
@ -1048,8 +1049,14 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
slock_ref
|
||||
});
|
||||
|
||||
let (auth_session, state) =
|
||||
AuthSession::new(account, init.issue, self.webauthn, ct, source);
|
||||
let (auth_session, state) = AuthSession::new(
|
||||
account,
|
||||
init.issue,
|
||||
init.privileged,
|
||||
self.webauthn,
|
||||
ct,
|
||||
source,
|
||||
);
|
||||
|
||||
match auth_session {
|
||||
Some(auth_session) => {
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
use std::time::SystemTime;
|
||||
|
||||
use kanidm_proto::v1::{
|
||||
ApiToken, CURegState, CredentialDetailType, Entry, Filter, Modify, ModifyList, UserAuthToken,
|
||||
ApiToken, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthResponse, AuthState,
|
||||
AuthStep, CURegState, CredentialDetailType, Entry, Filter, Modify, ModifyList, UatPurpose,
|
||||
UserAuthToken,
|
||||
};
|
||||
use kanidmd_lib::credential::totp::Totp;
|
||||
use tracing::debug;
|
||||
|
@ -13,7 +15,7 @@ use compact_jwt::JwsUnverified;
|
|||
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
|
||||
use webauthn_authenticator_rs::WebauthnAuthenticator;
|
||||
|
||||
use kanidm_client::KanidmClient;
|
||||
use kanidm_client::{ClientError, KanidmClient};
|
||||
use kanidmd_testkit::ADMIN_TEST_PASSWORD;
|
||||
|
||||
const UNIX_TEST_PASSWORD: &str = "unix test user password";
|
||||
|
@ -1221,6 +1223,67 @@ async fn setup_demo_account_passkey(rsclient: &KanidmClient) -> WebauthnAuthenti
|
|||
wa
|
||||
}
|
||||
|
||||
async fn setup_demo_account_password(
|
||||
rsclient: &KanidmClient,
|
||||
) -> Result<(String, String), ClientError> {
|
||||
let account_name = String::from_str("demo_account").expect("Failed to parse string");
|
||||
|
||||
let account_pass = String::from_str("eicieY7ahchaoCh0eeTa").expect("Failed to parse string");
|
||||
|
||||
rsclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await
|
||||
.expect("Failed to authenticate as admin");
|
||||
|
||||
// Not recommended in production!
|
||||
rsclient
|
||||
.idm_group_add_members("idm_admins", &["admin"])
|
||||
.await
|
||||
.expect("Failed to add admin to idm_admins");
|
||||
|
||||
rsclient
|
||||
.idm_person_account_create("demo_account", "Deeeeemo")
|
||||
.await
|
||||
.expect("Failed to create demo account");
|
||||
|
||||
// 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", None)
|
||||
.await
|
||||
.expect("Failed to create intent token");
|
||||
|
||||
// Logout, we don't need any auth now.
|
||||
rsclient.logout().await.expect("Failed to logout");
|
||||
|
||||
// Exchange the intent token
|
||||
let (session_token, _status) = rsclient
|
||||
.idm_account_credential_update_exchange(intent_token)
|
||||
.await
|
||||
.expect("Failed to exchange intent token");
|
||||
|
||||
// Setup and update the password
|
||||
rsclient
|
||||
.idm_account_credential_update_set_password(&session_token, account_pass.as_str())
|
||||
.await
|
||||
.expect("Failed to set password");
|
||||
|
||||
// Commit it
|
||||
rsclient
|
||||
.idm_account_credential_update_commit(&session_token)
|
||||
.await
|
||||
.expect("Failed to commit changes");
|
||||
|
||||
Ok((account_name, account_pass))
|
||||
}
|
||||
|
||||
#[kanidmd_testkit::test]
|
||||
async fn test_server_credential_update_session_passkey(rsclient: KanidmClient) {
|
||||
let mut wa = setup_demo_account_passkey(&rsclient).await;
|
||||
|
@ -1491,3 +1554,131 @@ async fn test_privilege_expiry(rsclient: KanidmClient) {
|
|||
let result = rsclient.system_auth_privilege_expiry_get().await.unwrap();
|
||||
assert_eq!(authsession_expiry, result);
|
||||
}
|
||||
|
||||
async fn start_password_session(
|
||||
rsclient: &KanidmClient,
|
||||
username: &str,
|
||||
password: &str,
|
||||
privileged: bool,
|
||||
) -> Result<UserAuthToken, ()> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let authreq = AuthRequest {
|
||||
step: AuthStep::Init2 {
|
||||
username: username.to_string(),
|
||||
issue: AuthIssueSession::Token,
|
||||
privileged,
|
||||
},
|
||||
};
|
||||
let authreq = serde_json::to_string(&authreq).expect("Failed to serialize AuthRequest");
|
||||
|
||||
let res = match client
|
||||
.post(rsclient.make_url("/v1/auth"))
|
||||
.header("Content-Type", "application/json")
|
||||
.body(authreq)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("Failed to post: {:#?}", error),
|
||||
};
|
||||
assert_eq!(res.status(), 200);
|
||||
|
||||
let session_id = res.headers().get("x-kanidm-auth-session-id").unwrap();
|
||||
|
||||
let authreq = AuthRequest {
|
||||
step: AuthStep::Begin(AuthMech::Password),
|
||||
};
|
||||
let authreq = serde_json::to_string(&authreq).expect("Failed to serialize AuthRequest");
|
||||
|
||||
let res = match client
|
||||
.post(rsclient.make_url("/v1/auth"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-kanidm-auth-session-id", session_id)
|
||||
.body(authreq)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("Failed to post: {:#?}", error),
|
||||
};
|
||||
assert_eq!(res.status(), 200);
|
||||
|
||||
let authreq = AuthRequest {
|
||||
step: AuthStep::Cred(AuthCredential::Password(password.to_string())),
|
||||
};
|
||||
let authreq = serde_json::to_string(&authreq).expect("Failed to serialize AuthRequest");
|
||||
|
||||
let res = match client
|
||||
.post(rsclient.make_url("/v1/auth"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-kanidm-auth-session-id", session_id)
|
||||
.body(authreq)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("Failed to post: {:#?}", error),
|
||||
};
|
||||
assert_eq!(res.status(), 200);
|
||||
|
||||
let res: AuthResponse = res.json().await.expect("Failed to read JSON response");
|
||||
let jwt = match res.state {
|
||||
AuthState::Success(val) => val,
|
||||
_ => panic!("Failed to extract jwt"),
|
||||
};
|
||||
|
||||
let jwt = JwsUnverified::from_str(&jwt).expect("Failed to parse jwt");
|
||||
let uat: UserAuthToken = jwt
|
||||
.validate_embeded()
|
||||
.map(|jws| jws.into_inner())
|
||||
.expect("Unable extract uat");
|
||||
|
||||
Ok(uat)
|
||||
}
|
||||
|
||||
#[kanidmd_testkit::test]
|
||||
async fn test_server_user_auth_unprivileged(rsclient: KanidmClient) {
|
||||
let (account_name, account_pass) = setup_demo_account_password(&rsclient)
|
||||
.await
|
||||
.expect("Failed to setup demo_account");
|
||||
|
||||
let uat = start_password_session(
|
||||
&rsclient,
|
||||
account_name.as_str(),
|
||||
account_pass.as_str(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to start session");
|
||||
|
||||
match uat.purpose {
|
||||
UatPurpose::ReadOnly => panic!("Unexpected uat purpose"),
|
||||
UatPurpose::ReadWrite { expiry } => {
|
||||
assert!(expiry.is_none())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[kanidmd_testkit::test]
|
||||
async fn test_server_user_auth_privileged_shortcut(rsclient: KanidmClient) {
|
||||
let (account_name, account_pass) = setup_demo_account_password(&rsclient)
|
||||
.await
|
||||
.expect("Failed to setup demo_account");
|
||||
|
||||
let uat = start_password_session(
|
||||
&rsclient,
|
||||
account_name.as_str(),
|
||||
account_pass.as_str(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to start session");
|
||||
|
||||
match uat.purpose {
|
||||
UatPurpose::ReadOnly => panic!("Unexpected uat purpose"),
|
||||
UatPurpose::ReadWrite { expiry } => {
|
||||
assert!(expiry.is_some())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,6 +100,7 @@ impl LoginApp {
|
|||
step: AuthStep::Init2 {
|
||||
username,
|
||||
issue: AuthIssueSession::Token,
|
||||
privileged: false,
|
||||
},
|
||||
};
|
||||
let req_jsvalue = serde_json::to_string(&authreq)
|
||||
|
|
Loading…
Reference in a new issue