Implemented userinfo endpoint and finalized OAuth2.0 flow

This commit is contained in:
Dennis Nemec
2025-10-05 00:54:21 +02:00
parent b87d7e0268
commit 98fb621108
9 changed files with 58 additions and 21 deletions

6
.gitignore vendored
View File

@ -25,4 +25,8 @@ target/
config.toml config.toml
.idea/ .idea/
.env
.DS_Store

1
Cargo.lock generated
View File

@ -353,6 +353,7 @@ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
"axum-keycloak-auth", "axum-keycloak-auth",
"base64",
"chrono", "chrono",
"log", "log",
"oauth2", "oauth2",

View File

@ -18,3 +18,4 @@ toml = "0.9.7"
oauth2 = "5.0.0" oauth2 = "5.0.0"
uuid = "1.18.1" uuid = "1.18.1"
axum-extra = { version = "0.10.3", features = ["cookie"] } axum-extra = { version = "0.10.3", features = ["cookie"] }
base64 = "0.22.1"

View File

@ -1,6 +1,6 @@
use crate::gsd::dto::GSDResponseDTO; use crate::gsd::dto::GSDResponseDTO;
use crate::middleware::AppState; use crate::middleware::AppState;
use crate::util::set_and_log_session; use crate::util::{decode_payload_unchecked, set_and_log_session};
use axum::Extension; use axum::Extension;
use axum::body::Body; use axum::body::Body;
use axum::extract::Request; use axum::extract::Request;
@ -8,6 +8,7 @@ use axum::http::{HeaderValue, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use log::{error, info}; use log::{error, info};
use std::sync::Arc; use std::sync::Arc;
use crate::model::{User};
pub async fn handle_post( pub async fn handle_post(
Extension(state): Extension<Arc<AppState>>, Extension(state): Extension<Arc<AppState>>,
@ -94,4 +95,9 @@ pub async fn handle_post(
} }
} }
pub async fn handle_login() -> impl IntoResponse {} pub async fn userinfo(request: Request<Body>) -> impl IntoResponse {
let access_token_string = &request.headers().get("authorization").unwrap().to_str().unwrap().to_string()[7..];
let user = decode_payload_unchecked::<User>(access_token_string).unwrap();
serde_json::to_string(&user.employee).unwrap().into_response()
}

View File

