From b50551a097fa3fb67ae6d45a025ca1cc2af9b76c Mon Sep 17 00:00:00 2001 From: Jonathan Krebs Date: Wed, 6 Nov 2024 18:19:32 +0100 Subject: [PATCH] Draft oidc integration --- Cargo.toml | 3 +- bffhd/authentication/mod.rs | 20 ++++- bffhd/authentication/oidc.rs | 131 ++++++++++++++++++++++++++++ bffhd/capnp/authenticationsystem.rs | 1 + bffhd/config/dhall.rs | 11 +++ bffhd/config/mod.rs | 2 +- bffhd/lib.rs | 2 +- examples/bffh.dhall | 14 ++- rust-toolchain.toml | 2 +- 9 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 bffhd/authentication/oidc.rs diff --git a/Cargo.toml b/Cargo.toml index 2805841..1931d43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,11 +111,12 @@ url = "2.2.2" rustls-native-certs = "0.6.1" shadow-rs = "0.11" +reqwest = { version = "0.12.9", features = ["blocking", "json"] } [dependencies.rsasl] version = "2.0.0" default_features = false -features = ["unstable_custom_mechanism", "provider", "registry_static", "config_builder", "plain"] +features = ["unstable_custom_mechanism", "provider", "registry_static", "config_builder", "plain", "oauthbearer"] [dev-dependencies] futures-test = "0.3.16" diff --git a/bffhd/authentication/mod.rs b/bffhd/authentication/mod.rs index f40fc3a..fee6dc6 100644 --- a/bffhd/authentication/mod.rs +++ b/bffhd/authentication/mod.rs @@ -12,15 +12,19 @@ use crate::users::db::User; mod fabfire; mod fabfire_bin; +mod oidc; +use crate::config::OIDCConfig; struct Callback { users: Users, span: tracing::Span, + oidc: Option, } impl Callback { - pub fn new(users: Users) -> Self { + pub fn new(users: Users, oidc_config: &Option) -> Self { let span = tracing::info_span!("SASL callback"); - Self { users, span } + let oidc = oidc_config.as_ref().map(|config| oidc::OIDC::new(config)); + Self { users, span, oidc } } } impl SessionCallback for Callback { @@ -44,6 +48,9 @@ impl SessionCallback for Callback { Ok(card_key) })?; } + if let Some(oidc) = &self.oidc { + oidc.sasl_callback(session_data, context, request)?; + } Ok(()) } @@ -94,6 +101,11 @@ impl SessionCallback for Callback { validate.finalize::(user) } } + "OAUTHBEARER" => { + if let Some(oidc) = &self.oidc { + oidc.sasl_validate(session_data, context, validate); + } + } _ => {} } } @@ -122,13 +134,13 @@ pub struct AuthenticationHandle { } impl AuthenticationHandle { - pub fn new(userdb: Users) -> Self { + pub fn new(userdb: Users, oidc_config: &Option) -> Self { let span = tracing::debug_span!("authentication"); let _guard = span.enter(); let config = SASLConfig::builder() .with_defaults() - .with_callback(Callback::new(userdb)) + .with_callback(Callback::new(userdb, oidc_config)) .unwrap(); let mechs: Vec<&'static str> = SASLServer::::new(config.clone()) diff --git a/bffhd/authentication/oidc.rs b/bffhd/authentication/oidc.rs new file mode 100644 index 0000000..1be677d --- /dev/null +++ b/bffhd/authentication/oidc.rs @@ -0,0 +1,131 @@ +use reqwest; +use rsasl; +use rsasl::callback::{Context, SessionData}; +use rsasl::mechanisms::oauthbearer::properties::OAuthBearerError; +use rsasl::validate::Validate; + +use miette::IntoDiagnostic; + +use crate::authentication::V; +use crate::users::db::{User, UserData}; + +// Data required for validating access tokens +pub struct OIDC { + configuration_endpoint: String, + userinfo_endpoint: String, + userid_attribute: String, + http: reqwest::blocking::Client, +} + +impl OIDC { + pub fn new(config: &crate::config::OIDCConfig) -> Self { + let http = reqwest::blocking::Client::new(); + let configuration_endpoint = config.configuration_endpoint.clone(); + let userinfo_endpoint = config.userinfo_endpoint.clone(); + let userid_attribute = config.userid_attribute.clone(); + return Self { + http, + configuration_endpoint, + userinfo_endpoint, + userid_attribute, + }; + } + + pub fn user_by_token(&self, token: &str) -> miette::Result { + let token_parts = token.split_whitespace().collect::>(); + let only_token; + if token_parts.len() == 1 { + only_token = token_parts[0]; + } else if token_parts.len() == 2 && token_parts[0].to_lowercase() == "bearer" { + only_token = token_parts[1]; + } else { + return Err(miette::Error::msg("auth token has invalid format")); + } + let response: std::collections::HashMap = self + .http + .get(&self.userinfo_endpoint) + .header("Authorization", &format!("Bearer {only_token}")) + .send() + .into_diagnostic()? + .error_for_status() + .into_diagnostic()? + .json() + .into_diagnostic()?; + + let id = response + .get(&self.userid_attribute) + .ok_or_else(|| { + miette::Error::msg("configured userid_attribute missing from userinfo response") + })? + .as_str() + .ok_or_else(|| miette::Error::msg("userid attribute value must be a string"))? + .to_owned(); + + // TODO: check audience == client id - prevents logging in with a token for another service + // TODO: get roles - from userdb or userinfo + Ok(User { + id, + userdata: UserData::new(vec![]), + }) + } + pub fn sasl_validate( + &self, + _session: &SessionData, + context: &Context, + validate: &mut Validate, + ) { + use rsasl::property as p; + let maybe_token = context.get_ref::(); + let token = match maybe_token { + Some(token) => token, + None => { + return; + } + }; + + // TODO: dont do it twice (duplicate with callback) + match self.user_by_token(token) { + Ok(user) => { + validate.finalize::(user); + } + Err(e) => { + tracing::warn!("OIDC login failure: {}", e); + } + } + } + + pub fn sasl_callback( + &self, + _session: &SessionData, + context: &Context, + request: &mut rsasl::callback::Request, + ) -> Result<(), rsasl::prelude::SessionError> { + request.satisfy_with::(| | { + let maybe_token = context.get_ref::(); + dbg!(&maybe_token); + if let Some(token) = maybe_token { + // TODO: dont do it twice (duplicate with validate) + match self.user_by_token(token) { + Ok(_user) => { + return Ok(Ok(())); + } + Err(e) => { + tracing::warn!("could not get user by token, {e}"); + dbg!(e); + } + } + } + + // FIXME: construct directly when #TODO is resolve + let e_json = serde_json::json!({"status": "invalid_token", "openid-configuration": self.configuration_endpoint.to_owned()}); + let x = serde_json::to_string(&e_json).unwrap(); + // FIXME: leaks memory. make static or something. + let x: &'static _ = Box::leak(x.into_boxed_str()); + let e : OAuthBearerError = serde_json::from_str(&x).unwrap(); + + return Ok(Err(e)); + + })?; + Ok(()) + } +} diff --git a/bffhd/capnp/authenticationsystem.rs b/bffhd/capnp/authenticationsystem.rs index c81f547..0622f91 100644 --- a/bffhd/capnp/authenticationsystem.rs +++ b/bffhd/capnp/authenticationsystem.rs @@ -143,6 +143,7 @@ impl AuthenticationSystem for Authentication { } else { let mut builder = builder.init_failed(); builder.set_code(ErrorCode::InvalidCredentials); + builder.set_additional_data(out.as_slice()); response = Response { union_field: "error", diff --git a/bffhd/config/dhall.rs b/bffhd/config/dhall.rs index 8f04d04..2b349db 100644 --- a/bffhd/config/dhall.rs +++ b/bffhd/config/dhall.rs @@ -61,6 +61,13 @@ pub struct MachineDescription { pub privs: PrivilegesBuf, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OIDCConfig { + pub configuration_endpoint: String, + pub userinfo_endpoint: String, + pub userid_attribute: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { /// A list of address/port pairs to listen on. @@ -100,6 +107,9 @@ pub struct Config { pub spacename: String, pub instanceurl: String, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub oidc: Option, } impl Config { @@ -170,6 +180,7 @@ impl Default for Config { logging: LogConfig::default(), instanceurl: "".into(), spacename: "".into(), + oidc: None, } } } diff --git a/bffhd/config/mod.rs b/bffhd/config/mod.rs index dfce376..9e52b2f 100644 --- a/bffhd/config/mod.rs +++ b/bffhd/config/mod.rs @@ -4,7 +4,7 @@ use miette::Diagnostic; use thiserror::Error; pub(crate) use dhall::deser_option; -pub use dhall::{Config, MachineDescription, ModuleConfig}; +pub use dhall::{Config, MachineDescription, ModuleConfig, OIDCConfig}; mod dhall; #[derive(Debug, Error, Diagnostic)] diff --git a/bffhd/lib.rs b/bffhd/lib.rs index 8e24c15..f855df9 100644 --- a/bffhd/lib.rs +++ b/bffhd/lib.rs @@ -204,7 +204,7 @@ impl Diflouroborane { .map_err(BFFHError::SignalsError)?; let sessionmanager = SessionManager::new(self.users.clone(), self.roles.clone()); - let authentication = AuthenticationHandle::new(self.users.clone()); + let authentication = AuthenticationHandle::new(self.users.clone(), &self.config.oidc); initiators::load( self.executor.clone(), diff --git a/examples/bffh.dhall b/examples/bffh.dhall index 65c5ae1..3fc1bcd 100644 --- a/examples/bffh.dhall +++ b/examples/bffh.dhall @@ -233,5 +233,17 @@ --init_connections = [{ machine = "Testmachine", initiator = "Initiator" }] instanceurl = "https://example.com", - spacename = "examplespace" + spacename = "examplespace", + + oidc = Some { + -- the configuration endpoint is sent to the client to discover + -- authentication and token exchange endpoints + configuration_endpoint = "https://keycloak.thejonny.de/realms/fabaccess/.well-known/openid-configuration", + -- the userinfo endpoint is queried to check an access token and to discover information about the user. + userinfo_endpoint = "https://keycloak.thejonny.de/realms/fabaccess/protocol/openid-connect/userinfo", + -- identifying attribute. defaults to "sub" + userid_attribute = "username", + -- TODO: autodiscover the endpoints from the issuer. + -- issuer = "https://keycloak.thejonny.de/realms/fabaccess/" + } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 99c6e11..5a39a27 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.66" +channel = "1.67"