Content-Length: 600317 | pFad | https://github.com/sebadob/rauthy/commit/544bebe162797870401ae60ad98dfb8cb6ecae92

8D Merge pull request #379 from sebadob/client-device-flow-impl · sebadob/rauthy@544bebe · GitHub
Skip to content

Commit

Permalink
Merge pull request #379 from sebadob/client-device-flow-impl
Browse files Browse the repository at this point in the history
feat: impl `device_code` flow into `rauthy-client`
  • Loading branch information
sebadob authored Apr 28, 2024
2 parents 201fbf2 + c9453b2 commit 544bebe
Show file tree
Hide file tree
Showing 14 changed files with 405 additions and 59 deletions.
14 changes: 14 additions & 0 deletions rauthy-client/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## UNRELEASED

### BREAKING

The `rauthy_client::init()` has been renamed to `rauthy_client::init_with()` and a new `rauthy_client::init()`
has been added which just uses safe defaults.

### Changes

- renamed `rauthy_client::init()` to `rauthy_client::init_with()` and added a new `rauthy_client::init()`
with safe defaults
- added a new features `device-code` which will make it possible to use the Device Authorization Grant flow
with this client

## v0.3.0

- Changes the `RauthyConfig` to now accept `ClaimMapping` for `admin_claim` and `user_claim`.
Expand Down
17 changes: 17 additions & 0 deletions rauthy-client/examples/device-code/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "device-code-example"
version = "0.1.0"
edition = "2021"
authors = ["Sebastian Dobe <sebastiandobe@mailbox.org>"]
license = "Apache-2.0"

[dependencies]
anyhow = "1.0.75"
axum = { version = "0.7.1", features = ["http2"] }
axum-extra = { version = "0.9.0", features = ["cookie"] }
dotenvy = "0.15.7"
tokio = { version = "1.34.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["tracing"] }

rauthy-client = { path = "../..", features = ["device-code"] }
3 changes: 3 additions & 0 deletions rauthy-client/examples/device-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Device Authorization Grant Example

TODO readme
33 changes: 33 additions & 0 deletions rauthy-client/examples/device-code/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use rauthy_client::device_code::DeviceCode;
use rauthy_client::{DangerAcceptInvalidCerts, RauthyHttpsOnly};
use tracing::{subscriber, Level};
use tracing_subscriber::FmtSubscriber;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::INFO)
.finish();
subscriber::set_global_default(subscriber).expect("setting default subscriber failed");

let mut device_code = DeviceCode::request_with(
"http://localhost:8080",
"device".to_string(),
Some("ZEW3dyKiPG27LdWrOFkNg6m9CaAEW5beoCxSufwdEBzSXMsxjNAXsnEbeI074d4V".to_string()),
None,
None,
RauthyHttpsOnly::No,
DangerAcceptInvalidCerts::Yes,
)
.await?;
println!("{}", device_code);

let _ts = device_code.wait_for_token().await?;

// If the request has been verified, we have received a TokenSet at this point,
// which we can use for furter requests.

// TODO complete the example with qr code generation and fetch_userinfo()

Ok(())
}
255 changes: 251 additions & 4 deletions rauthy-client/src/device_code.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,259 @@
use crate::rauthy_error::RauthyError;
use crate::token_set::OidcTokenSet;
use crate::{DangerAcceptInvalidCerts, RauthyHttpsOnly, RootCertificate, VERSION};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt::{Debug, Display, Formatter};
use std::ops::Add;
use std::time::Duration;
use tracing::{debug, info, warn};

#[derive(Debug, Deserialize)]
struct DeviceCodeResponse {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: Option<String>,
pub expires_in: u16,
pub interval: Option<u8>,
}

#[derive(Serialize)]
struct DeviceGrantRequest<'a> {
pub client_id: &'a str,
pub client_secret: Option<&'a str>,
pub scope: Option<&'a str>,
}

#[derive(Serialize)]
struct DeviceGrantTokenRequest {
pub client_id: String,
pub client_secret: Option<String>,
pub device_code: String,
pub grant_type: &'static str,
}

#[derive(Debug, Deserialize)]
struct MetaResponse {
pub device_authorization_endpoint: String,
pub token_endpoint: String,
}

