Draft oidc integration

This commit is contained in:
Jonathan Krebs 2024-11-06 18:19:32 +01:00
parent 66877159f0
commit b50551a097
9 changed files with 177 additions and 9 deletions

View File

@ -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"

View File

@ -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<oidc::OIDC>,
}
impl Callback {
pub fn new(users: Users) -> Self {
pub fn new(users: Users, oidc_config: &Option<OIDCConfig>) -> 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::<V>(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<OIDCConfig>) -> 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::<V>::new(config.clone())

View File

@ -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<User> {
let token_parts = token.split_whitespace().collect::<Vec<_>>();
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<String, serde_json::Value> = 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::<p::OAuthBearerToken>();
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::<V>(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::<rsasl::mechanisms::oauthbearer::properties::OAuthBearerValidate, _>(| | {
let maybe_token = context.get_ref::<rsasl::property::OAuthBearerToken>();
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(())
}
}

View File

@ -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",

View File

@ -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<OIDCConfig>,
}
impl Config {
@ -170,6 +180,7 @@ impl Default for Config {
logging: LogConfig::default(),
instanceurl: "".into(),
spacename: "".into(),
oidc: None,
}
}
}

View File

@ -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)]

View File

@ -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(),

View File

@ -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/"
}
}

View File

@ -1,2 +1,2 @@
[toolchain]
channel = "1.66"
channel = "1.67"