From 854ed2779750993275c41e5aa18ca3696c95af84 Mon Sep 17 00:00:00 2001 From: vegard Date: Tue, 17 Mar 2026 12:26:34 +0100 Subject: [PATCH] Auth-middleware: JWT-validering og auth_identities-oppslag (oppgave 2.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Legger til Authentik JWT-validering i maskinrommet: - Henter JWKS fra Authentik ved oppstart - Validerer RS256-signatur, issuer og utløpstid - Slår opp sub-claim i auth_identities → node_id - AuthUser axum-extractor for beskyttede endepunkter - /me test-endepunkt som krever gyldig token - /health forblir offentlig Co-Authored-By: Claude Opus 4.6 --- maskinrommet/Cargo.lock | 389 ++++++++++++++++++++++++++++++++++++++- maskinrommet/Cargo.toml | 2 + maskinrommet/src/auth.rs | 209 +++++++++++++++++++++ maskinrommet/src/main.rs | 38 +++- 4 files changed, 626 insertions(+), 12 deletions(-) create mode 100644 maskinrommet/src/auth.rs diff --git a/maskinrommet/Cargo.lock b/maskinrommet/Cargo.lock index 28255a7..b77a93c 100644 --- a/maskinrommet/Cargo.lock +++ b/maskinrommet/Cargo.lock @@ -169,6 +169,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -264,6 +270,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -460,8 +475,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -472,7 +503,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -606,6 +637,24 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.6", ] [[package]] @@ -614,13 +663,21 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -767,6 +824,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "1.0.17" @@ -783,6 +856,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -853,12 +941,20 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "maskinrommet" version = "0.1.0" dependencies = [ "axum", "chrono", + "jsonwebtoken", + "reqwest", "serde", "serde_json", "sqlx", @@ -926,6 +1022,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -937,11 +1043,17 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "num-integer" version = "0.1.46" @@ -1007,6 +1119,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1076,6 +1198,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1104,6 +1232,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -1113,6 +1296,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1126,8 +1315,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1137,7 +1336,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1149,6 +1358,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1184,6 +1402,44 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.6", +] + [[package]] name = "ring" version = "0.17.14" @@ -1211,13 +1467,19 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", "zeroize", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustls" version = "0.23.37" @@ -1238,6 +1500,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -1396,7 +1659,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", ] [[package]] @@ -1562,7 +1837,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "serde", "sha1", @@ -1602,7 +1877,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "sha2", @@ -1680,6 +1955,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -1721,6 +1999,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -1774,6 +2083,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -1809,9 +2128,12 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", + "futures-util", "http", "http-body", + "iri-string", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", @@ -1904,6 +2226,12 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.19.0" @@ -1997,6 +2325,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2040,6 +2377,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.114" @@ -2106,6 +2457,26 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.11" diff --git a/maskinrommet/Cargo.toml b/maskinrommet/Cargo.toml index 98996d5..6ed4612 100644 --- a/maskinrommet/Cargo.toml +++ b/maskinrommet/Cargo.toml @@ -14,3 +14,5 @@ chrono = { version = "0.4", features = ["serde"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } tower-http = { version = "0.6", features = ["cors", "trace"] } +jsonwebtoken = "9" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } diff --git a/maskinrommet/src/auth.rs b/maskinrommet/src/auth.rs new file mode 100644 index 0000000..8be4810 --- /dev/null +++ b/maskinrommet/src/auth.rs @@ -0,0 +1,209 @@ +use axum::{ + extract::FromRequestParts, + http::{request::Parts, StatusCode}, + response::{IntoResponse, Response}, + Json, +}; +use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::AppState; + +// --------------------------------------------------------------------------- +// JWKS types +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +pub struct JwksResponse { + pub keys: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct JwkKey { + pub kid: Option, + pub kty: String, + pub n: String, + pub e: String, +} + +/// Cached JWKS keys fetched from Authentik at startup. +#[derive(Debug, Clone)] +pub struct JwksKeys { + pub keys: Vec, + pub issuer: String, +} + +impl JwksKeys { + /// Fetch JWKS from Authentik's OIDC discovery endpoint. + pub async fn fetch(issuer: &str) -> Result { + let jwks_url = format!("{}jwks/", issuer.trim_end_matches('/').to_owned() + "/"); + tracing::info!("Henter JWKS fra {jwks_url}"); + + let resp = reqwest::get(&jwks_url) + .await + .map_err(|e| format!("Kunne ikke hente JWKS: {e}"))?; + + let jwks: JwksResponse = resp + .json() + .await + .map_err(|e| format!("Kunne ikke parse JWKS: {e}"))?; + + if jwks.keys.is_empty() { + return Err("JWKS inneholder ingen nøkler".to_string()); + } + + tracing::info!("Lastet {} JWKS-nøkler", jwks.keys.len()); + + Ok(Self { + keys: jwks.keys, + issuer: issuer.to_string(), + }) + } + + /// Find decoding key by kid, or use the first key if no kid in header. + fn decoding_key(&self, kid: Option<&str>) -> Result { + let key = match kid { + Some(kid) => self + .keys + .iter() + .find(|k| k.kid.as_deref() == Some(kid)) + .ok_or_else(|| format!("Ukjent kid: {kid}"))?, + None => self.keys.first().ok_or("Ingen JWKS-nøkler tilgjengelig")?, + }; + + DecodingKey::from_rsa_components(&key.n, &key.e) + .map_err(|e| format!("Ugyldig RSA-nøkkel: {e}")) + } +} + +// --------------------------------------------------------------------------- +// JWT claims +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +struct Claims { + sub: String, + // iss and exp are validated by jsonwebtoken automatically +} + +// --------------------------------------------------------------------------- +// AuthUser extractor +// --------------------------------------------------------------------------- + +/// Authenticated user extracted from a valid JWT. +/// Use as an axum extractor on protected endpoints. +#[derive(Debug, Clone)] +pub struct AuthUser { + pub node_id: Uuid, + pub authentik_sub: String, +} + +/// Error response for auth failures. +#[derive(Serialize)] +struct AuthError { + error: String, +} + +impl IntoResponse for AuthErrorKind { + fn into_response(self) -> Response { + let (status, message) = match self { + AuthErrorKind::MissingToken => (StatusCode::UNAUTHORIZED, "Mangler Authorization-header"), + AuthErrorKind::InvalidToken(ref _e) => (StatusCode::UNAUTHORIZED, "Ugyldig token"), + AuthErrorKind::UnknownIdentity => { + (StatusCode::UNAUTHORIZED, "Ukjent brukeridentitet") + } + AuthErrorKind::Internal => { + (StatusCode::INTERNAL_SERVER_ERROR, "Intern feil ved autentisering") + } + }; + + let body = Json(AuthError { + error: message.to_string(), + }); + + (status, body).into_response() + } +} + +#[derive(Debug)] +pub enum AuthErrorKind { + MissingToken, + InvalidToken(String), + UnknownIdentity, + Internal, +} + +impl FromRequestParts for AuthUser +where + S: Send + Sync, + AppState: FromRef, +{ + type Rejection = AuthErrorKind; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let app_state = AppState::from_ref(state); + + // Extract Bearer token from Authorization header + let auth_header = parts + .headers + .get("authorization") + .and_then(|v| v.to_str().ok()) + .ok_or(AuthErrorKind::MissingToken)?; + + let token = auth_header + .strip_prefix("Bearer ") + .ok_or(AuthErrorKind::MissingToken)?; + + // Decode header to get kid + let header = decode_header(token) + .map_err(|e| AuthErrorKind::InvalidToken(e.to_string()))?; + + // Get decoding key from JWKS + let decoding_key = app_state + .jwks + .decoding_key(header.kid.as_deref()) + .map_err(|e| { + tracing::warn!("JWKS-nøkkel ikke funnet: {e}"); + AuthErrorKind::InvalidToken(e) + })?; + + // Validate JWT (signature, exp, iss) + let mut validation = Validation::new(Algorithm::RS256); + validation.set_issuer(&[&app_state.jwks.issuer]); + + let token_data = decode::(token, &decoding_key, &validation).map_err(|e| { + tracing::debug!("JWT-validering feilet: {e}"); + AuthErrorKind::InvalidToken(e.to_string()) + })?; + + let authentik_sub = token_data.claims.sub; + + // Look up auth_identities → node_id + let row = sqlx::query_scalar::<_, Uuid>( + "SELECT node_id FROM auth_identities WHERE authentik_sub = $1", + ) + .bind(&authentik_sub) + .fetch_optional(&app_state.db) + .await + .map_err(|e| { + tracing::error!("DB-feil ved oppslag av auth_identities: {e}"); + AuthErrorKind::Internal + })?; + + let node_id = row.ok_or_else(|| { + tracing::warn!("Ingen auth_identity for sub={authentik_sub}"); + AuthErrorKind::UnknownIdentity + })?; + + tracing::debug!("Autentisert bruker: node_id={node_id}, sub={authentik_sub}"); + + Ok(AuthUser { + node_id, + authentik_sub, + }) + } +} + +// We need FromRef to extract AppState from the state +use axum::extract::FromRef; diff --git a/maskinrommet/src/main.rs b/maskinrommet/src/main.rs index d614ac2..24c629d 100644 --- a/maskinrommet/src/main.rs +++ b/maskinrommet/src/main.rs @@ -1,3 +1,5 @@ +mod auth; + use axum::{extract::State, http::StatusCode, routing::get, Json, Router}; use serde::Serialize; use sqlx::postgres::PgPoolOptions; @@ -5,9 +7,12 @@ use sqlx::PgPool; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use auth::{AuthUser, JwksKeys}; + #[derive(Clone)] -struct AppState { - db: PgPool, +pub struct AppState { + pub db: PgPool, + pub jwks: JwksKeys, } #[derive(Serialize)] @@ -17,6 +22,12 @@ struct HealthResponse { db: &'static str, } +#[derive(Serialize)] +struct MeResponse { + node_id: uuid::Uuid, + authentik_sub: String, +} + #[tokio::main] async fn main() { tracing_subscriber::registry() @@ -27,6 +38,7 @@ async fn main() { .with(tracing_subscriber::fmt::layer()) .init(); + // Database let database_url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://sidelinja:sidelinja@localhost:5432/synops".to_string()); @@ -38,10 +50,20 @@ async fn main() { tracing::info!("Koblet til PostgreSQL"); - let state = AppState { db }; + // JWKS — hent nøkler fra Authentik ved oppstart + let issuer = std::env::var("AUTHENTIK_ISSUER") + .unwrap_or_else(|_| "https://auth.sidelinja.org/application/o/sidelinja/".to_string()); + let jwks = JwksKeys::fetch(&issuer) + .await + .expect("Kunne ikke hente JWKS fra Authentik"); + + let state = AppState { db, jwks }; + + // Ruter: /health er offentlig, /me krever gyldig JWT let app = Router::new() .route("/health", get(health)) + .route("/me", get(me)) .layer(TraceLayer::new_for_http()) .with_state(state); @@ -51,6 +73,7 @@ async fn main() { axum::serve(listener, app).await.unwrap(); } +/// Offentlig helsesjekk — ingen auth påkrevd. async fn health(State(state): State) -> Result, StatusCode> { sqlx::query("SELECT 1") .execute(&state.db) @@ -63,3 +86,12 @@ async fn health(State(state): State) -> Result, S db: "connected", })) } + +/// Beskyttet endepunkt — returnerer autentisert brukers node_id. +/// Brukes for å verifisere at auth-middleware fungerer. +async fn me(user: AuthUser) -> Json { + Json(MeResponse { + node_id: user.node_id, + authentik_sub: user.authentik_sub, + }) +}