Auth-middleware: JWT-validering og auth_identities-oppslag (oppgave 2.2)

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 <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-17 12:26:34 +01:00
parent 0d0fd03415
commit 854ed27797
4 changed files with 626 additions and 12 deletions

389
maskinrommet/Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

209
maskinrommet/src/auth.rs Normal file
View file

@ -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<JwkKey>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct JwkKey {
pub kid: Option<String>,
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<JwkKey>,
pub issuer: String,
}
impl JwksKeys {
/// Fetch JWKS from Authentik's OIDC discovery endpoint.
pub async fn fetch(issuer: &str) -> Result<Self, String> {
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<DecodingKey, String> {
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<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
AppState: FromRef<S>,
{
type Rejection = AuthErrorKind;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
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::<Claims>(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;

View file

@ -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<AppState>) -> Result<Json<HealthResponse>, StatusCode> {
sqlx::query("SELECT 1")
.execute(&state.db)
@ -63,3 +86,12 @@ async fn health(State(state): State<AppState>) -> Result<Json<HealthResponse>, S
db: "connected",
}))
}
/// Beskyttet endepunkt — returnerer autentisert brukers node_id.
/// Brukes for å verifisere at auth-middleware fungerer.
async fn me(user: AuthUser) -> Json<MeResponse> {
Json(MeResponse {
node_id: user.node_id,
authentik_sub: user.authentik_sub,
})
}