Compare commits

...

7 commits

Author SHA1 Message Date
James 8395271e21
Merge 543d3cb088 into f40679cd52 2025-02-20 14:29:38 +05:30
sinavir f40679cd52
Accept invalid certs and fix token_cache_path ()
* Add accept-invalid-certs option for cli
* Fix token_cache_path behavior

---------

Co-authored-by: sinavir <sinavir@sinavir.fr>
2025-02-20 08:07:48 +00:00
Firstyear 52824b58f1
Accept lowercase ldap pwd hashes () 2025-02-20 04:34:27 +00:00
CEbbinghaus 848af4cecd
TOTP label verification ()
* Adding TOTP Label verification (for both empty and duplicate)
2025-02-19 06:54:50 +00:00
micolous de506a5f53
Rewrite WebFinger docs () 2025-02-19 12:26:15 +10:00
micolous 7f3b1f2580
doc: fix formatting of URL table, remove Caddyfile instructions ()
There are many web servers, and this breaks the flow of the rest of the table.
2025-02-19 11:18:58 +10:00
James Roberts 543d3cb088 Replace lazy_static with LazyLock 2025-02-08 06:16:06 -05:00
13 changed files with 370 additions and 111 deletions
book/src/integrations
libs
client/src
crypto/src
proto/src/internal
server
tools/cli/src

View file

@ -70,36 +70,10 @@ anything special for Kanidm (or another provider).
**Note:** some apps automatically append `/.well-known/openid-configuration` to
the end of an OIDC Discovery URL, so you may need to omit that.
<dl>
<dt>[Webfinger](https://datatracker.ietf.org/doc/html/rfc7033) URL</dt>
<dd>
`https://idm.example.com/oauth2/openid/:client_id:/.well-known/webfinger`
The webfinger URL is implemented for each OpenID client, under its specific endpoint, giving full control to the administrator regarding which to use.
To make this compliant with the standard, it must be made available under the correct [well-known endpoint](https://datatracker.ietf.org/doc/html/rfc7033#section-10.1) (e.g `example.com/.well-known/webfinger`), typically via a reverse proxy or similar. Kanidm doesn't currently provide a mechanism for this URI rewrite.
One example would be dedicating one client as the "primary" or "default" and redirecting all requests to that. Alternatively, source IP or other request metadata could be used to decide which client to forward the request to.
### Caddy
`Caddyfile`
```caddy
# assuming a kanidm service with domain "example.com"
example.com {
redir /.well-known/webfinger https://idm.example.com/oauth2/openid/:client_id:{uri} 307
}
```
**Note:** the `{uri}` is important as it preserves the original request past the redirect.
</dd>
<dt>
[RFC 8414 OAuth 2.0 Authorisation Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) URL
[RFC 8414 OAuth 2.0 Authorisation Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414)
URL **(recommended)**
</dt>
@ -111,6 +85,21 @@ example.com {
<dt>
[WebFinger URL **(discouraged)**](#webfinger)
</dt>
<dd>
`https://idm.example.com/oauth2/openid/:client_id:/.well-known/webfinger`
See [the WebFinger section](#webfinger) for more details, as there a number of
caveats for WebFinger clients.
</dd>
<dt>
User auth
</dt>
@ -466,3 +455,61 @@ kanidm system oauth2 reset-secrets
```
Each client has unique signing keys and access secrets, so this is limited to each service.
## WebFinger
[WebFinger](https://datatracker.ietf.org/doc/html/rfc7033) provides a mechanism
for discovering information about people or other entities. It can be used by an
identity provider to supply OpenID Connect discovery information.
Kanidm provides
[an Identity Provider Discovery for OIDC URL](https://datatracker.ietf.org/doc/html/rfc7033#section-3.1)
response to all incoming WebFinger requests, using a user's SPN as their account
ID. This does not match on email addresses as they are not guaranteed to be
unique.
However, WebFinger has a number of flaws which make it difficult to use with
Kanidm:
* WebFinger assumes that the identity provider will give the same `iss`
(Issuer) for every OAuth 2.0/OIDC client, and there is no standard way for a
WebFinger client to report its client ID.
Kanidm uses a *different* `iss` (Issuer) value for each client.
* WebFinger requires that this be served at the *root* of the domain of a user's
SPN (ie: information about the user with SPN `user@idm.example.com` is at
`https://idm.example.com/.well-known/webfinger`).
Kanidm *does not* provide a WebFinger endpoint at its root URL, because it has
no way to know *which* OAuth 2.0/OIDC client a WebFinger request is associated
with, so could report an incorrect `iss` (Issuer).
You will need a load balancer in front of Kanidm's HTTPS server to redirect
requests to the appropriate `/oauth2/openid/:client_id:/.well-known/webfinger`
URL. If the client does not follow redirects, you may need to rewrite the
request in the load balancer instead.
If you have *multiple* WebFinger clients, it will need to map some other
property of the request (such as a source IP address or `User-Agent` header)
to a client ID, and redirect to the appropriate WebFinger URL for that client.
* Kanidm responds to *all* WebFinger queries with
[an Identity Provider Discovery for OIDC URL](https://datatracker.ietf.org/doc/html/rfc7033#section-3.1),
**regardless** of what
[`rel` parameter](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.1)
was specified.
This is to work around
[a broken client](https://tailscale.com/kb/1240/sso-custom-oidc) which doesn't
send a `rel` parameter, but expects an Identity Provider Discovery issuer URL
in response.
If you want to use WebFinger in any *other* context on Kanidm's hostname,
you'll need a load balancer in front of Kanidm which matches on some property
of the request.
Because of the flaws of the WebFinger specification and the deployment
difficulties they introduce, we recommend that applications use OpenID Connect
Discovery or OAuth 2.0 Authorisation Server Metadata for client configuration
instead of WebFinger.

View file

@ -94,7 +94,7 @@ pub struct KanidmClientConfigInstance {
pub verify_hostnames: Option<bool>,
/// Whether to verify the Certificate Authority details of the server's TLS certificate, defaults to `true`.
///
/// Environment variable is slightly inverted - `KANIDM_SKIP_HOSTNAME_VERIFICATION`.
/// Environment variable is slightly inverted - `KANIDM_ACCEPT_INVALID_CERTS`.
pub verify_ca: Option<bool>,
/// Optionally you can specify the path of a CA certificate to use for verifying the server, if you're not using one trusted by your system certificate store.
///
@ -453,6 +453,13 @@ impl KanidmClientBuilder {
}
}
pub fn set_token_cache_path(self, token_cache_path: Option<String>) -> Self {
KanidmClientBuilder {
token_cache_path,
..self
}
}
#[allow(clippy::result_unit_err)]
pub fn add_root_certificate_filepath(self, ca_path: &str) -> Result<Self, ClientError> {
//Okay we have a ca to add. Let's read it in and setup.

View file

@ -662,9 +662,13 @@ impl TryFrom<&str> for Password {
});
}
// Test 389ds formats
// Test 389ds/openldap formats. Shout outs openldap which sometimes makes these
// lowercase.
if let Some(ds_ssha1) = value.strip_prefix("{SHA}") {
if let Some(ds_ssha1) = value
.strip_prefix("{SHA}")
.or_else(|| value.strip_prefix("{sha}"))
{
let h = general_purpose::STANDARD.decode(ds_ssha1).map_err(|_| ())?;
if h.len() != DS_SHA1_HASH_LEN {
return Err(());
@ -674,7 +678,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha1) = value.strip_prefix("{SSHA}") {
if let Some(ds_ssha1) = value
.strip_prefix("{SSHA}")
.or_else(|| value.strip_prefix("{ssha}"))
{
let sh = general_purpose::STANDARD.decode(ds_ssha1).map_err(|_| ())?;
let (h, s) = sh.split_at(DS_SHA1_HASH_LEN);
if s.len() != DS_SHA_SALT_LEN {
@ -685,7 +692,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha256) = value.strip_prefix("{SHA256}") {
if let Some(ds_ssha256) = value
.strip_prefix("{SHA256}")
.or_else(|| value.strip_prefix("{sha256}"))
{
let h = general_purpose::STANDARD
.decode(ds_ssha256)
.map_err(|_| ())?;
@ -697,7 +707,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha256) = value.strip_prefix("{SSHA256}") {
if let Some(ds_ssha256) = value
.strip_prefix("{SSHA256}")
.or_else(|| value.strip_prefix("{ssha256}"))
{
let sh = general_purpose::STANDARD
.decode(ds_ssha256)
.map_err(|_| ())?;
@ -710,7 +723,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha512) = value.strip_prefix("{SHA512}") {
if let Some(ds_ssha512) = value
.strip_prefix("{SHA512}")
.or_else(|| value.strip_prefix("{sha512}"))
{
let h = general_purpose::STANDARD
.decode(ds_ssha512)
.map_err(|_| ())?;
@ -722,7 +738,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha512) = value.strip_prefix("{SSHA512}") {
if let Some(ds_ssha512) = value
.strip_prefix("{SSHA512}")
.or_else(|| value.strip_prefix("{ssha512}"))
{
let sh = general_purpose::STANDARD
.decode(ds_ssha512)
.map_err(|_| ())?;
@ -1441,8 +1460,12 @@ mod tests {
#[test]
fn test_password_from_ds_sha1() {
let im_pw = "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{sha}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1451,8 +1474,12 @@ mod tests {
#[test]
fn test_password_from_ds_ssha1() {
let im_pw = "{SSHA}EyzbBiP4u4zxOrLpKTORI/RX3HC6TCTJtnVOCQ==";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{ssha}EyzbBiP4u4zxOrLpKTORI/RX3HC6TCTJtnVOCQ==";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1461,8 +1488,12 @@ mod tests {
#[test]
fn test_password_from_ds_sha256() {
let im_pw = "{SHA256}XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{sha256}XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1471,8 +1502,12 @@ mod tests {
#[test]
fn test_password_from_ds_ssha256() {
let im_pw = "{SSHA256}luYWfFJOZgxySTsJXHgIaCYww4yMpu6yest69j/wO5n5OycuHFV/GQ==";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{ssha256}luYWfFJOZgxySTsJXHgIaCYww4yMpu6yest69j/wO5n5OycuHFV/GQ==";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1481,8 +1516,12 @@ mod tests {
#[test]
fn test_password_from_ds_sha512() {
let im_pw = "{SHA512}sQnzu7wkTrgkQZF+0G1hi5AI3Qmzvv0bXgc5THBqi7mAsdd4Xll27ASbRt9fEyavWi6m0QP9B8lThf+rDKy8hg==";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{sha512}sQnzu7wkTrgkQZF+0G1hi5AI3Qmzvv0bXgc5THBqi7mAsdd4Xll27ASbRt9fEyavWi6m0QP9B8lThf+rDKy8hg==";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1491,8 +1530,12 @@ mod tests {
#[test]
fn test_password_from_ds_ssha512() {
let im_pw = "{SSHA512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{ssha512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));

View file

@ -130,6 +130,7 @@ pub enum CURegState {
None,
TotpCheck(TotpSecret),
TotpTryAgain,
TotpNameTryAgain(String),
TotpInvalidSha1,
BackupCodes(Vec<String>),
Passkey(CreationChallengeResponse),

View file

@ -1396,6 +1396,7 @@ pub async fn credential_update_update(
return Err(WebError::InternalServerError(errmsg));
}
};
let session_token = match serde_json::from_value(cubody[1].clone()) {
Ok(val) => val,
Err(err) => {
@ -1406,6 +1407,7 @@ pub async fn credential_update_update(
};
debug!("session_token: {:?}", session_token);
debug!("scr: {:?}", scr);
state
.qe_r_ref
.handle_idmcredentialupdate(session_token, scr, kopid.eventid)

View file

@ -210,6 +210,8 @@ pub(crate) struct TotpInit {
pub(crate) struct TotpCheck {
wrong_code: bool,
broken_app: bool,
bad_name: bool,
taken_name: Option<String>,
}
#[derive(Template)]
@ -599,6 +601,25 @@ pub(crate) async fn add_totp(
let cu_session_token = get_cu_session(&jar).await?;
let check_totpcode = u32::from_str(&new_totp_form.check_totpcode).unwrap_or_default();
let swapped_handler_trigger =
HxResponseTrigger::after_swap([HxEvent::new("addTotpSwapped".to_string())]);
// If the user has not provided a name or added only spaces we exit early
if new_totp_form.name.trim().is_empty() {
return Ok((
swapped_handler_trigger,
AddTotpPartial {
totp_init: None,
totp_name: "".into(),
totp_value: new_totp_form.check_totpcode.clone(),
check: TotpCheck {
bad_name: true,
..Default::default()
},
},
)
.into_response());
}
let cu_status = if new_totp_form.ignore_broken_app {
// Cope with SHA1 apps because the user has intended to do so, their totp code was already verified
@ -624,6 +645,10 @@ pub(crate) async fn add_totp(
wrong_code: true,
..Default::default()
},
CURegState::TotpNameTryAgain(val) => TotpCheck {
taken_name: Some(val.clone()),
..Default::default()
},
CURegState::TotpInvalidSha1 => TotpCheck {
broken_app: true,
..Default::default()
@ -646,9 +671,6 @@ pub(crate) async fn add_totp(
new_totp_form.check_totpcode.clone()
};
let swapped_handler_trigger =
HxResponseTrigger::after_swap([HxEvent::new("addTotpSwapped".to_string())]);
Ok((
swapped_handler_trigger,
AddTotpPartial {

View file

@ -19,7 +19,8 @@
<label for="new-totp-name" class="form-label">Enter a name for your TOTP</label>
<input
aria-describedby="totp-name-validation-feedback"
class="form-control"
class="form-control (%- if let Some(_) = check.taken_name -%)is-invalid(%- endif -%)
(%- if check.bad_name -%)is-invalid(%- endif -%)"
name="name"
id="new-totp-name"
value="(( totp_name ))"
@ -51,6 +52,18 @@
<li>Incorrect TOTP code - Please try again</li>
</ul>
</div>
(% else if check.bad_name %)
<div id="neq-totp-validation-feedback">
<ul>
<li>The name you provided was empty or blank. Please provide a proper name</li>
</ul>
</div>
(% else if let Some(name) = check.taken_name %)
<div id="neq-totp-validation-feedback">
<ul>
<li>The name "((name))" is either invalid or already taken, Please pick a different one</li>
</ul>
</div>
(% endif %)
</form>

View file

@ -702,6 +702,13 @@ impl Credential {
}
}
pub(crate) fn has_totp_by_name(&self, label: &str) -> bool {
match &self.type_ {
CredentialType::PasswordMfa(_, totp, _, _) => totp.contains_key(label),
_ => false,
}
}
pub(crate) fn new_from_generatedpassword(pw: Password) -> Self {
Credential {
type_: CredentialType::GeneratedPassword(pw),

View file

@ -86,6 +86,7 @@ enum MfaRegState {
None,
TotpInit(Totp),
TotpTryAgain(Totp),
TotpNameTryAgain(Totp, String),
TotpInvalidSha1(Totp, Totp, String),
Passkey(Box<CreationChallengeResponse>, PasskeyRegistration),
#[allow(dead_code)]
@ -98,6 +99,7 @@ impl fmt::Debug for MfaRegState {
MfaRegState::None => "MfaRegState::None",
MfaRegState::TotpInit(_) => "MfaRegState::TotpInit",
MfaRegState::TotpTryAgain(_) => "MfaRegState::TotpTryAgain",
MfaRegState::TotpNameTryAgain(_, _) => "MfaRegState::TotpNameTryAgain",
MfaRegState::TotpInvalidSha1(_, _, _) => "MfaRegState::TotpInvalidSha1",
MfaRegState::Passkey(_, _) => "MfaRegState::Passkey",
MfaRegState::AttestedPasskey(_, _) => "MfaRegState::AttestedPasskey",
@ -273,6 +275,7 @@ pub enum MfaRegStateStatus {
None,
TotpCheck(TotpSecret),
TotpTryAgain,
TotpNameTryAgain(String),
TotpInvalidSha1,
BackupCodes(HashSet<String>),
Passkey(CreationChallengeResponse),
@ -283,8 +286,9 @@ impl fmt::Debug for MfaRegStateStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let t = match self {
MfaRegStateStatus::None => "MfaRegStateStatus::None",
MfaRegStateStatus::TotpCheck(_) => "MfaRegStateStatus::TotpCheck(_)",
MfaRegStateStatus::TotpCheck(_) => "MfaRegStateStatus::TotpCheck",
MfaRegStateStatus::TotpTryAgain => "MfaRegStateStatus::TotpTryAgain",
MfaRegStateStatus::TotpNameTryAgain(_) => "MfaRegStateStatus::TotpNameTryAgain",
MfaRegStateStatus::TotpInvalidSha1 => "MfaRegStateStatus::TotpInvalidSha1",
MfaRegStateStatus::BackupCodes(_) => "MfaRegStateStatus::BackupCodes",
MfaRegStateStatus::Passkey(_) => "MfaRegStateStatus::Passkey",
@ -389,6 +393,7 @@ impl Into<CUStatus> for CredentialUpdateSessionStatus {
MfaRegStateStatus::None => CURegState::None,
MfaRegStateStatus::TotpCheck(c) => CURegState::TotpCheck(c),
MfaRegStateStatus::TotpTryAgain => CURegState::TotpTryAgain,
MfaRegStateStatus::TotpNameTryAgain(label) => CURegState::TotpNameTryAgain(label),
MfaRegStateStatus::TotpInvalidSha1 => CURegState::TotpInvalidSha1,
MfaRegStateStatus::BackupCodes(s) => {
CURegState::BackupCodes(s.into_iter().collect())
@ -469,6 +474,9 @@ impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
MfaRegState::TotpInit(token) => MfaRegStateStatus::TotpCheck(
token.to_proto(session.account.name.as_str(), session.issuer.as_str()),
),
MfaRegState::TotpNameTryAgain(_, name) => {
MfaRegStateStatus::TotpNameTryAgain(name.clone())
}
MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain,
MfaRegState::TotpInvalidSha1(_, _, _) => MfaRegStateStatus::TotpInvalidSha1,
MfaRegState::Passkey(r, _) => MfaRegStateStatus::Passkey(r.as_ref().clone()),
@ -1899,7 +1907,22 @@ impl IdmServerCredUpdateTransaction<'_> {
match &session.mfaregstate {
MfaRegState::TotpInit(totp_token)
| MfaRegState::TotpTryAgain(totp_token)
| MfaRegState::TotpNameTryAgain(totp_token, _)
| MfaRegState::TotpInvalidSha1(totp_token, _, _) => {
if session
.primary
.as_ref()
.map(|cred| cred.has_totp_by_name(label))
.unwrap_or_default()
|| label.trim().is_empty()
|| !Value::validate_str_escapes(label)
{
// The user is trying to add a second TOTP under the same name. Lets save them from themselves
session.mfaregstate =
MfaRegState::TotpNameTryAgain(totp_token.clone(), label.into());
return Ok(session.deref().into());
}
if totp_token.verify(totp_chal, ct) {
// It was valid. Update the credential.
let ncred = session
@ -3368,10 +3391,39 @@ mod tests {
.credential_primary_check_totp(&cust, ct, chal + 1, "totp")
.expect("Failed to update the primary cred totp");
assert!(matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpTryAgain
));
assert!(
matches!(c_status.mfaregstate, MfaRegStateStatus::TotpTryAgain),
"{:?}",
c_status.mfaregstate
);
// Check that the user actually put something into the label
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "")
.expect("Failed to update the primary cred totp");
assert!(
matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpNameTryAgain(ref val) if val == ""
),
"{:?}",
c_status.mfaregstate
);
// Okay, Now they are trying to be smart...
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, " ")
.expect("Failed to update the primary cred totp");
assert!(
matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpNameTryAgain(ref val) if val == " "
),
"{:?}",
c_status.mfaregstate
);
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "totp")
@ -3383,6 +3435,40 @@ mod tests {
_ => false,
});
{
let c_status = cutxn
.credential_primary_init_totp(&cust, ct)
.expect("Failed to update the primary cred password");
// Check the status has the token.
let totp_token: Totp = match c_status.mfaregstate {
MfaRegStateStatus::TotpCheck(secret) => Some(secret.try_into().unwrap()),
_ => None,
}
.expect("Unable to retrieve totp token, invalid state.");
trace!(?totp_token);
let chal = totp_token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
// They tried to add a second totp under the same name
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "totp")
.expect("Failed to update the primary cred totp");
assert!(
matches!(
c_status.mfaregstate,
MfaRegStateStatus::TotpNameTryAgain(ref val) if val == "totp"
),
"{:?}",
c_status.mfaregstate
);
assert!(cutxn.credential_update_cancel_mfareg(&cust, ct).is_ok())
}
// Should be okay now!
drop(cutxn);

View file

@ -11,6 +11,7 @@ use std::convert::TryFrom;
use std::fmt;
use std::fmt::Formatter;
use std::str::FromStr;
use std::sync::LazyLock;
use std::time::Duration;
#[cfg(test)]
@ -47,83 +48,80 @@ use kanidm_proto::scim_v1::ScimOauth2ClaimMapJoinChar;
use kanidm_proto::v1::UatPurposeStatus;
use std::hash::Hash;
lazy_static! {
pub static ref SPN_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("(?P<name>[^@]+)@(?P<realm>[^@]+)").expect("Invalid SPN regex found")
};
pub static SPN_RE: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::new("(?P<name>[^@]+)@(?P<realm>[^@]+)").expect("Invalid SPN regex found")
});
pub static ref DISALLOWED_NAMES: HashSet<&'static str> = {
// Most of these were removed in favour of the unixd daemon filtering out
// local users instead.
let mut m = HashSet::with_capacity(2);
m.insert("root");
m.insert("dn=token");
m
};
pub static DISALLOWED_NAMES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
// Most of these were removed in favour of the unixd daemon filtering out
// local users instead.
let mut m = HashSet::with_capacity(2);
m.insert("root");
m.insert("dn=token");
m
});
/// Only lowercase+numbers, with limited chars.
pub static ref INAME_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^[a-z][a-z0-9-_\\.]{0,63}$").expect("Invalid Iname regex found")
};
/// Only lowercase+numbers, with limited chars.
pub static INAME_RE: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::new("^[a-z][a-z0-9-_\\.]{0,63}$").expect("Invalid Iname regex found")
});
/// Only alpha-numeric with limited special chars and space
pub static ref LABEL_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^[a-zA-Z0-9][ a-zA-Z0-9-_\\.@]{0,63}$").expect("Invalid Iname regex found")
};
/// Only alpha-numeric with limited special chars and space
pub static LABEL_RE: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::new("^[a-zA-Z0-9][ a-zA-Z0-9-_\\.@]{0,63}$").expect("Invalid Iname regex found")
});
/// Only lowercase+numbers, with limited chars.
pub static ref HEXSTR_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^[a-f0-9]+$").expect("Invalid hexstring regex found")
};
/// Only lowercase+numbers, with limited chars.
pub static HEXSTR_RE: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::new("^[a-f0-9]+$").expect("Invalid hexstring regex found")
});
pub static ref EXTRACT_VAL_DN: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^(([^=,]+)=)?(?P<val>[^=,]+)").expect("extract val from dn regex")
// Regex::new("^(([^=,]+)=)?(?P<val>[^=,]+)(,.*)?$").expect("Invalid Iname regex found")
};
pub static EXTRACT_VAL_DN: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::new("^(([^=,]+)=)?(?P<val>[^=,]+)").expect("extract val from dn regex")
// Regex::new("^(([^=,]+)=)?(?P<val>[^=,]+)(,.*)?$").expect("Invalid Iname regex found")
});
pub static ref NSUNIQUEID_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$").expect("Invalid Nsunique regex found")
};
pub static NSUNIQUEID_RE: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$").expect("Invalid Nsunique regex found")
});
/// Must not contain whitespace.
pub static ref OAUTHSCOPE_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^[0-9a-zA-Z_]+$").expect("Invalid oauthscope regex found")
};
/// Must not contain whitespace.
pub static OAUTHSCOPE_RE: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::new("^[0-9a-zA-Z_]+$").expect("Invalid oauthscope regex found")
});
pub static ref SINGLELINE_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("[\n\r\t]").expect("Invalid singleline regex found")
};
pub static SINGLELINE_RE: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::new("[\n\r\t]").expect("Invalid singleline regex found")
});
/// Per https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
/// this regex validates for valid emails.
pub static ref VALIDATE_EMAIL_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new(r"^[a-zA-Z0-9.!#$%&'*+=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$").expect("Invalid singleline regex found")
};
/// Per https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
/// this regex validates for valid emails.
pub static VALIDATE_EMAIL_RE: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::new(r"^[a-zA-Z0-9.!#$%&'*+=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$").expect("Invalid singleline regex found")
});
// Formerly checked with
/*
pub static ref ESCAPES_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new(r"\x1b\[([\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e])")
.expect("Invalid escapes regex found")
};
*/
// Formerly checked with
/*
pub static ref ESCAPES_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new(r"\x1b\[([\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e])")
.expect("Invalid escapes regex found")
};
*/
pub static ref UNICODE_CONTROL_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new(r"[[:cntrl:]]")
.expect("Invalid unicode control regex found")
};
}
pub static UNICODE_CONTROL_RE: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::new(r"[[:cntrl:]]").expect("Invalid unicode control regex found")
});
#[derive(Debug, Clone, PartialOrd, Ord, Eq, PartialEq, Hash)]
// https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim

View file

@ -91,6 +91,18 @@ impl CommonOpt {
false => client_builder,
};
let client_builder = match self.accept_invalid_certs {
true => {
warn!(
"TLS Certificate Verification disabled!!! This can lead to credential and account compromise!!!"
);
client_builder.danger_accept_invalid_certs(true)
}
false => client_builder,
};
let client_builder = client_builder.set_token_cache_path(self.token_cache_path.clone());
client_builder.build().unwrap_or_else(|e| {
error!("Failed to build client instance -- {:?}", e);
std::process::exit(1);

View file

@ -831,6 +831,13 @@ async fn totp_enroll_prompt(session_token: &CUSessionToken, client: &KanidmClien
let label: String = Input::new()
.with_prompt("TOTP Label")
.validate_with(|input: &String| -> Result<(), &str> {
if input.trim().is_empty() {
Err("Label cannot be empty")
} else {
Ok(())
}
})
.interact_text()
.expect("Failed to interact with interactive session");
@ -919,6 +926,13 @@ async fn totp_enroll_prompt(session_token: &CUSessionToken, client: &KanidmClien
eprintln!("Incorrect TOTP code entered. Please try again.");
continue;
}
Ok(CUStatus {
mfaregstate: CURegState::TotpNameTryAgain(label),
..
}) => {
eprintln!("{label} is either invalid or already taken. Please try again.");
continue;
}
Ok(CUStatus {
mfaregstate: CURegState::TotpInvalidSha1,
..

View file

@ -87,6 +87,13 @@ pub struct CommonOpt {
default_value_t = false
)]
skip_hostname_verification: bool,
/// Don't verify CA
#[clap(
long = "accept-invalid-certs",
env = "KANIDM_ACCEPT_INVALID_CERTS",
default_value_t = false
)]
accept_invalid_certs: bool,
/// Path to a file to cache tokens in, defaults to ~/.cache/kanidm_tokens
#[clap(short, long, env = "KANIDM_TOKEN_CACHE_PATH", hide = true, default_value = None,
value_parser = clap::builder::NonEmptyStringValueParser::new())]