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"
|
rustls-native-certs = "0.6.1"
|
||||||
|
|
||||||
shadow-rs = "0.11"
|
shadow-rs = "0.11"
|
||||||
|
reqwest = { version = "0.12.9", features = ["blocking", "json"] }
|
||||||
|
|
||||||
[dependencies.rsasl]
|
[dependencies.rsasl]
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
default_features = false
|
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]
|
[dev-dependencies]
|
||||||
futures-test = "0.3.16"
|
futures-test = "0.3.16"
|
||||||
|
@ -12,15 +12,19 @@ use crate::users::db::User;
|
|||||||
|
|
||||||
mod fabfire;
|
mod fabfire;
|
||||||
mod fabfire_bin;
|
mod fabfire_bin;
|
||||||
|
mod oidc;
|
||||||
|
use crate::config::OIDCConfig;
|
||||||
|
|
||||||
struct Callback {
|
struct Callback {
|
||||||
users: Users,
|
users: Users,
|
||||||
span: tracing::Span,
|
span: tracing::Span,
|
||||||
|
oidc: Option<oidc::OIDC>,
|
||||||
}
|
}
|
||||||
impl Callback {
|
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");
|
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 {
|
impl SessionCallback for Callback {
|
||||||
@ -44,6 +48,9 @@ impl SessionCallback for Callback {
|
|||||||
Ok(card_key)
|
Ok(card_key)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
if let Some(oidc) = &self.oidc {
|
||||||
|
oidc.sasl_callback(session_data, context, request)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +101,11 @@ impl SessionCallback for Callback {
|
|||||||
validate.finalize::<V>(user)
|
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 {
|
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 span = tracing::debug_span!("authentication");
|
||||||
let _guard = span.enter();
|
let _guard = span.enter();
|
||||||
|
|
||||||
let config = SASLConfig::builder()
|
let config = SASLConfig::builder()
|
||||||
.with_defaults()
|
.with_defaults()
|
||||||
.with_callback(Callback::new(userdb))
|
.with_callback(Callback::new(userdb, oidc_config))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mechs: Vec<&'static str> = SASLServer::<V>::new(config.clone())
|
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 {
|
} else {
|
||||||
let mut builder = builder.init_failed();
|
let mut builder = builder.init_failed();
|
||||||
builder.set_code(ErrorCode::InvalidCredentials);
|
builder.set_code(ErrorCode::InvalidCredentials);
|
||||||
|
builder.set_additional_data(out.as_slice());
|
||||||
|
|
||||||
response = Response {
|
response = Response {
|
||||||
union_field: "error",
|
union_field: "error",
|
||||||
|
@ -61,6 +61,13 @@ pub struct MachineDescription {
|
|||||||
pub privs: PrivilegesBuf,
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// A list of address/port pairs to listen on.
|
/// A list of address/port pairs to listen on.
|
||||||
@ -100,6 +107,9 @@ pub struct Config {
|
|||||||
pub spacename: String,
|
pub spacename: String,
|
||||||
|
|
||||||
pub instanceurl: String,
|
pub instanceurl: String,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub oidc: Option<OIDCConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@ -170,6 +180,7 @@ impl Default for Config {
|
|||||||
logging: LogConfig::default(),
|
logging: LogConfig::default(),
|
||||||
instanceurl: "".into(),
|
instanceurl: "".into(),
|
||||||
spacename: "".into(),
|
spacename: "".into(),
|
||||||
|
oidc: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use miette::Diagnostic;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub(crate) use dhall::deser_option;
|
pub(crate) use dhall::deser_option;
|
||||||
pub use dhall::{Config, MachineDescription, ModuleConfig};
|
pub use dhall::{Config, MachineDescription, ModuleConfig, OIDCConfig};
|
||||||
mod dhall;
|
mod dhall;
|
||||||
|
|
||||||
#[derive(Debug, Error, Diagnostic)]
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
|
@ -204,7 +204,7 @@ impl Diflouroborane {
|
|||||||
.map_err(BFFHError::SignalsError)?;
|
.map_err(BFFHError::SignalsError)?;
|
||||||
|
|
||||||
let sessionmanager = SessionManager::new(self.users.clone(), self.roles.clone());
|
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(
|
initiators::load(
|
||||||
self.executor.clone(),
|
self.executor.clone(),
|
||||||
|
@ -233,5 +233,17 @@
|
|||||||
--init_connections = [{ machine = "Testmachine", initiator = "Initiator" }]
|
--init_connections = [{ machine = "Testmachine", initiator = "Initiator" }]
|
||||||
|
|
||||||
instanceurl = "https://example.com",
|
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]
|
[toolchain]
|
||||||
channel = "1.66"
|
channel = "1.67"
|
||||||
|
Loading…
Reference in New Issue
Block a user