From bcb14b3c4832fb8fd4c11705a5ef248a04282422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Wed, 26 Jun 2024 16:34:00 +0200 Subject: [PATCH] IDP with rauthy --- Cargo.lock | 74 +----------- crates/cache-adapter/src/lib.rs | 4 +- crates/channels/src/accounts.rs | 24 ++++ crates/config/Cargo.toml | 1 + crates/config/src/lib.rs | 8 ++ crates/event-bus-redis-plugin/src/lib.rs | 2 +- crates/file-storage-s3-plugin/src/lib.rs | 2 +- crates/idp/Cargo.toml | 2 +- crates/idp/src/actions.rs | 144 ++++++++++++++++++++++- crates/idp/src/main.rs | 19 +-- crates/payment-adapter/Cargo.toml | 2 +- docker-compose.yml | 65 ++++++---- 12 files changed, 228 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 163882a..5d929f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/crates/cache-adapter/src/lib.rs b/crates/cache-adapter/src/lib.rs index 3696a56..34e9df3 100644 --- a/crates/cache-adapter/src/lib.rs +++ b/crates/cache-adapter/src/lib.rs @@ -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}"); diff --git a/crates/channels/src/accounts.rs b/crates/channels/src/accounts.rs index d096673..772f5a5 100644 --- a/crates/channels/src/accounts.rs +++ b/crates/channels/src/accounts.rs @@ -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; } +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; +} + impl AsyncClient { pub async fn emit_account_created(&self, account: &model::FullAccount) { self.publish_or_log(Topic::AccountCreated, QoS::AtLeastOnce, true, account) diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 014432c..b4d6a56 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -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 } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 7f42f38..e8daa44 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -357,10 +357,12 @@ pub struct IdpConfig { pub idm_url: String, #[serde(default)] pub secret: Option, + pub enc_key: Vec, } 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::() + .into_bytes(), } } } diff --git a/crates/event-bus-redis-plugin/src/lib.rs b/crates/event-bus-redis-plugin/src/lib.rs index d61696b..a1af36b 100644 --- a/crates/event-bus-redis-plugin/src/lib.rs +++ b/crates/event-bus-redis-plugin/src/lib.rs @@ -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)); } diff --git a/crates/file-storage-s3-plugin/src/lib.rs b/crates/file-storage-s3-plugin/src/lib.rs index abbb648..37b6385 100644 --- a/crates/file-storage-s3-plugin/src/lib.rs +++ b/crates/file-storage-s3-plugin/src/lib.rs @@ -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}; diff --git a/crates/idp/Cargo.toml b/crates/idp/Cargo.toml index 1822532..d83479a 100644 --- a/crates/idp/Cargo.toml +++ b/crates/idp/Cargo.toml @@ -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"] } diff --git a/crates/idp/src/actions.rs b/crates/idp/src/actions.rs index 61026e0..703d9eb 100644 --- a/crates/idp/src/actions.rs +++ b/crates/idp/src/actions.rs @@ -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 { + // 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, +) -> 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 { + let principal = extractors::principal_from_req(&req).await?; + + Ok(HttpResponse::Ok().body(format!( + "Hello from Protected Resource:
{:?}", + principal + ))) +} +*/ diff --git a/crates/idp/src/main.rs b/crates/idp/src/main.rs index 68454f8..1ba94c0 100644 --- a/crates/idp/src/main.rs +++ b/crates/idp/src/main.rs @@ -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; } diff --git a/crates/payment-adapter/Cargo.toml b/crates/payment-adapter/Cargo.toml index bfa8125..5518275 100644 --- a/crates/payment-adapter/Cargo.toml +++ b/crates/payment-adapter/Cargo.toml @@ -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"] } diff --git a/docker-compose.yml b/docker-compose.yml index e064102..d52e76f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: