mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-28 20:03:54 +02:00
Compare commits
7 commits
7b391772d8
...
2f9537dfb2
Author | SHA1 | Date | |
---|---|---|---|
|
2f9537dfb2 | ||
|
f40679cd52 | ||
|
490a6caa18 | ||
|
f61bab6631 | ||
|
036ac23151 | ||
|
7a825ccc6d | ||
|
52824b58f1 |
|
@ -458,27 +458,24 @@ Each client has unique signing keys and access secrets, so this is limited to ea
|
|||
|
||||
## WebFinger
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> WebFinger support requires Kanidm v1.5.1 or later.
|
||||
|
||||
[WebFinger](https://datatracker.ietf.org/doc/html/rfc7033) provides a mechanism
|
||||
for discovering information about entities at a well-known URL
|
||||
(`http://example.com/.well-known/webfinger`).
|
||||
[WebFinger][webfinger] provides a mechanism for discovering information about
|
||||
entities at a well-known URL (`https://{hostname}/.well-known/webfinger`).
|
||||
|
||||
It can be used by a WebFinger client to
|
||||
[discover the OIDC issuer URL](https://datatracker.ietf.org/doc/html/rfc7033#section-3.1)
|
||||
of an identity provider from the hostname alone, and seems to be intended to
|
||||
support dynamic client registration flows for large public identity providers.
|
||||
[discover the OIDC issuer URL][webfinger-oidc] of an identity provider from the
|
||||
hostname alone, and seems to be intended to support dynamic client registration
|
||||
flows for large public identity providers.
|
||||
|
||||
Kanidm v1.5.1 and later can respond to WebFinger requests, using a user's SPN as
|
||||
the account (eg: `user@idm.example.com`). This *does not* match on email
|
||||
addresses, because they are not required by Kanidm nor guaranteed to be unique.
|
||||
part of [an `acct` URI][rfc7565] (eg: `acct:user@idm.example.com`). While SPNs
|
||||
and `acct` URIs look like email addresses, [as per RFC 7565][rfc7565s4], there
|
||||
is no guarantee that it is valid for any particular application protocol, unless
|
||||
an administrator explicitly provides for it.
|
||||
|
||||
When setting up an (enterprise) application to authenticate with Kanidm,
|
||||
WebFinger **does not add any security** over configuring an OpenID Discovery
|
||||
URL directly. In an OIDC context, the specification makes a number of flawed
|
||||
assumptions which make it difficult to use with Kanidm:
|
||||
When setting up an application to authenticate with Kanidm, WebFinger **does not
|
||||
add any security** over configuring an OpenID Discovery URL directly. In an OIDC
|
||||
context, the specification makes a number of flawed assumptions which make it
|
||||
difficult to use with Kanidm:
|
||||
|
||||
* WebFinger assumes that the identity provider will give the same `iss`
|
||||
(issuer) and OpenID Discovery document, including all URLs and signing keys,
|
||||
|
@ -486,11 +483,11 @@ assumptions which make it difficult to use with Kanidm:
|
|||
|
||||
Kanidm uses *different* `iss` (issuer), signing keys, and some client-specific
|
||||
endpoint URLs, which ensures that tokens can only be used with their intended
|
||||
service. *Changing this behaviour would reduce Kanidm's security.*
|
||||
service.
|
||||
|
||||
* WebFinger endpoints must 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?resource=acct%3Auser%40idm.example.com&rel=...`).
|
||||
`https://idm.example.com/.well-known/webfinger?resource=acct%3Auser%40idm.example.com`).
|
||||
|
||||
Unlike OIDC Discovery, WebFinger clients do not report their OAuth 2.0/OIDC
|
||||
client ID in the request, so there is no way to tell them apart.
|
||||
|
@ -517,19 +514,28 @@ assumptions which make it difficult to use with Kanidm:
|
|||
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),
|
||||
**ignoring** any supplied
|
||||
[`rel` parameter](https://datatracker.ietf.org/doc/html/rfc7033#section-4.3).
|
||||
[an Identity Provider Discovery for OIDC URL][webfinger-oidc], **ignoring**
|
||||
any supplied [`rel` parameter][webfinger-rel].
|
||||
|
||||
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.
|
||||
|
||||
WebFinger clients *may* omit the `rel=` parameter, so if another service has
|
||||
relations for an `acct:` entity and a client *does not* supply the `rel=`
|
||||
parameter, your load balancer will need to merge JSON responses from Kanidm
|
||||
and the other service(s).
|
||||
WebFinger clients *may* omit the `rel=` parameter, so if you host another
|
||||
service with relations for a Kanidm [`acct:` entity][rfc7565s4] and a client
|
||||
*does not* supply the `rel=` parameter, your load balancer will need to merge
|
||||
JSON responses from Kanidm and the other service(s).
|
||||
|
||||
Because of these issues, we recommend that (enterprise) applications support
|
||||
*directly* configuring OIDC using a Discovery URL or OAuth 2.0 Authorisation
|
||||
Server Metadata URL instead of WebFinger.
|
||||
Because of these issues, we recommend that applications support *directly*
|
||||
configuring OIDC using a Discovery URL or OAuth 2.0 Authorisation Server
|
||||
Metadata URL instead of WebFinger.
|
||||
|
||||
If a WebFinger client only checks WebFinger once during setup, you may wish to
|
||||
temporarily serve an appropriate static WebFinger document for that client
|
||||
instead.
|
||||
|
||||
[rfc7565]: https://datatracker.ietf.org/doc/html/rfc7565
|
||||
[rfc7565s4]: https://datatracker.ietf.org/doc/html/rfc7565#section-4
|
||||
[webfinger]: https://datatracker.ietf.org/doc/html/rfc7033
|
||||
[webfinger-oidc]: https://datatracker.ietf.org/doc/html/rfc7033#section-3.1
|
||||
[webfinger-rel]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.3
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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())]
|
||||
|
|
Loading…
Reference in a new issue