mirror of
https://gitlab.com/fabinfra/fabaccess/bffh.git
synced 2024-12-22 03:33:48 +01:00
Merge branch 'oidc' into 'development'
Draft: oidc integration See merge request fabinfra/fabaccess/bffh!48
This commit is contained in:
commit
fe2c9645b7
@ -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"
|
||||
|
@ -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())
|
||||
|
131
bffhd/authentication/oidc.rs
Normal file
131
bffhd/authentication/oidc.rs
Normal 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(())
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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(),
|
||||
|
@ -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/"
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.66"
|
||||
channel = "1.67"
|
||||
|
Loading…
Reference in New Issue
Block a user