kanidm/server/testkit/tests/oauth2_device_flow.rs

304 lines
9.8 KiB
Rust
Raw Permalink Normal View History

2024-10-26 04:08:48 +02:00
#![allow(unused_imports)]
use std::collections::BTreeMap;
use std::str::FromStr;
use compact_jwt::{JwkKeySet, JwsEs256Verifier, JwsVerifier, OidcToken, OidcUnverified};
use kanidm_client::KanidmClient;
use kanidm_proto::constants::uri::{OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT};
use kanidm_proto::internal::Oauth2ClaimMapJoin;
use kanidm_proto::oauth2::{
AccessTokenRequest, AccessTokenResponse, AuthorisationResponse, GrantTypeReq,
};
use kanidmd_lib::prelude::uri::{OAUTH2_AUTHORISE_DEVICE, OAUTH2_TOKEN_ENDPOINT};
use kanidmd_lib::prelude::{
Attribute, IDM_ALL_ACCOUNTS, OAUTH2_SCOPE_EMAIL, OAUTH2_SCOPE_OPENID, OAUTH2_SCOPE_READ,
};
use kanidmd_testkit::{
assert_no_cache, ADMIN_TEST_PASSWORD, ADMIN_TEST_USER, IDM_ADMIN_TEST_PASSWORD,
IDM_ADMIN_TEST_USER, NOT_ADMIN_TEST_EMAIL, NOT_ADMIN_TEST_PASSWORD, NOT_ADMIN_TEST_USERNAME,
TEST_INTEGRATION_RS_DISPLAY, TEST_INTEGRATION_RS_GROUP_ALL, TEST_INTEGRATION_RS_ID,
TEST_INTEGRATION_RS_REDIRECT_URL, TEST_INTEGRATION_RS_URL,
};
use oauth2_ext::basic::BasicClient;
use oauth2_ext::http::StatusCode;
use oauth2_ext::{
AuthUrl, ClientId, DeviceAuthorizationUrl, HttpRequest, HttpResponse, PkceCodeChallenge,
RequestTokenError, Scope, StandardDeviceAuthorizationResponse, StandardErrorResponse, TokenUrl,
};
use reqwest::Client;
use tracing::{debug, error, info};
use url::Url;
#[cfg(feature = "dev-oauth2-device-flow")]
async fn http_client(
request: HttpRequest,
) -> Result<HttpResponse, oauth2_ext::reqwest::Error<reqwest::Error>> {
// let ca_contents = std::fs::read("/tmp/kanidm/ca.pem")
// .map_err(|err| oauth2::reqwest::Error::Other(err.to_string()))?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
// Following redirects opens the client up to SSRF vulnerabilities.
.redirect(reqwest::redirect::Policy::none())
// reqwest::Certificate::from_der(&ca_contents)
// .map_err(oauth2::reqwest::Error::Reqwest)?,
// )
.build()
.map_err(oauth2_ext::reqwest::Error::Reqwest)?;
let method = reqwest::Method::from_str(request.method.as_str())
.map_err(|err| oauth2_ext::reqwest::Error::Other(err.to_string()))?;
let mut request_builder = client
.request(method, request.url.as_str())
.body(request.body);
for (name, value) in &request.headers {
request_builder = request_builder.header(name.as_str(), value.as_bytes());
}
let response = client
.execute(request_builder.build().map_err(|err| {
error!("Failed to build request... {:?}", err);
oauth2_ext::reqwest::Error::Reqwest(err)
})?)
.await
.map_err(|err| {
error!("Failed to query url {} error={:?}", request.url, err);
oauth2_ext::reqwest::Error::Reqwest(err)
})?;
let status_code = StatusCode::from_u16(response.status().as_u16())
.map_err(|err| oauth2_ext::reqwest::Error::Other(err.to_string()))?;
let headers = response
.headers()
.into_iter()
.map(|(k, v)| {
debug!("header key={:?} value={:?}", k, v);
(
oauth2_ext::http::HeaderName::from_str(k.as_str()).expect("Failed to parse header"),
oauth2_ext::http::HeaderValue::from_str(
v.to_str().expect("Failed to parse header value"),
)
.expect("Failed to parse header value"),
)
})
.collect();
let body = response.bytes().await.map_err(|err| {
error!("Failed to parse body...? {:?}", err);
oauth2_ext::reqwest::Error::Reqwest(err)
})?;
info!("Response body: {:?}", String::from_utf8(body.to_vec()));
Ok(HttpResponse {
status_code,
headers,
body: body.to_vec(),
})
}
#[cfg(feature = "dev-oauth2-device-flow")]
#[kanidmd_testkit::test]
async fn oauth2_device_flow(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
// Create an oauth2 application integration.
rsclient
.idm_oauth2_rs_public_create(
TEST_INTEGRATION_RS_ID,
TEST_INTEGRATION_RS_DISPLAY,
TEST_INTEGRATION_RS_URL,
)
.await
.expect("Failed to create oauth2 config");
rsclient
.idm_oauth2_client_add_origin(
TEST_INTEGRATION_RS_ID,
&Url::parse(TEST_INTEGRATION_RS_REDIRECT_URL).expect("Invalid URL"),
)
.await
.expect("Failed to update oauth2 config");
// Extend the admin account with extended details for openid claims.
rsclient
.idm_person_account_create(NOT_ADMIN_TEST_USERNAME, NOT_ADMIN_TEST_USERNAME)
.await
.expect("Failed to create account details");
rsclient
.idm_person_account_set_attr(
NOT_ADMIN_TEST_USERNAME,
Attribute::Mail.as_ref(),
&[NOT_ADMIN_TEST_EMAIL],
)
.await
.expect("Failed to create account mail");
rsclient
.idm_person_account_primary_credential_set_password(
NOT_ADMIN_TEST_USERNAME,
NOT_ADMIN_TEST_PASSWORD,
)
.await
.expect("Failed to configure account password");
rsclient
.idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true, true, true)
.await
.expect("Failed to update oauth2 config");
rsclient
.idm_oauth2_rs_update_scope_map(
TEST_INTEGRATION_RS_ID,
IDM_ALL_ACCOUNTS.name,
vec![OAUTH2_SCOPE_READ, OAUTH2_SCOPE_EMAIL, OAUTH2_SCOPE_OPENID],
)
.await
.expect("Failed to update oauth2 scopes");
rsclient
.idm_oauth2_rs_update_sup_scope_map(
TEST_INTEGRATION_RS_ID,
IDM_ALL_ACCOUNTS.name,
vec![ADMIN_TEST_USER],
)
.await
.expect("Failed to update oauth2 scopes");
// Add a custom claim map.
rsclient
.idm_oauth2_rs_update_claim_map(
TEST_INTEGRATION_RS_ID,
"test_claim",
IDM_ALL_ACCOUNTS.name,
&["claim_a".to_string(), "claim_b".to_string()],
)
.await
.expect("Failed to update oauth2 claims");
// Set an alternate join
rsclient
.idm_oauth2_rs_update_claim_map_join(
TEST_INTEGRATION_RS_ID,
"test_claim",
Oauth2ClaimMapJoin::Ssv,
)
.await
.expect("Failed to update oauth2 claims");
// Get our admin's auth token for our new client.
// We have to re-auth to update the mail field.
let res = rsclient
.auth_simple_password(IDM_ADMIN_TEST_USER, IDM_ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
// set up the device flow values
let rsdata = rsclient
.idm_oauth2_rs_get(TEST_INTEGRATION_RS_ID)
.await
.expect("failed to query rs")
.expect("failed to get rsdata");
dbg!(&rsdata);
assert!(
!rsdata
.attrs
.contains_key(Attribute::OAuth2DeviceFlowEnable.as_str()),
"Found device flow enable attribute, shouldn't be there yet!"
);
rsclient
.idm_oauth2_client_device_flow_update(TEST_INTEGRATION_RS_ID, false)
.await
.expect("Failed to update oauth2 config to disable device flow");
rsclient
.idm_oauth2_client_device_flow_update(TEST_INTEGRATION_RS_ID, true)
.await
.expect("Failed to update oauth2 config to enable device flow");
let rsdata = rsclient
.idm_oauth2_rs_get(TEST_INTEGRATION_RS_ID)
.await
.expect("failed to query rs")
.expect("failed to get rsdata");
dbg!(&rsdata);
assert!(
rsdata
.attrs
.contains_key(Attribute::OAuth2DeviceFlowEnable.as_str()),
"Couldn't find device flow enable attribute"
);
assert_eq!(
rsdata
.attrs
.get(Attribute::OAuth2DeviceFlowEnable.as_str())
.expect("Couldn't find device flow enable attribute"),
&vec!["true".to_string()],
"Device flow enable attribute not set to true"
);
// ok we've checked that adding the thing works.
// now we need to test the device flow itself.
// first we need to get the device code.
// kanidm system oauth2 create-public device_flow device_flow 'https://deviceauth'
let client = BasicClient::new(
ClientId::new(TEST_INTEGRATION_RS_ID.to_string()),
None,
AuthUrl::new(rsclient.make_url(OAUTH2_AUTHORISE).to_string())
.expect("Failed to build authurl"),
Some(
TokenUrl::new(rsclient.make_url(OAUTH2_TOKEN_ENDPOINT).to_string())
.expect("Failed to build token url"),
),
)
.set_device_authorization_url(
DeviceAuthorizationUrl::new(rsclient.make_url(OAUTH2_AUTHORISE_DEVICE).to_string())
.expect("Failed to build DeviceAuthorizationUrl"),
);
let details: StandardDeviceAuthorizationResponse = client
.exchange_device_code()
.expect("Failed to exchange device code")
.add_scope(Scope::new("read".to_string()))
.request_async(http_client)
.await
.expect("Failed to get device code!");
debug!("{:?}", details);
dbg!(&details.device_code().secret());
assert!(details.device_code().secret().len() == 24);
// now take that device code and get the token... glhf!
let result = client
.exchange_device_access_token(&details)
.request_async(
http_client,
tokio::time::sleep,
Some(std::time::Duration::from_secs(1)),
)
.await;
assert!(result.is_err());
let err = result.err().expect("Failed to get error");
dbg!(&err.to_string());
assert!(err.to_string().contains("Server returned error response"));
}