Implemented userinfo endpoint and finalized OAuth2.0 flow
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@ -26,3 +26,7 @@ target/
|
|||||||
config.toml
|
config.toml
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -353,6 +353,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"axum-keycloak-auth",
|
"axum-keycloak-auth",
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"log",
|
"log",
|
||||||
"oauth2",
|
"oauth2",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
10
src/api.rs
10
src/api.rs
@ -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()
|
||||||
|
}
|
||||||
|
|||||||
22
src/auth.rs
22
src/auth.rs
@ -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);
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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
13
src/model.rs
Normal 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
|
||||||
|
}
|
||||||
12
src/util.rs
12
src/util.rs
@ -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![
|
||||||
|
|||||||
Reference in New Issue
Block a user