IDP with rauthy

This commit is contained in:
Adrian Woźniak 2024-06-26 16:34:00 +02:00
parent a6cb828aee
commit bcb14b3c48
12 changed files with 228 additions and 119 deletions

74
Cargo.lock generated
View File

@ -296,12 +296,6 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "anymap"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344"
[[package]]
name = "arc-swap"
version = "1.7.1"
@ -1232,6 +1226,7 @@ dependencies = [
"cookie 0.18.1",
"parking_lot 0.12.3",
"password-hash",
"rand 0.7.3",
"serde",
"serde_json",
"thiserror",
@ -1430,16 +1425,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df"
[[package]]
name = "ctor"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "darling"
version = "0.20.9"
@ -2155,17 +2140,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "ghost"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0e085ded9f1267c32176b40921b9754c474f7dd96f7e808d4a982e48aa1e854"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]]
name = "gimli"
version = "0.29.0"
@ -2718,28 +2692,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "inventory"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0eb5160c60ba1e809707918ee329adb99d222888155835c6feedba19f6c3fd4"
dependencies = [
"ctor",
"ghost",
"inventory-impl",
]
[[package]]
name = "inventory-impl"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e41b53715c6f0c4be49510bb82dee2c1e51c8586d885abe65396e82ed518548"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
@ -3618,7 +3570,6 @@ dependencies = [
"thiserror",
"toml 0.7.8",
"tracing",
"traitcast",
"uuid 1.9.0",
]
@ -4081,13 +4032,11 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff5addf3e73bea9ef39c44c077335f1be5e9bfa368aaa89368879fda3e931f"
dependencies = [
"actix-web",
"base64 0.22.1",
"bincode",
"cached",
"chacha20poly1305",
"chrono",
"http 1.1.0",
"jwt-simple",
"qrcode",
"rand 0.8.5",
@ -6038,27 +5987,6 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "traitcast"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f80b1cde694e5ff2dcb33875530f2f031a9a34dec8ba2744cacaf80a88658740"
dependencies = [
"inventory",
"lazy_static",
"traitcast_core",
]
[[package]]
name = "traitcast_core"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aabba8f4a83963f61a84d8cfc5829b4fad692aa6c6ad5d7b08b9549777e3cc4a"
dependencies = [
"anymap",
"inventory",
]
[[package]]
name = "try-lock"
version = "0.2.5"

View File

@ -35,9 +35,7 @@ impl CacheObject {
where
T: serde::de::DeserializeOwned,
{
let Some(data) = self.0 else {
return Ok(None)
};
let Some(data) = self.0 else { return Ok(None) };
bincode::deserialize(&data)
.map_err(|e| {
warn!("Malformed cache entry: {e}");

View File

@ -20,6 +20,8 @@ pub enum Error {
Addresses,
#[error("No account for identity found")]
InvalidIdentity,
#[error("OIDC Provider is not set up")]
OidcNotProvided,
}
pub static CLIENT_NAME: &str = "account-manager";
@ -150,6 +152,28 @@ pub mod find_by_identity {
pub type Output = Result<Details, Error>;
}
pub mod auth_check {
use model::*;
pub use crate::accounts::Error;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Input {
pub authorization: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum Details {
Redirect {
location: String,
set_cookie: String,
},
Accepted,
}
pub type Output = Result<Details, Error>;
}
impl AsyncClient {
pub async fn emit_account_created(&self, account: &model::FullAccount) {
self.publish_or_log(Topic::AccountCreated, QoS::AtLeastOnce, true, account)

View File

@ -12,6 +12,7 @@ actix-web = { workspace = true, features = [], optional = true }
cookie = { workspace = true, features = ["signed"], optional = true }
parking_lot = { workspace = true, features = [] }
password-hash = { workspace = true, features = ["alloc"] }
rand.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = [] }
thiserror = { workspace = true }

View File

@ -357,10 +357,12 @@ pub struct IdpConfig {
pub idm_url: String,
#[serde(default)]
pub secret: Option<String>,
pub enc_key: Vec<u8>,
}
impl Default for IdpConfig {
fn default() -> Self {
use rand::{distributions, Rng};
Self {
rpc_port: 19329,
rpc_bind: "0.0.0.0".into(),
@ -369,6 +371,12 @@ impl Default for IdpConfig {
database_url: "postgres://postgres@localhost/myco_accounts".into(),
idm_url: "https://localhost:8443".into(),
secret: Some("CHANGE ME".into()),
enc_key: rand::thread_rng()
.sample_iter(&distributions::Alphanumeric)
.take(32)
.map(char::from)
.collect::<String>()
.into_bytes(),
}
}
}

View File

@ -132,7 +132,7 @@ impl plugin_api::Plugin for RedisEventBusPlugin {
async fn register_event_bus(&mut self, register: &'static mut EventBusRegister) {
let Ok((stream, sender)) = RedisEventBus::connect(self.plugin_config.clone()).await else {
return
return;
};
register.register(PLUGIN_NAME, EventBus::new(stream, sender));
}

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use async_trait::async_trait;
use file_storage_adapter::{Error, PluginConfig, SResult, Url};
use futures::{AsyncRead, AsyncReadExt, TryFutureExt};
use futures::{AsyncRead, AsyncReadExt};
use s3::creds::Credentials;
use s3::{Bucket, Region};
use serde::{Deserialize, Serialize};

View File

@ -17,7 +17,7 @@ futures = { version = "0" }
gumdrop = { version = "0" }
json = { version = "0" }
model = { path = "../model", features = ['db'] }
rauthy-client = { version = "0.4.0", features = ["actix-web", "qrcode"] }
rauthy-client = { version = "0.4.0", features = ["qrcode", "userinfo"] }
rumqttc = { version = "*" }
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"] }

View File

@ -1,6 +1,9 @@
use channels::accounts::{all, find_by_identity, me, register};
use channels::accounts::{all, auth_check, find_by_identity, me, register};
use config::SharedAppConfig;
use model::{Encrypt, FullAccount, Ranged};
use rauthy_client::cookie_state::OidcCookieState;
use rauthy_client::handler::{OidcCallbackParams, OidcCookieInsecure};
use rauthy_client::principal::PrincipalOidc;
use crate::db::{AccountAddresses, Database, FindAccount};
use crate::{Error, Result};
@ -122,3 +125,142 @@ pub async fn find_by_identity(
Ok(find_by_identity::Details { account })
}
pub async fn principal_opt_from_req(value: &str) -> Option<PrincipalOidc> {
// split off the `Bearer ` part from the value
let (_bearer, access_token) = value.split_once("Bearer ").unwrap_or_default();
if access_token.is_empty() {
None
} else {
// PrincipalOidc::from_token_validated() is the important part.
// This will build and validate the principal from a given raw JWT access token.
match PrincipalOidc::from_token_validated(access_token).await {
Ok(p) => {
// This just makes sure, that at least the user_claim is satisfied.
// You might skip this, if just any user from Rauthy should have access
// to this app. no matter what groups / roles he is assigned to.
if p.is_user {
Some(p)
} else {
None
}
}
Err(_err) => None,
}
}
}
/// OIDC Auth check and login
///
/// Endpoint with no redirect on purpose to use the result inside Javascript
/// from the frontend. HTTP 202 means logged in Principal
/// HTTP 406 will have a location header and a manual redirect must be done
pub async fn get_auth_check(
input: auth_check::Input,
config: SharedAppConfig,
) -> auth_check::Output {
let enc_key = config.lock().idp().enc_key.clone();
// We need to get the principal manually.
// We use our own custom helper function to extract the principal.
let principal = principal_opt_from_req(&input.authorization).await;
// if we are in dev mode, we allow insecure cookies
let insecure = if cfg!(debug_assertions) {
OidcCookieInsecure::Yes
} else {
OidcCookieInsecure::No
};
match rauthy_client::handler::validate_principal_generic(principal, &enc_key, insecure).await {
Ok(()) => Ok(auth_check::Details::Accepted),
Err(header_values) => match header_values {
// we return HTTP 406 to not trigger default browser behavior from 401
Some((loc, state)) => Ok(auth_check::Details::Redirect {
location: loc,
set_cookie: state,
}),
None => Err(auth_check::Error::OidcNotProvided),
},
}
}
/*
/// OIDC Callback endpoint - must match the `redirect_uri` for the login flow
pub async fn get_callback(
req: HttpRequest,
config: ConfigExt,
// How you extract these OidcCallbackParams depends on your setup.
// These are query parameters that are being appended to the callback URL.
// You just need to extract these in whatever way makes sense to you.
// We need `code` and `state` params here to proceed.
params: Query<OidcCallbackParams>,
) -> HttpResponse {
let enc_key = config.enc_key.as_slice();
// We need to manually extract the state cookie
let cookie_state = match req.cookie(rauthy_client::cookie_state::OIDC_STATE_COOKIE) {
None => {
tracing::warn!("STATE_COOKIE is missing - Request may have expired");
return HttpResponse::BadRequest()
.body("STATE_COOKIE is missing - Request may have expired");
}
Some(cookie) => match OidcCookieState::from_cookie_value(cookie.value(), enc_key) {
Ok(cookie_state) => cookie_state,
Err(err) => {
return HttpResponse::BadRequest().body(err.to_string());
}
},
};
// The `DEV_MODE` again here to just have a nicer DX when developing -> we allow
// insecure cookies
let insecure = if DEV_MODE {
OidcCookieInsecure::Yes
} else {
OidcCookieInsecure::No
};
let callback_res =
rauthy_client::handler::oidc_callback(cookie_state, params.into_inner(), insecure).await;
let (cookie_str, token_set, _id_claims) = match callback_res {
Ok(res) => res,
Err(err) => {
return HttpResponse::BadRequest().body(format!("Invalid OIDC Callback: {}", err))
}
};
// At this point, the redirect was valid and everything was fine.
// Depending on how you like to proceed, you could create an independant session
// for the user, or maybe create just another factor of authentication like
// a CSRF token. Otherwise, you could just go on and using the existing
// access token for further authentication.
//
// For the sake of this example, we will return the raw access token to the user
// via the HTML so we can use it for future authentication from the
// frontend, but this is really up to you and the security needs of your
// application.
// This is a very naive approach to HTML templating and only for simplicity in
// this example. Please don't do this in production and use a proper
// templating engine.
let body = templates::HTML_CALLBACK
.replace("{{ TOKEN }}", &token_set.access_token)
.replace("{{ URI }}", "/");
HttpResponse::Ok()
.append_header((SET_COOKIE, cookie_str))
.append_header((CONTENT_TYPE, "text/html"))
.body(body)
}
pub async fn get_protected(req: HttpRequest) -> Result<HttpResponse, actix_web::Error> {
let principal = extractors::principal_from_req(&req).await?;
Ok(HttpResponse::Ok().body(format!(
"Hello from Protected Resource:<br/>{:?}",
principal
)))
}
*/

View File

@ -4,7 +4,7 @@ use config::UpdateConfig;
pub mod actions;
pub mod db;
pub mod idp;
// pub mod idp;
pub mod mqtt;
pub mod rpc;
@ -39,23 +39,6 @@ async fn main() {
let db = db::Database::build(config.clone()).await;
let kanidm = kanidm_client::KanidmClientBuilder::new()
.address(config.lock().idp().idm_url().to_owned())
.danger_accept_invalid_certs(cfg!(debug_assertions))
.connect_timeout(2)
.build()
.unwrap();
idp::accounts(&kanidm).await.unwrap();
idp::create_account_with_password(
&kanidm,
"eraden",
"Adrian Woźniak",
"adrian.wozniak@ita-prog.pl",
"n59GmOOdcpVUJqJ1",
)
.await
.unwrap();
let mqtt_client = mqtt::start(config.clone(), db.clone()).await;
rpc::start(config.clone(), db.clone(), mqtt_client.clone()).await;
}

View File

@ -14,5 +14,5 @@ serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
toml = { version = "0.7.3" }
tracing = { version = "0" }
traitcast = { workspace = true }
# traitcast = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

View File

@ -1,28 +1,44 @@
version: '3'
services:
kanidm-server:
image: kanidm/server:latest
volumes:
- kanidmd:/data
- ./config/kanidm.toml:/data/server.toml
- ./config/ca.pem:/data/ca.pem
- ./config/ca.key:/data/ca.key
ports:
- 636:3636
- 443:8443
- 8443:8443
- 8400:80
rumqqtd:
image: bytebeamio/rumqttd
ports:
- 1883:1883
- 1884:1884
rauthy:
image: ghcr.io/sebadob/rauthy:0.23.5-lite
rauthy-nginx:
image: nginx:latest
ports:
- 8080:8080
- 80:80
volumes:
# /etc/nginx/conf.d/*.conf;
- ./config/rauth.nginx:/etc/nginx/conf.d/rauth.conf:ro
depends_on:
- rauthy
rauthy-psql:
image: postgres:latest
environment:
POSTGRES_PASSWORD: 123SuperSafe
POSTGRES_USER: rauthy
POSTGRES_DB: rauthy
DATABASE_URL: postgresql://rauthy:123SuperSafe@localhost:5432/rauthy
volumes:
- rauthy-psql:/var/lib/postgresql/data
rauthy:
image: ghcr.io/sebadob/rauthy:0.23.5
depends_on:
- rauthy-psql
ports:
- 8302:8302
- 8301:9090
environment:
COOKIE_MODE: danger-insecure
SWAGGER_UI_EXTERNAL: true
volumes:
- rauthy:/app/data
- ./config/rauthy.cfg:/app/rauthy.cfg
quickwit:
image: quickwit/quickwit:v0.5.2
command: run
@ -30,9 +46,9 @@ services:
environment:
QW_ENABLE_OTLP_ENDPOINT: true
QW_ENABLE_JAEGER_ENDPOINT: true
ports:
- '7280:7280'
- '7281:7281'
# ports:
# - '7280:7280'
# - '7281:7281'
# volumes:
# - ./qwdata:/quickwit/qwdata
@ -44,8 +60,8 @@ services:
environment:
SPAN_STORAGE_TYPE: 'grpc-plugin'
GRPC_STORAGE_SERVER: 'quickwit:7281'
ports:
- '16686:16686'
# ports:
# - '16686:16686'
grafana:
image: grafana/grafana-enterprise:10.0.0
@ -59,5 +75,14 @@ services:
volumes:
- ./grafana/plugins:/var/lib/grafana/plugins
mailcrab:
image: 'marlonb/mailcrab:latest'
restart: unless-stopped
ports:
- 1125:1025
- 1180:1080
volumes:
kanidmd:
rauthy:
rauthy-psql: