fabaccess-bffh/bffhd/authentication/oidc.rs

132 lines
4.4 KiB
Rust
Raw Permalink Normal View History

2024-11-06 18:19:32 +01:00
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(())
}
}