kanidm/tools/device_flow/examples/device_flow.rs
2024-10-26 12:08:48 +10:00

137 lines
4.5 KiB
Rust

use std::str::FromStr;
use kanidm_proto::constants::uri::{
OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_DEVICE, OAUTH2_TOKEN_ENDPOINT,
};
use oauth2::basic::BasicClient;
use oauth2::devicecode::StandardDeviceAuthorizationResponse;
use oauth2::http::StatusCode;
use oauth2::{
AuthUrl, ClientId, DeviceAuthorizationUrl, HttpRequest, HttpResponse, Scope, TokenUrl,
};
use reqwest::Client;
use sketching::tracing_subscriber::layer::SubscriberExt;
use sketching::tracing_subscriber::util::SubscriberInitExt;
use sketching::tracing_subscriber::{fmt, EnvFilter};
use tracing::level_filters::LevelFilter;
use tracing::{debug, error, info};
async fn http_client(
request: HttpRequest,
) -> Result<HttpResponse, oauth2::reqwest::Error<reqwest::Error>> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
// Following redirects opens the client up to SSRF vulnerabilities.
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(oauth2::reqwest::Error::Reqwest)?;
let method = reqwest::Method::from_str(request.method.as_str())
.map_err(|err| oauth2::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::reqwest::Error::Reqwest(err)
})?)
.await
.map_err(|err| {
error!("Failed to query url {} error={:?}", request.url, err);
oauth2::reqwest::Error::Reqwest(err)
})?;
let status_code = StatusCode::from_u16(response.status().as_u16())
.map_err(|err| oauth2::reqwest::Error::Other(err.to_string()))?;
let headers = response
.headers()
.into_iter()
.map(|(k, v)| {
debug!("header key={:?} value={:?}", k, v);
(
oauth2::http::HeaderName::from_str(k.as_str()).expect("Failed to parse header"),
oauth2::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::reqwest::Error::Reqwest(err)
})?;
info!("Response body: {:?}", String::from_utf8(body.to_vec()));
Ok(HttpResponse {
status_code,
headers,
body: body.to_vec(),
})
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let fmt_layer = fmt::layer().with_writer(std::io::stderr);
let filter_layer = EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.parse_lossy("info,kanidm_client=warn,kanidm_cli=info");
sketching::tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.init();
info!("building client...");
// kanidm system oauth2 create-public device_flow device_flow 'https://deviceauth'
let client = BasicClient::new(
ClientId::new("device_code".to_string()),
None,
AuthUrl::new(format!("https://localhost:8443{}", OAUTH2_AUTHORISE))?,
Some(TokenUrl::new(format!(
"https://localhost:8443{}",
OAUTH2_TOKEN_ENDPOINT
))?),
)
.set_device_authorization_url(DeviceAuthorizationUrl::new(format!(
"https://localhost:8443{}",
OAUTH2_AUTHORISE_DEVICE
))?);
info!("Getting details...");
let details: StandardDeviceAuthorizationResponse = client
.exchange_device_code()
.inspect_err(|err| error!("configuration error: {:?}", err))?
.add_scope(Scope::new("read".to_string()))
.request_async(http_client)
.await?;
println!(
"Open this URL in your browser: {}",
match details.verification_uri_complete() {
Some(uri) => uri.secret().as_str(),
None => details.verification_uri().as_str(),
}
);
println!("the code is {}", details.user_code().secret());
let token_result = client
.exchange_device_access_token(&details)
.request_async(http_client, tokio::time::sleep, None)
.await?;
println!("Result: {:?}", token_result);
Ok(())
}