Skip to content

Commit 825d4bb

Browse files
committed
Implemented Authentication with JWT
1 parent 26f2a87 commit 825d4bb

5 files changed

Lines changed: 147 additions & 0 deletions

File tree

openlineplanner-backend/Cargo.lock

Lines changed: 89 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openlineplanner-backend/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ acc_reader = "2.0.0"
2929
uuid = { version = "1.3.0", features = ["v4", "serde", "v5"] }
3030
predicates = "3.0.3"
3131
postcard = { version = "1.0.4", features = ["alloc"] }
32+
actix-web-httpauth = "0.8.0"
33+
alcoholic_jwt = "4091.0.0"

openlineplanner-backend/src/main.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::sync::RwLock;
55

66
use actix_cors::Cors;
77
use actix_web::{http, web, App, HttpServer};
8+
use actix_web_httpauth::middleware::HttpAuthentication;
89
use anyhow::Result;
910
use config::Config;
1011
use error::OLPError;
@@ -14,6 +15,7 @@ use openhousepopulator::Buildings;
1415
use osmpbfreader::OsmPbfReader;
1516
use population::InhabitantsMap;
1617
use serde::Deserialize;
18+
use alcoholic_jwt::JWKS;
1719

1820
mod coverage;
1921
mod error;
@@ -22,6 +24,7 @@ mod layers;
2224
mod persistence;
2325
mod population;
2426
mod station;
27+
mod middleware;
2528

2629
use coverage::{CoverageMap, Method, Routing};
2730
use layers::streetgraph::generate_streetgraph;
@@ -49,6 +52,7 @@ async fn station_info(
4952
request: web::Json<StationInfoRequest>,
5053
layers: web::Data<RwLock<Layers>>,
5154
streets: web::Data<Streets>,
55+
auth: web::ReqData<middleware::auth::Claims>
5256
) -> Result<InhabitantsMap, OLPError> {
5357
let merged_layers = layers
5458
.read()
@@ -101,13 +105,17 @@ async fn main() -> std::io::Result<()> {
101105
let config = Config::builder()
102106
.set_default("cache.dir", "./cache/").unwrap()
103107
.set_default("data.dir", "./pbf/").unwrap()
108+
.set_default("oidc.issuer", "https://dex.prod.k8s.xatellite.space").unwrap()
104109
.add_source(config::File::with_name("Config.toml").required(false))
105110
.build()
106111
.unwrap();
107112

108113
let (streets, buildings) = load_base_data(&config);
109114
let layers = load_layers(&config);
110115
let config = web::Data::new(config);
116+
let jwks_data: JWKS = reqwest::get(format!("{}/keys", config.get_string("oidc.issuer").unwrap())).await.unwrap().json().await.unwrap();
117+
log::info!("loaded jwks: {:?}", jwks_data);
118+
let jwks = web::Data::new(jwks_data);
111119

112120
log::info!("loading data done");
113121

@@ -126,12 +134,16 @@ async fn main() -> std::io::Result<()> {
126134
.allowed_header(http::header::CONTENT_TYPE)
127135
.max_age(3600);
128136

137+
let authentication = HttpAuthentication::bearer(middleware::auth::validator);
138+
129139
App::new()
130140
.wrap(cors)
141+
.wrap(authentication)
131142
.app_data(layers.clone())
132143
.app_data(streets.clone())
133144
.app_data(buildings.clone())
134145
.app_data(config.clone())
146+
.app_data(jwks.clone())
135147
.route("/station-info", web::post().to(station_info))
136148
.route(
137149
"/coverage-info/{router}",
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use actix_web::HttpMessage;
2+
use actix_web::web::Data;
3+
use actix_web::dev::ServiceRequest;
4+
use actix_web_httpauth::extractors::bearer::{BearerAuth, Config};
5+
use actix_web_httpauth::extractors::AuthenticationError;
6+
use serde::{Deserialize, Serialize};
7+
use alcoholic_jwt::{JWKS, Validation, validate, token_kid};
8+
use anyhow::anyhow;
9+
10+
#[derive(Debug, Serialize, Deserialize, Clone)]
11+
pub struct Claims {
12+
pub sub: String,
13+
pub name: String
14+
}
15+
16+
pub async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
17+
let config = req.app_data::<Config>().cloned().unwrap_or_default();
18+
let jwks = req.app_data::<Data<JWKS>>().unwrap();
19+
match validate_token(credentials.token(), jwks) {
20+
Ok(claims) => {
21+
log::debug!("Validated token with claims {:?}", claims);
22+
req.extensions_mut().insert(claims);
23+
Ok(req)
24+
}
25+
Err(e) => {
26+
log::error!("Failed to validate token: {}", e);
27+
Err((AuthenticationError::from(config).into(), req))
28+
},
29+
}
30+
}
31+
32+
fn validate_token(token: &str, jwks: &JWKS) -> Result<Claims, anyhow::Error> {
33+
let validations = vec![
34+
Validation::Issuer("https://dex.prod.k8s.xatellite.space".into()),
35+
Validation::Audience("example-app".into()),
36+
Validation::NotExpired,
37+
Validation::SubjectPresent,
38+
];
39+
let kid = token_kid(&token)?.ok_or(anyhow!("KID is empty"))?;
40+
let jwk = jwks.find(&kid).ok_or(anyhow!("KID not found"))?;
41+
let decoded_token = validate(token, jwk, validations)?;
42+
Ok(serde_json::from_value(decoded_token.claims)?)
43+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod auth;

0 commit comments

Comments
 (0)