#[derive(Debug, Deserialize)]
pub struct OAuth2ErrorResponse<'a> {
pub error: OAuth2ErrorTypeResponse,
pub error_description: Option<Cow<'a, str>>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OAuth2ErrorTypeResponse {
InvalidRequest,
InvalidClient,
InvalidGrant,
UnauthorizedClient,
UnsupportedGrantType,
InvalidScope,
// specific to the device grant
AuthorizationPending,
SlowDown,
AccessDenied,
ExpiredToken,
}

#[derive(Debug)]
pub struct DeviceCode {
pub issuer: String,
client: reqwest::Client,
token_endpoint: String,
token_endpoint_payload: DeviceGrantTokenRequest,
// token_endpoint_payload: String,
interval: u64,

pub expires: DateTime<Utc>,
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: Option<String>,
}

impl Debug for DeviceCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"DeviceCode {{ token_endpoint: {}, token_endpoint_payload: <hidden>, expires: {}, \
user_code: {}, verification_uri: {}, verification_uri_complete: {:?} }}",
self.token_endpoint,
self.expires,
self.user_code,
self.verification_uri,
self.verification_uri_complete,
)
}
}

impl Display for DeviceCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
r#"
Please visit {} and enter your User Code: {}
"#,
self.verification_uri, self.user_code
)
}
}

impl DeviceCode {
pub async fn request() -> Result<Self, RauthyError> {
todo!()
fn build_client(
root_certificate: Option<RootCertificate>,
https_only: RauthyHttpsOnly,
danger_insecure: DangerAcceptInvalidCerts,
) -> Result<reqwest::Client, RauthyError> {
let builder = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.connect_timeout(Duration::from_secs(10))
.https_only(https_only.bool())
.danger_accept_invalid_certs(danger_insecure.bool())
.user_agent(format!("Rauthy OIDC Client v{}", VERSION))
.timeout(Duration::from_secs(10));
if let Some(root) = root_certificate {
Ok(builder.add_root_certificate(root).build()?)
} else {
Ok(builder.build()?)
}
}

async fn fetch<T>(req: reqwest::RequestBuilder) -> Result<T, RauthyError>
where
T: Debug + for<'a> Deserialize<'a>,
{
let res = req.send().await?;
if !res.status().is_success() {
let status = res.status().as_u16();
let err = match res.text().await {
Ok(body) => {
format!("Rauthy request error - HTTP {}: {:?}", status, body)
}
Err(_) => {
format!("Rauthy request error - HTTP {}", status)
}
};
return Err(RauthyError::Request(Cow::from(err)));
}
Ok(res.json::<T>().await?)
}

//github.com/ Request a `device_code` from your Rauthy instance.
//github.com/ This code can then be used in exchange for an OIDC Token Set.
//github.com/ Clients requesting `device_code`'s are typically public. If you have a confidential
//github.com/ client or need additional configuration, use `DeviceCode::request_with()`.
pub async fn request(issuer: &str, client_id: String) -> Result<Self, RauthyError> {
Self::request_with(
issuer,
client_id,
None,
None,
None,
RauthyHttpsOnly::Yes,
DangerAcceptInvalidCerts::No,
)
.await
}

//github.com/ Request a `device_code` from your Rauthy instance.
//github.com/ This code can then be used in exchange for an OIDC Token Set.
pub async fn request_with(
issuer: &str,
client_id: String,
client_secret: Option<String>,
scope: Option<&str>,
root_certificate: Option<RootCertificate>,
https_only: RauthyHttpsOnly,
danger_insecure: DangerAcceptInvalidCerts,
) -> Result<Self, RauthyError> {
let append = if issuer.ends_with('/') {
".well-known/openid-configuration"
} else {
"/.well-known/openid-configuration"
};
let oidc_config_url = format!("{}{}", issuer, append);

let client = Self::build_client(root_certificate, https_only, danger_insecure)?;
let meta = Self::fetch::<MetaResponse>(client.get(oidc_config_url)).await?;
let device_code = Self::fetch::<DeviceCodeResponse>(
client
.post(meta.device_authorization_endpoint)
.form(&DeviceGrantRequest {
client_id: &client_id,
client_secret: client_secret.as_deref(),
scope,
}),
)
.await?;
let expires = Utc::now().add(chrono::Duration::seconds(device_code.expires_in as i64));
let token_endpoint_payload = DeviceGrantTokenRequest {
client_id,
client_secret,
device_code: device_code.device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
};

Ok(DeviceCode {
client,
token_endpoint: meta.token_endpoint,
token_endpoint_payload,
interval: device_code.interval.unwrap_or(5) as u64,
expires,
user_code: device_code.user_code,
verification_uri: device_code.verification_uri,
verification_uri_complete: device_code.verification_uri_complete,
})
}

//github.com/ With a valid `device_code`, continuously poll the Rauthy instance and wait
//github.com/ for user verification of your request, to get an OIDC Token Set.
pub async fn wait_for_token(&mut self) -> Result<OidcTokenSet, RauthyError> {
let mut wait_for = self.interval;

loop {
tokio::time::sleep(Duration::from_secs(wait_for)).await;

let res = self
.client
.post(&self.token_endpoint)
.form(&self.token_endpoint_payload)
.send()
.await?;

if res.status().is_success() {
let ts = res.json::<OidcTokenSet>().await?;
info!("Success - received an OIDC TokenSet");
return Ok(ts);
}

let err = res.json::<OAuth2ErrorResponse>().await?;
match err.error {
OAuth2ErrorTypeResponse::AuthorizationPending => {
debug!("Authorization Pending - awaiting user verification");
}

// this should not happen with Rauthy -> should always be just 5 seconds
OAuth2ErrorTypeResponse::SlowDown => {
warn!("Received a `slow_down` - doubling token fetch interval");
wait_for *= 2;
}

OAuth2ErrorTypeResponse::AccessDenied => {
return Err(RauthyError::Provider(Cow::from(format!("{:?}", err))));
}

OAuth2ErrorTypeResponse::ExpiredToken => {
return Err(RauthyError::Provider(Cow::from(format!("{:?}", err))));
}

// the others should not come up, only if the connection dies in between
// or something like that
_ => return Err(RauthyError::Provider(Cow::from(format!("{:?}", err)))),
}
}
}
}
19 changes: 16 additions & 3 deletions rauthy-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ pub mod provider;
//github.com/ Provides everything necessary to extract and validate JWT token claims
pub mod token_set;

#[cfg(feature = "device_code")]
#[cfg(feature = "device-code")]
pub mod device_code;

mod rauthy_error;

pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
Expand Down Expand Up @@ -165,13 +166,25 @@ impl DangerAcceptInvalidCerts {
}
}