@ -1,8 +1,9 @@
use std::collections::HashMap;
use crate::config::Config; use crate::config::Config;
use crate::middleware::AppState; use crate::middleware::AppState;
use crate::repository::RedisRepository; use crate::repository::RedisRepository;
use axum::http::{StatusCode, header}; use axum::http::{StatusCode, header};
use axum::response::Response; use axum::response::{Html, Response};
use axum::{ use axum::{
Router, Router,
extract::{Query, State}, extract::{Query, State},
@ -22,6 +23,7 @@ use oauth2::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use axum::routing::post; use axum::routing::post;
use log::info;
pub type OAuthClient = Client< pub type OAuthClient = Client<
BasicErrorResponse, BasicErrorResponse,
@ -188,23 +190,13 @@ async fn callback(
return (StatusCode::INTERNAL_SERVER_ERROR, "Login failed").into_response(); return (StatusCode::INTERNAL_SERVER_ERROR, "Login failed").into_response();
} }
log::info!("Successfully created session {} for user", session_id); info!("Successfully created session {} for user", session_id);
info!("Token scopes: {:?}", token.extra_fields());
let cookie = format!(
"session_id={}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400",
session_id
);
// 4. Redirect to frontend // 4. Redirect to frontend
let redirect_url = format!("{}?login=success", cloned_state.frontend_url); let redirect_url = format!("{}/?session_id={}", cloned_state.frontend_url.clone(), session_id);
Response::builder() Redirect::to(redirect_url.as_str()).into_response()
.status(StatusCode::FOUND)
.header(header::SET_COOKIE, cookie)
.header(header::LOCATION, redirect_url.clone())
.body::<String>(format!("Redirecting to {}", redirect_url).into())
.unwrap()
.into_response()
} }
Err(e) => { Err(e) => {
log::error!("Token exchange failed: {:?}", e); log::error!("Token exchange failed: {:?}", e);

View File

@ -1,9 +1,9 @@
use crate::api::handle_post; use crate::api::{handle_post, userinfo};
use crate::config::load_config; use crate::config::load_config;
use crate::middleware::AppState; use crate::middleware::AppState;
use crate::repository::RedisRepository; use crate::repository::RedisRepository;
use crate::util::initialize_logging; use crate::util::initialize_logging;
use axum::routing::post; use axum::routing::{get, post};
use axum::{Extension, Router}; use axum::{Extension, Router};
use axum_keycloak_auth::instance::{KeycloakAuthInstance, KeycloakConfig}; use axum_keycloak_auth::instance::{KeycloakAuthInstance, KeycloakConfig};
use axum_keycloak_auth::layer::KeycloakAuthLayer; use axum_keycloak_auth::layer::KeycloakAuthLayer;
@ -18,12 +18,14 @@ mod gsd;
mod middleware; mod middleware;
mod repository; mod repository;
mod util; mod util;
mod model;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = load_config()?; let config = load_config()?;
initialize_logging(&config); initialize_logging(&config);
info!("Redirect URI: {}", config.keycloak.redirect_url);
info!("Logging initialized"); info!("Logging initialized");
info!("Starting Holzleitner Delivery Backend"); info!("Starting Holzleitner Delivery Backend");
@ -51,6 +53,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let auth_router = auth::router(state.clone()); let auth_router = auth::router(state.clone());
let proxy_router = Router::new() let proxy_router = Router::new()
.route("/{*wildcard}", post(handle_post)) .route("/{*wildcard}", post(handle_post))
.route("/userinfo", get(userinfo))
.route_layer(Extension(state.clone())) .route_layer(Extension(state.clone()))
.route_layer(axum::middleware::from_fn_with_state( .route_layer(axum::middleware::from_fn_with_state(
state.clone(), state.clone(),

View File

@ -172,5 +172,10 @@ pub async fn gsd_decorate_header(
HeaderValue::from_str(state_cloned.config.gsd_app_key.as_str()).unwrap(), HeaderValue::from_str(state_cloned.config.gsd_app_key.as_str()).unwrap(),
); );
next.run(request).await let response = next.run(request).await;
info!("Response: {:?}", response);
response
} }

13
src/model.rs Normal file
View File

@ -0,0 +1,13 @@
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct User {
pub employee: Employee,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Employee {
pub number: String,
pub last_name: String,
pub first_name: String,
pub mail: String
}

View File

@ -6,6 +6,18 @@ use log::{LevelFilter, error, info};
use simplelog::{ColorChoice, CombinedLogger, TermLogger, TerminalMode, WriteLogger}; use simplelog::{ColorChoice, CombinedLogger, TermLogger, TerminalMode, WriteLogger};
use std::fs::File; use std::fs::File;
use std::sync::Arc; use std::sync::Arc;
use serde::de::DeserializeOwned;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
pub fn decode_payload_unchecked<T: DeserializeOwned>(token: &str) -> Result<T, Box<dyn std::error::Error>> {
let mut parts = token.split('.');
let _header = parts.next().ok_or("missing header")?;
let payload_b64 = parts.next().ok_or("missing payload")?;
// signature is parts.next() but we ignore it here
let payload = URL_SAFE_NO_PAD.decode(payload_b64.as_bytes())?;
let claims = serde_json::from_slice::<T>(&payload)?;
Ok(claims)
}
pub fn initialize_logging(config: &Config) { pub fn initialize_logging(config: &Config) {
CombinedLogger::init(vec![ CombinedLogger::init(vec![