feat: HydraClient — OAuth2/OIDC admin API (35 endpoints)
Typed Hydra admin API client covering OAuth2 clients, login/consent/logout flows, JWK sets, trusted JWT issuers, sessions, and token introspection. Bump: sunbeam-sdk v0.4.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sunbeam-sdk"
|
name = "sunbeam-sdk"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Sunbeam SDK — reusable library for cluster management"
|
description = "Sunbeam SDK — reusable library for cluster management"
|
||||||
repository = "https://src.sunbeam.pt/studio/cli"
|
repository = "https://src.sunbeam.pt/studio/cli"
|
||||||
|
|||||||
482
sunbeam-sdk/src/auth/hydra/mod.rs
Normal file
482
sunbeam-sdk/src/auth/hydra/mod.rs
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
//! Ory Hydra OAuth2/OIDC admin API client.
|
||||||
|
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
use crate::client::{AuthMethod, HttpTransport, ServiceClient};
|
||||||
|
use crate::error::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use types::*;
|
||||||
|
|
||||||
|
/// Client for the Ory Hydra Admin API.
|
||||||
|
pub struct HydraClient {
|
||||||
|
pub(crate) transport: HttpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceClient for HydraClient {
|
||||||
|
fn service_name(&self) -> &'static str {
|
||||||
|
"hydra"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_url(&self) -> &str {
|
||||||
|
&self.transport.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(base_url: String, auth: AuthMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
transport: HttpTransport::new(&base_url, auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HydraClient {
|
||||||
|
/// Build a HydraClient from domain (e.g. `https://auth.{domain}`).
|
||||||
|
pub fn connect(domain: &str) -> Self {
|
||||||
|
let base_url = format!("https://auth.{domain}");
|
||||||
|
Self::from_parts(base_url, AuthMethod::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- OAuth2 Clients -----------------------------------------------------
|
||||||
|
|
||||||
|
pub async fn list_clients(
|
||||||
|
&self,
|
||||||
|
limit: Option<u32>,
|
||||||
|
offset: Option<u32>,
|
||||||
|
) -> Result<Vec<OAuth2Client>> {
|
||||||
|
let limit = limit.unwrap_or(20);
|
||||||
|
let offset = offset.unwrap_or(0);
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/clients?limit={limit}&offset={offset}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra list clients",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_client(&self, body: &OAuth2Client) -> Result<OAuth2Client> {
|
||||||
|
self.transport
|
||||||
|
.json(Method::POST, "admin/clients", Some(body), "hydra create client")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_client(&self, id: &str) -> Result<OAuth2Client> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/clients/{id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra get client",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_client(&self, id: &str, body: &OAuth2Client) -> Result<OAuth2Client> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/clients/{id}"),
|
||||||
|
Some(body),
|
||||||
|
"hydra update client",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn patch_client(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
patches: &[serde_json::Value],
|
||||||
|
) -> Result<OAuth2Client> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PATCH,
|
||||||
|
&format!("admin/clients/{id}"),
|
||||||
|
Some(&patches),
|
||||||
|
"hydra patch client",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_client(&self, id: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("admin/clients/{id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra delete client",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_client_lifespans(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
body: &TokenLifespans,
|
||||||
|
) -> Result<OAuth2Client> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/clients/{id}/lifespans"),
|
||||||
|
Some(body),
|
||||||
|
"hydra set lifespans",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Login flow ---------------------------------------------------------
|
||||||
|
|
||||||
|
pub async fn get_login_request(&self, challenge: &str) -> Result<LoginRequest> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/oauth2/auth/requests/login?login_challenge={challenge}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra get login request",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn accept_login(
|
||||||
|
&self,
|
||||||
|
challenge: &str,
|
||||||
|
body: &AcceptLoginBody,
|
||||||
|
) -> Result<RedirectResponse> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/oauth2/auth/requests/login/accept?login_challenge={challenge}"),
|
||||||
|
Some(body),
|
||||||
|
"hydra accept login",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reject_login(
|
||||||
|
&self,
|
||||||
|
challenge: &str,
|
||||||
|
body: &RejectBody,
|
||||||
|
) -> Result<RedirectResponse> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/oauth2/auth/requests/login/reject?login_challenge={challenge}"),
|
||||||
|
Some(body),
|
||||||
|
"hydra reject login",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Consent flow -------------------------------------------------------
|
||||||
|
|
||||||
|
pub async fn get_consent_request(&self, challenge: &str) -> Result<ConsentRequest> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/oauth2/auth/requests/consent?consent_challenge={challenge}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra get consent request",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn accept_consent(
|
||||||
|
&self,
|
||||||
|
challenge: &str,
|
||||||
|
body: &AcceptConsentBody,
|
||||||
|
) -> Result<RedirectResponse> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/oauth2/auth/requests/consent/accept?consent_challenge={challenge}"),
|
||||||
|
Some(body),
|
||||||
|
"hydra accept consent",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reject_consent(
|
||||||
|
&self,
|
||||||
|
challenge: &str,
|
||||||
|
body: &RejectBody,
|
||||||
|
) -> Result<RedirectResponse> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/oauth2/auth/requests/consent/reject?consent_challenge={challenge}"),
|
||||||
|
Some(body),
|
||||||
|
"hydra reject consent",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Logout flow --------------------------------------------------------
|
||||||
|
|
||||||
|
pub async fn get_logout_request(&self, challenge: &str) -> Result<LogoutRequest> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/oauth2/auth/requests/logout?logout_challenge={challenge}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra get logout request",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn accept_logout(&self, challenge: &str) -> Result<RedirectResponse> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/oauth2/auth/requests/logout/accept?logout_challenge={challenge}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra accept logout",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reject_logout(
|
||||||
|
&self,
|
||||||
|
challenge: &str,
|
||||||
|
body: &RejectBody,
|
||||||
|
) -> Result<RedirectResponse> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/oauth2/auth/requests/logout/reject?logout_challenge={challenge}"),
|
||||||
|
Some(body),
|
||||||
|
"hydra reject logout",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- JWK ----------------------------------------------------------------
|
||||||
|
|
||||||
|
pub async fn get_jwk_set(&self, set_name: &str) -> Result<JwkSet> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/keys/{set_name}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra get jwk set",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_jwk_set(
|
||||||
|
&self,
|
||||||
|
set_name: &str,
|
||||||
|
body: &CreateJwkBody,
|
||||||
|
) -> Result<JwkSet> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
&format!("admin/keys/{set_name}"),
|
||||||
|
Some(body),
|
||||||
|
"hydra create jwk set",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_jwk_set(&self, set_name: &str, body: &JwkSet) -> Result<JwkSet> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/keys/{set_name}"),
|
||||||
|
Some(body),
|
||||||
|
"hydra update jwk set",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_jwk_set(&self, set_name: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("admin/keys/{set_name}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra delete jwk set",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_jwk_key(&self, set_name: &str, kid: &str) -> Result<JwkSet> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/keys/{set_name}/{kid}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra get jwk key",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_jwk_key(
|
||||||
|
&self,
|
||||||
|
set_name: &str,
|
||||||
|
kid: &str,
|
||||||
|
body: &serde_json::Value,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::PUT,
|
||||||
|
&format!("admin/keys/{set_name}/{kid}"),
|
||||||
|
Some(body),
|
||||||
|
"hydra update jwk key",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_jwk_key(&self, set_name: &str, kid: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("admin/keys/{set_name}/{kid}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra delete jwk key",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Trusted issuers ----------------------------------------------------
|
||||||
|
|
||||||
|
pub async fn create_trusted_issuer(
|
||||||
|
&self,
|
||||||
|
body: &TrustedJwtIssuer,
|
||||||
|
) -> Result<TrustedJwtIssuer> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::POST,
|
||||||
|
"admin/trust/grants/jwt-bearer/issuers",
|
||||||
|
Some(body),
|
||||||
|
"hydra create trusted issuer",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_trusted_issuers(&self) -> Result<Vec<TrustedJwtIssuer>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
"admin/trust/grants/jwt-bearer/issuers",
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra list trusted issuers",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_trusted_issuer(&self, id: &str) -> Result<TrustedJwtIssuer> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/trust/grants/jwt-bearer/issuers/{id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra get trusted issuer",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_trusted_issuer(&self, id: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("admin/trust/grants/jwt-bearer/issuers/{id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra delete trusted issuer",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Sessions -----------------------------------------------------------
|
||||||
|
|
||||||
|
pub async fn list_consent_sessions(&self, subject: &str) -> Result<Vec<ConsentSession>> {
|
||||||
|
self.transport
|
||||||
|
.json(
|
||||||
|
Method::GET,
|
||||||
|
&format!("admin/oauth2/auth/sessions/consent?subject={subject}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra list consent sessions",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn revoke_consent_sessions(
|
||||||
|
&self,
|
||||||
|
subject: &str,
|
||||||
|
client_id: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut path = format!("admin/oauth2/auth/sessions/consent?subject={subject}");
|
||||||
|
if let Some(cid) = client_id {
|
||||||
|
path.push_str(&format!("&client={cid}"));
|
||||||
|
}
|
||||||
|
self.transport
|
||||||
|
.send(Method::DELETE, &path, Option::<&()>::None, "hydra revoke consent")
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn revoke_login_sessions(&self, subject: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("admin/oauth2/auth/sessions/login?subject={subject}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra revoke login sessions",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Tokens -------------------------------------------------------------
|
||||||
|
|
||||||
|
pub async fn introspect_token(&self, token: &str) -> Result<IntrospectResult> {
|
||||||
|
// Hydra requires application/x-www-form-urlencoded for introspect
|
||||||
|
let resp = self
|
||||||
|
.transport
|
||||||
|
.request(Method::POST, "admin/oauth2/introspect")
|
||||||
|
.form(&[("token", token)])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| crate::error::SunbeamError::network(format!("hydra introspect: {e}")))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
return Err(crate::error::SunbeamError::network(format!(
|
||||||
|
"hydra introspect: HTTP {status}: {body}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.json().await.map_err(|e| {
|
||||||
|
crate::error::SunbeamError::network(format!("hydra introspect parse: {e}"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_tokens_for_client(&self, client_id: &str) -> Result<()> {
|
||||||
|
self.transport
|
||||||
|
.send(
|
||||||
|
Method::DELETE,
|
||||||
|
&format!("admin/oauth2/tokens?client_id={client_id}"),
|
||||||
|
Option::<&()>::None,
|
||||||
|
"hydra delete tokens",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_url() {
|
||||||
|
let c = HydraClient::connect("sunbeam.pt");
|
||||||
|
assert_eq!(c.base_url(), "https://auth.sunbeam.pt");
|
||||||
|
assert_eq!(c.service_name(), "hydra");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_parts() {
|
||||||
|
let c = HydraClient::from_parts(
|
||||||
|
"http://localhost:4445".into(),
|
||||||
|
AuthMethod::None,
|
||||||
|
);
|
||||||
|
assert_eq!(c.base_url(), "http://localhost:4445");
|
||||||
|
}
|
||||||
|
}
|
||||||
317
sunbeam-sdk/src/auth/hydra/types.rs
Normal file
317
sunbeam-sdk/src/auth/hydra/types.rs
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
//! Hydra admin API types.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// An OAuth2 client registration.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct OAuth2Client {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub client_id: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub client_name: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub client_secret: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub client_uri: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub redirect_uris: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub grant_types: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub response_types: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub scope: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub audience: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub token_endpoint_auth_method: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub metadata: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub updated_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Token lifespan configuration.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct TokenLifespans {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub authorization_code_grant_access_token_lifespan: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub authorization_code_grant_id_token_lifespan: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub authorization_code_grant_refresh_token_lifespan: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub client_credentials_grant_access_token_lifespan: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub implicit_grant_access_token_lifespan: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub implicit_grant_id_token_lifespan: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub jwt_bearer_grant_access_token_lifespan: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub refresh_token_grant_access_token_lifespan: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub refresh_token_grant_id_token_lifespan: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub refresh_token_grant_refresh_token_lifespan: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Login request details.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LoginRequest {
|
||||||
|
#[serde(default)]
|
||||||
|
pub challenge: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requested_scope: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requested_access_token_audience: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub skip: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub subject: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub client: Option<OAuth2Client>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub request_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub session_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub oidc_context: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for accepting a login request.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AcceptLoginBody {
|
||||||
|
pub subject: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub remember: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub remember_for: Option<i64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub acr: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub amr: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub context: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub force_subject_identifier: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consent request details.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConsentRequest {
|
||||||
|
#[serde(default)]
|
||||||
|
pub challenge: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requested_scope: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requested_access_token_audience: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub skip: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub subject: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub client: Option<OAuth2Client>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub request_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub login_challenge: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub login_session_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub acr: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub amr: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub context: Option<serde_json::Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub oidc_context: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for accepting a consent request.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AcceptConsentBody {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub grant_scope: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub grant_access_token_audience: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub session: Option<ConsentSession>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub remember: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub remember_for: Option<i64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub handled_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consent session data.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct ConsentSession {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub access_token: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub id_token: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub consent_request: Option<Box<ConsentRequest>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub grant_scope: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub grant_access_token_audience: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub handled_at: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub session: Option<Box<ConsentSessionData>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inner session data.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct ConsentSessionData {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub access_token: Option<serde_json::Value>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub id_token: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for rejecting a request.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RejectBody {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub error: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub error_debug: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub error_description: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub error_hint: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status_code: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redirect response from accept/reject operations.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RedirectResponse {
|
||||||
|
pub redirect_to: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Logout request details.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LogoutRequest {
|
||||||
|
#[serde(default)]
|
||||||
|
pub challenge: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub subject: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub session_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub request_url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub rp_initiated: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub client: Option<OAuth2Client>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A JWK set.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct JwkSet {
|
||||||
|
#[serde(default)]
|
||||||
|
pub keys: Vec<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body for creating a JWK set.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateJwkBody {
|
||||||
|
pub alg: String,
|
||||||
|
pub kid: String,
|
||||||
|
#[serde(rename = "use")]
|
||||||
|
pub use_: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trusted JWT issuer.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TrustedJwtIssuer {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub issuer: String,
|
||||||
|
pub subject: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub scope: Vec<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub public_key: Option<TrustedIssuerKey>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub expires_at: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public key info for a trusted issuer.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TrustedIssuerKey {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub set: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub kid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Token introspection result.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct IntrospectResult {
|
||||||
|
pub active: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub scope: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub client_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sub: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub exp: Option<i64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub iat: Option<i64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub nbf: Option<i64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub aud: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub iss: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub token_type: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub token_use: Option<String>,
|
||||||
|
#[serde(default, rename = "ext")]
|
||||||
|
pub extra: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_oauth2_client_roundtrip() {
|
||||||
|
let client = OAuth2Client {
|
||||||
|
client_id: Some("sunbeam-cli".into()),
|
||||||
|
client_name: Some("Sunbeam CLI".into()),
|
||||||
|
redirect_uris: Some(vec!["http://localhost:9876/callback".into()]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let json = serde_json::to_value(&client).unwrap();
|
||||||
|
assert_eq!(json["client_id"], "sunbeam-cli");
|
||||||
|
assert!(json.get("client_secret").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_redirect_response() {
|
||||||
|
let json = serde_json::json!({"redirect_to": "https://example.com/callback"});
|
||||||
|
let r: RedirectResponse = serde_json::from_value(json).unwrap();
|
||||||
|
assert_eq!(r.redirect_to, "https://example.com/callback");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_introspect_active() {
|
||||||
|
let json = serde_json::json!({"active": true, "scope": "openid", "sub": "user-123"});
|
||||||
|
let r: IntrospectResult = serde_json::from_value(json).unwrap();
|
||||||
|
assert!(r.active);
|
||||||
|
assert_eq!(r.sub, Some("user-123".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
//! OAuth2 Authorization Code flow with PKCE for CLI authentication against Hydra.
|
//! OAuth2 Authorization Code flow with PKCE for CLI authentication against Hydra.
|
||||||
|
|
||||||
|
#[cfg(feature = "identity")]
|
||||||
|
pub mod hydra;
|
||||||
|
|
||||||
use crate::error::{Result, ResultExt, SunbeamError};
|
use crate::error::{Result, ResultExt, SunbeamError};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|||||||
Reference in New Issue
Block a user