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
.idea/
.idea/
.env
.DS_Store

1
Cargo.lock generated
View File

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

View File

@ -18,3 +18,4 @@ toml = "0.9.7"
oauth2 = "5.0.0"
uuid = "1.18.1"
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::middleware::AppState;
use crate::util::set_and_log_session;
use crate::util::{decode_payload_unchecked, set_and_log_session};
use axum::Extension;
use axum::body::Body;
use axum::extract::Request;
@ -8,6 +8,7 @@ use axum::http::{HeaderValue, StatusCode};
use axum::response::IntoResponse;
use log::{error, info};
use std::sync::Arc;
use crate::model::{User};
pub async fn handle_post(
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::middleware::AppState;
use crate::repository::RedisRepository;
use axum::http::{StatusCode, header};
use axum::response::Response;
use axum::response::{Html, Response};
use axum::{
Router,
extract::{Query, State},
@ -22,6 +23,7 @@ use oauth2::{
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use axum::routing::post;
use log::info;
pub type OAuthClient = Client<
BasicErrorResponse,
@ -188,23 +190,13 @@ async fn callback(
return (StatusCode::INTERNAL_SERVER_ERROR, "Login failed").into_response();
}
log::info!("Successfully created session {} for user", session_id);
let cookie = format!(
"session_id={}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400",
session_id
);
info!("Successfully created session {} for user", session_id);
info!("Token scopes: {:?}", token.extra_fields());
// 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()
.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()
Redirect::to(redirect_url.as_str()).into_response()
}
Err(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::middleware::AppState;
use crate::repository::RedisRepository;
use crate::util::initialize_logging;
use axum::routing::post;
use axum::routing::{get, post};
use axum::{Extension, Router};
use axum_keycloak_auth::instance::{KeycloakAuthInstance, KeycloakConfig};
use axum_keycloak_auth::layer::KeycloakAuthLayer;
@ -18,12 +18,14 @@ mod gsd;
mod middleware;
mod repository;
mod util;
mod model;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = load_config()?;
initialize_logging(&config);
info!("Redirect URI: {}", config.keycloak.redirect_url);
info!("Logging initialized");
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 proxy_router = Router::new()
.route("/{*wildcard}", post(handle_post))
.route("/userinfo", get(userinfo))
.route_layer(Extension(state.clone()))
.route_layer(axum::middleware::from_fn_with_state(
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(),
);
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 std::fs::File;
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) {
CombinedLogger::init(vec![