feat: oidc login (#5)
Co-authored-by: Nico Fricke <nfricke@eos-uptrade.de> Reviewed-on: #5 Co-authored-by: Nico Fricke <nico@nifni.net> Co-committed-by: Nico Fricke <nico@nifni.net>
This commit is contained in:
parent
edf6e84a9a
commit
6bbe4ea6ae
1130
Cargo.lock
generated
1130
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,8 @@ repository = "https://git.nifni.eu/nif/casket"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8.1", features = ["multipart", "macros"] }
|
||||
axum-extra = { version = "0.10.0", features = ["typed-header"] }
|
||||
axum-jwks = "0.11.0"
|
||||
tokio = { version = "1.42.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7.13", features = ["io"] }
|
||||
futures = "0.3"
|
||||
|
||||
57
casket-backend/src/auth.rs
Normal file
57
casket-backend/src/auth.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use crate::AppState;
|
||||
use axum::extract::{FromRef, Path, Request, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::middleware::Next;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum_extra::headers::authorization::Bearer;
|
||||
use axum_extra::headers::Authorization;
|
||||
use axum_extra::TypedHeader;
|
||||
use axum_jwks::Jwks;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for Jwks {
|
||||
fn from_ref(state: &AppState) -> Self {
|
||||
state.jwks.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn validate_token(
|
||||
State(state): State<AppState>,
|
||||
user: Option<Path<String>>,
|
||||
auth_header: Option<TypedHeader<Authorization<Bearer>>>,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let jwks = Jwks::from_ref(&state);
|
||||
if request.uri().path().starts_with("/api/v1/user/") {
|
||||
if auth_header.is_none() {
|
||||
debug!("No auth header passed.");
|
||||
return StatusCode::UNAUTHORIZED.into_response();
|
||||
}
|
||||
match jwks.validate_claims::<Claims>(auth_header.unwrap().0 .0.token()) {
|
||||
Err(err) => {
|
||||
debug!("JWT validation failed: {:?}", err);
|
||||
return StatusCode::UNAUTHORIZED.into_response();
|
||||
}
|
||||
Ok(jwk) => match user {
|
||||
Some(user) => {
|
||||
if !user.0.eq(&jwk.claims.sub) {
|
||||
debug!("JWT sub {:?} does not match user {:?}", jwk.claims.sub, user.0);
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
panic!("User was none")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
next.run(request).await
|
||||
}
|
||||
@ -9,6 +9,7 @@ pub const CONFIG_LOCATIONS: [&str; 2] = ["casket-backend/casket.toml", "/config/
|
||||
pub struct Config {
|
||||
pub server: Server,
|
||||
pub files: Files,
|
||||
pub oidc: Oidc
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
@ -21,6 +22,11 @@ pub struct Files {
|
||||
pub directory: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Oidc {
|
||||
pub oidc_endpoint: String
|
||||
}
|
||||
|
||||
pub fn get_config() -> figment::Result<Config> {
|
||||
CONFIG_LOCATIONS
|
||||
.iter()
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
#![warn(clippy::pedantic)]
|
||||
|
||||
mod auth;
|
||||
mod config;
|
||||
mod errors;
|
||||
mod extractor_helper;
|
||||
mod files;
|
||||
mod routes;
|
||||
mod extractor_helper;
|
||||
|
||||
use axum::Router;
|
||||
use axum::{middleware, Router};
|
||||
use axum_jwks::Jwks;
|
||||
use std::env;
|
||||
use std::process::exit;
|
||||
use std::str::FromStr;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
config: config::Config,
|
||||
pub jwks: Jwks,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@ -26,10 +29,24 @@ async fn main() {
|
||||
match config {
|
||||
Ok(config) => {
|
||||
debug!("Config loaded {:?}", &config,);
|
||||
let jwks = Jwks::from_oidc_url(
|
||||
// The Authorization Server that signs the JWTs you want to consume.
|
||||
&config.oidc.oidc_endpoint,
|
||||
// The audience identifier for the application. This ensures that
|
||||
// JWTs are intended for this application.
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bind = format!("{}:{}", &config.server.bind_address, &config.server.port);
|
||||
let state = AppState { config, jwks };
|
||||
let app = Router::new()
|
||||
.merge(routes::routes())
|
||||
.with_state(AppState { config });
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
auth::validate_token,
|
||||
))
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(bind).await.unwrap();
|
||||
info!("listening on {}", listener.local_addr().unwrap());
|
||||
|
||||
29
casket-bruno-collection/Auth.bru
Normal file
29
casket-bruno-collection/Auth.bru
Normal file
@ -0,0 +1,29 @@
|
||||
meta {
|
||||
name: Auth
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
url:
|
||||
body: none
|
||||
auth: oauth2
|
||||
}
|
||||
|
||||
auth:oauth2 {
|
||||
grant_type: authorization_code
|
||||
callback_url: https://localhost:1234
|
||||
authorization_url: {{keycloak_host}}/realms/{{keycloak_realm}}/protocol/openid-connect/auth
|
||||
access_token_url: {{keycloak_host}}/realms/{{keycloak_realm}}/protocol/openid-connect/token
|
||||
client_id: casket
|
||||
client_secret:
|
||||
scope:
|
||||
state:
|
||||
pkce: false
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setVar('access_token', res.body.access_token);
|
||||
const atob = require('atob');
|
||||
bru.setVar('user_id',JSON.parse(atob(res.body.access_token.split('.')[1])).sub)
|
||||
}
|
||||
@ -5,11 +5,15 @@ meta {
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/user/test123/download?path=test/blub.txt
|
||||
url: {{casket_host}}/api/v1/user/{{user_id}}/download?path=test/blub.txt
|
||||
body: none
|
||||
auth: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
params:query {
|
||||
path: test/blub.txt
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{access_token}}
|
||||
}
|
||||
|
||||
@ -5,12 +5,16 @@ meta {
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/user/test123/files?path=&nesting=5
|
||||
url: {{casket_host}}/api/v1/user/{{user_id}}/files?path=&nesting=5
|
||||
body: none
|
||||
auth: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
params:query {
|
||||
path:
|
||||
nesting: 5
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{access_token}}
|
||||
}
|
||||
|
||||
15
casket-bruno-collection/Invalid.bru
Normal file
15
casket-bruno-collection/Invalid.bru
Normal file
@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: Invalid
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{casket_host}}/api/v1/user//files
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{access_token}}
|
||||
}
|
||||
@ -5,9 +5,13 @@ meta {
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/user/test123/files
|
||||
url: {{casket_host}}/api/v1/user/{{user_id}}/files
|
||||
body: multipartForm
|
||||
auth: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{access_token}}
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user