//github.com/ This function must(!) be called exactly once during your app start up before(!) the
//github.com/ The init function must be called exactly once during your app start up before(!) the
//github.com/ OidcProvider::setup_*() function.
//github.com/ It will initialize variables, clients, cache, and validate the OIDC configuration.
//github.com/
//github.com/ # Panics
//github.com/ This will panic if it is called more than once.
pub async fn init() -> Result<(), RauthyError> {
OidcProvider::init_client(None, RauthyHttpsOnly::Yes, DangerAcceptInvalidCerts::No)?;
jwks_handler().await;
Ok(())
}

//github.com/ This function must be called exactly once during your app start up before(!) the
//github.com/ OidcProvider::setup_*() function.
//github.com/ It will initialize variables, clients, cache, and validate the OIDC configuration.
//github.com/
//github.com/ # Panics
//github.com/ This will panic if it is called more than once.
pub async fn init(
pub async fn init_with(
root_certificate: Option<RootCertificate>,
https_only: RauthyHttpsOnly,
danger_accept_invalid_certs: DangerAcceptInvalidCerts,
Expand Down
1 change: 1 addition & 0 deletions rauthy-client/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl OidcProviderConfig {
pub struct OidcProvider {
pub issuer: String,
pub authorization_endpoint: String,
pub device_authorization_endpoint: String,
pub token_endpoint: String,
pub introspection_endpoint: String,
pub userinfo_endpoint: String,
Expand Down
Loading

0 comments on commit 544bebe

Please sign in to comment.








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://github.com/sebadob/rauthy/commit/544bebe162797870401ae60ad98dfb8cb6ecae92

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy