This commit is contained in:
despiegk 2025-05-08 08:05:19 +03:00
parent 4274fdaf93
commit 433606eb25
12 changed files with 207 additions and 147 deletions

View File

@ -2,16 +2,6 @@
This guide provides detailed instructions on how to integrate Gitea authentication into your Hostbasket application.
## Table of Contents
1. [Introduction](#introduction)
2. [Prerequisites](#prerequisites)
3. [Setting Up Gitea OAuth Application](#setting-up-gitea-oauth-application)
4. [Configuring Hostbasket for Gitea Authentication](#configuring-hostbasket-for-gitea-authentication)
5. [Implementing the OAuth Flow](#implementing-the-oauth-flow)
6. [Testing the Integration](#testing-the-integration)
7. [Troubleshooting](#troubleshooting)
## Introduction
Gitea is a self-hosted Git service that provides a GitHub-like interface. By integrating Gitea authentication into your Hostbasket application, you can allow users to log in using their Gitea accounts, simplifying the authentication process and providing a seamless experience.
@ -27,7 +17,7 @@ Before you begin, ensure you have:
## Setting Up Gitea OAuth Application
1. Log in to your Gitea instance as an administrator
2. Navigate to **Site Administration** > **Applications**
2. Navigate to **Site Administration** > **Integrations** > **Applications**
3. Click **Create a New OAuth2 Application**
4. Fill in the application details:
- **Application Name**: Hostbasket
@ -96,11 +86,11 @@ impl GiteaOAuthConfig {
.expect("Missing GITEA_CLIENT_SECRET environment variable");
let instance_url = env::var("GITEA_INSTANCE_URL")
.expect("Missing GITEA_INSTANCE_URL environment variable");
// Create OAuth client
let auth_url = format!("{}/login/oauth/authorize", instance_url);
let token_url = format!("{}/login/oauth/access_token", instance_url);
let client = BasicClient::new(
ClientId::new(client_id),
Some(ClientSecret::new(client_secret)),
@ -110,7 +100,7 @@ impl GiteaOAuthConfig {
.set_redirect_uri(
RedirectUrl::new("http://localhost:9999/auth/gitea/callback".to_string()).unwrap(),
);
Self {
client,
instance_url,
@ -158,16 +148,16 @@ impl GiteaAuthController {
.add_scope(Scope::new("read:user".to_string()))
.add_scope(Scope::new("user:email".to_string()))
.url();
// Store the CSRF token in the session
session.insert("oauth_csrf_token", csrf_token.secret())?;
// Redirect to the authorization URL
Ok(HttpResponse::Found()
.append_header((header::LOCATION, auth_url.to_string()))
.finish())
}
// Handle the OAuth callback
pub async fn callback(
oauth_config: web::Data<GiteaOAuthConfig>,
@ -177,11 +167,11 @@ impl GiteaAuthController {
// Verify the CSRF token
let csrf_token = session.get::<String>("oauth_csrf_token")?
.ok_or_else(|| actix_web::error::ErrorBadRequest("Missing CSRF token"))?;
if csrf_token != query.state {
return Err(actix_web::error::ErrorBadRequest("Invalid CSRF token"));
}
// Exchange the authorization code for an access token
let token = oauth_config
.client
@ -189,11 +179,11 @@ impl GiteaAuthController {
.request_async(oauth2::reqwest::async_http_client)
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("Token exchange error: {}", e)))?;
// Get the user information from Gitea
let client = Client::new();
let user_info_url = format!("{}/api/v1/user", oauth_config.instance_url);
let gitea_user = client
.get(&user_info_url)
.bearer_auth(token.access_token().secret())
@ -203,26 +193,26 @@ impl GiteaAuthController {
.json::<crate::config::oauth::GiteaUser>()
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("JSON parsing error: {}", e)))?;
// Create or update the user in your system
let mut user = User::new(
gitea_user.full_name.clone(),
gitea_user.email.clone(),
);
// Set the user ID and role
user.id = Some(gitea_user.id as i32);
user.role = UserRole::User;
// Generate JWT token
let token = AuthController::generate_token(&user.email, &user.role)
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?;
// Store user data in session
let user_json = serde_json::to_string(&user).unwrap();
session.insert("user", &user_json)?;
session.insert("auth_token", &token)?;
// Create a cookie with the JWT token
let cookie = Cookie::build("auth_token", token)
.path("/")
@ -230,7 +220,7 @@ impl GiteaAuthController {
.secure(false) // Set to true in production with HTTPS
.max_age(actix_web::cookie::time::Duration::hours(24))
.finish();
// Redirect to the home page with JWT token in cookie
Ok(HttpResponse::Found()
.cookie(cookie)
@ -258,7 +248,7 @@ use crate::config::oauth::GiteaOAuthConfig;
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
// Create the OAuth configuration
let oauth_config = web::Data::new(GiteaOAuthConfig::new());
// Configure session middleware with the consistent key
let session_middleware = SessionMiddleware::builder(
CookieSessionStore::default(),
@ -273,7 +263,7 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.wrap(session_middleware)
.app_data(oauth_config.clone())
// Existing routes...
// Gitea OAuth routes
.route("/auth/gitea", web::get().to(GiteaAuthController::login))
.route("/auth/gitea/callback", web::get().to(GiteaAuthController::callback))
@ -310,9 +300,9 @@ Update your login page template (`src/views/auth/login.html`) to include a "Logi
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<hr>
<div class="text-center">
<p>Or login with:</p>
<a href="/auth/gitea" class="btn btn-secondary">

View File

@ -1,12 +1,8 @@
#[cfg(feature = "gitea")]
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use serde::{Deserialize, Serialize};
use std::env;
#[cfg(feature = "gitea")]
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
/// Gitea OAuth configuration
#[cfg(feature = "gitea")]
#[derive(Clone, Debug)]
pub struct GiteaOAuthConfig {
/// OAuth client
@ -15,7 +11,6 @@ pub struct GiteaOAuthConfig {
pub instance_url: String,
}
#[cfg(feature = "gitea")]
impl GiteaOAuthConfig {
/// Creates a new Gitea OAuth configuration
pub fn new() -> Self {
@ -53,7 +48,6 @@ impl GiteaOAuthConfig {
}
/// Gitea user information structure
#[cfg(feature = "gitea")]
#[derive(Debug, Deserialize, Serialize)]
pub struct GiteaUser {
/// User ID

View File

@ -1,12 +1,13 @@
use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cookie};
use actix_session::Session;
use tera::Tera;
use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole};
use crate::models::user::{LoginCredentials, RegistrationData, User, UserRole};
use crate::utils::render_template;
use jsonwebtoken::{encode, Header, EncodingKey};
use serde::{Deserialize, Serialize};
use chrono::{Utc, Duration};
use actix_session::Session;
use actix_web::{cookie::Cookie, http::header, web, HttpResponse, Responder, Result};
use chrono::{Duration, Utc};
use jsonwebtoken::{encode, EncodingKey, Header};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::env;
use tera::Tera;
// JWT Claims structure
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -19,7 +20,8 @@ pub struct Claims {
// JWT Secret key
lazy_static! {
static ref JWT_SECRET: String = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your_jwt_secret_key".to_string());
static ref JWT_SECRET: String =
std::env::var("JWT_SECRET").unwrap_or_else(|_| "your_jwt_secret_key".to_string());
}
/// Controller for handling authentication-related routes
@ -27,7 +29,10 @@ pub struct AuthController;
impl AuthController {
/// Generate a JWT token for a user
pub fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> {
pub fn generate_token(
email: &str,
role: &UserRole,
) -> Result<String, jsonwebtoken::errors::Error> {
let role_str = match role {
UserRole::Admin => "admin",
UserRole::User => "user",
@ -48,7 +53,7 @@ impl AuthController {
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(JWT_SECRET.as_bytes())
&EncodingKey::from_secret(JWT_SECRET.as_bytes()),
)
}
@ -61,8 +66,12 @@ impl AuthController {
/// Renders the login page
pub async fn login_page(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
.ok()
.filter(|s| !s.is_empty())
.is_some();
ctx.insert("gitea_enabled", &is_gitea_flow_active);
ctx.insert("active_page", "login");
// Add user to context if available
if let Ok(Some(user_json)) = session.get::<String>("user") {
// Keep the raw JSON for backward compatibility
@ -73,7 +82,7 @@ impl AuthController {
Ok(user) => {
log::info!("Successfully parsed user in login_page: {:?}", user);
ctx.insert("user", &user);
},
}
Err(e) => {
log::error!("Failed to parse user JSON in login_page: {}", e);
log::error!("User JSON: {}", user_json);
@ -88,14 +97,11 @@ impl AuthController {
pub async fn login(
form: web::Form<LoginCredentials>,
session: Session,
_tmpl: web::Data<Tera>
_tmpl: web::Data<Tera>,
) -> Result<impl Responder> {
// For simplicity, always log in the user without checking credentials
// Create a user object with admin role
let mut test_user = User::new(
"Admin User".to_string(),
form.email.clone()
);
let mut test_user = User::new("Admin User".to_string(), form.email.clone());
// Set the ID and admin role
test_user.id = Some(1);
@ -132,6 +138,12 @@ impl AuthController {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "register");
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
.ok()
.filter(|s| !s.is_empty())
.is_some();
ctx.insert("gitea_enabled", &is_gitea_flow_active);
// Add user to context if available
if let Ok(Some(user_json)) = session.get::<String>("user") {
// Keep the raw JSON for backward compatibility
@ -142,7 +154,7 @@ impl AuthController {
Ok(user) => {
log::info!("Successfully parsed user in register_page: {:?}", user);
ctx.insert("user", &user);
},
}
Err(e) => {
log::error!("Failed to parse user JSON in register_page: {}", e);
log::error!("User JSON: {}", user_json);
@ -157,13 +169,10 @@ impl AuthController {
pub async fn register(
form: web::Form<RegistrationData>,
session: Session,
_tmpl: web::Data<Tera>
_tmpl: web::Data<Tera>,
) -> Result<impl Responder> {
// Skip validation and always create an admin user
let mut user = User::new(
form.name.clone(),
form.email.clone()
);
let mut user = User::new(form.name.clone(), form.email.clone());
// Set the ID and admin role
user.id = Some(1);

View File

@ -1,21 +1,15 @@
use actix_web::{web, HttpRequest, HttpResponse, Responder, Result, http::header, cookie::Cookie};
use actix_session::Session;
use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse};
use reqwest::Client;
use crate::config::oauth::GiteaOAuthConfig;
use crate::models::user::{User, UserRole};
use crate::controllers::auth::AuthController;
#[cfg(feature = "gitea")]
use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse};
#[cfg(feature = "gitea")]
use reqwest::Client;
#[cfg(feature = "gitea")]
use crate::config::oauth::GiteaOAuthConfig;
/// Controller for handling Gitea authentication
#[cfg(feature = "gitea")]
pub struct GiteaAuthController;
#[cfg(feature = "gitea")]
impl GiteaAuthController {
/// Initiate the OAuth flow
pub async fn login(
@ -166,14 +160,22 @@ impl GiteaAuthController {
let client = Client::new();
let user_info_url = format!("{}/api/v1/user", oauth_config.instance_url);
let gitea_user = client
log::info!("Gitea instance URL from config: {}", oauth_config.instance_url);
let access_token_secret = token.access_token().secret();
log::info!("Using access token for /api/v1/user: {}", access_token_secret);
let response = client
.get(&user_info_url)
.bearer_auth(token.access_token().secret())
.bearer_auth(access_token_secret)
.send()
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("API request error: {}", e)))?
.json::<crate::config::oauth::GiteaUser>()
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("API request error: {}", e)))?;
let response_body = response.text().await.map_err(|e| actix_web::error::ErrorInternalServerError(format!("Failed to get response body: {}", e)))?;
log::info!("Raw Gitea user info response: {}", response_body);
let gitea_user: crate::config::oauth::GiteaUser = serde_json::from_str(&response_body)
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("JSON parsing error: {}", e)))?;
// Create or update the user in your system
@ -213,7 +215,6 @@ impl GiteaAuthController {
}
/// Query parameters for the OAuth callback
#[cfg(feature = "gitea")]
#[derive(serde::Deserialize)]
pub struct CallbackQuery {
pub code: String,

View File

@ -2,6 +2,7 @@ use actix_web::{web, Result, Responder};
use tera::Tera;
use crate::utils::render_template;
use actix_session::Session;
use std::env;
/// Controller for handling home-related routes
pub struct HomeController;
@ -12,6 +13,12 @@ impl HomeController {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "home");
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
.ok()
.filter(|s| !s.is_empty())
.is_some();
ctx.insert("gitea_enabled", &is_gitea_flow_active);
// Add user to context if available
if let Ok(Some(user_json)) = session.get::<String>("user") {
// Keep the raw JSON for backward compatibility
@ -36,6 +43,11 @@ impl HomeController {
/// Renders the about page
pub async fn about(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
.ok()
.filter(|s| !s.is_empty())
.is_some();
ctx.insert("gitea_enabled", &is_gitea_flow_active);
ctx.insert("active_page", "about");
// Add user to context if available

View File

@ -1,6 +1,5 @@
// Export controllers
pub mod auth;
pub mod debug;
#[cfg(feature = "gitea")]
pub mod gitea_auth;
pub mod home;

View File

@ -1,14 +1,13 @@
#[cfg(feature = "gitea")]
use crate::config::oauth::GiteaOAuthConfig;
use crate::controllers::auth::AuthController;
use crate::controllers::debug::DebugController;
#[cfg(feature = "gitea")]
use crate::controllers::gitea_auth::GiteaAuthController;
use crate::controllers::home::HomeController;
use crate::middleware::JwtAuth;
use crate::SESSION_KEY;
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::web;
use std::env;
/// Configures all application routes
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
@ -26,35 +25,44 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
)
.build();
let mut scope = web::scope("")
.wrap(session_middleware)
// Build the main scope with common routes
let mut main_scope = web::scope("")
.wrap(session_middleware) // Wrap with session middleware
// Home routes
.route("/", web::get().to(HomeController::index))
.route("/about", web::get().to(HomeController::about))
// Auth routes
.route("/login", web::get().to(AuthController::login_page))
.route("/login", web::post().to(AuthController::login))
.route("/register", web::get().to(AuthController::register_page))
.route("/register", web::post().to(AuthController::register))
.route("/logout", web::get().to(AuthController::logout))
// Debug routes
.route("/debug", web::get().to(DebugController::debug_info));
.route("/about", web::get().to(HomeController::about));
#[cfg(feature = "gitea")]
{
// Create the OAuth configuration
// Conditionally add authentication routes based on GITEA_CLIENT_ID environment variable
if env::var("GITEA_CLIENT_ID").ok().filter(|s| !s.is_empty()).is_some() {
// Use Gitea OAuth flow
// Create the OAuth configuration and add it to the scope
let oauth_config = web::Data::new(GiteaOAuthConfig::new());
// Gitea OAuth configuration and routes
scope = scope
.app_data(oauth_config.clone())
main_scope = main_scope
.app_data(oauth_config) // Add oauth_config data
// Gitea OAuth routes
.route("/login", web::get().to(GiteaAuthController::login)) // Add /login route for gitea
.route("/auth/gitea", web::get().to(GiteaAuthController::login))
.route(
"/auth/gitea/callback",
web::get().to(GiteaAuthController::callback),
);
} else {
// Use standard username/password login
main_scope = main_scope
.route("/login", web::get().to(AuthController::login_page))
.route("/login", web::post().to(AuthController::login))
.route("/register", web::get().to(AuthController::register_page))
.route("/register", web::post().to(AuthController::register));
}
cfg.service(scope);
// Add common auth and debug routes (logout is common to both flows)
main_scope = main_scope
.route("/logout", web::get().to(AuthController::logout))
// Debug routes
.route("/debug", web::get().to(DebugController::debug_info));
// Register the main scope service
cfg.service(main_scope);
// Protected routes that require authentication
cfg.service(

View File

@ -1,8 +1,5 @@
{% extends "base.html" %}
{% block title %}Login - Hostbasket{% endblock %}
{% block content %}
{% extends "base.html" %} {% block title %}Login - Hostbasket{% endblock %} {%
block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
@ -11,39 +8,67 @@
<h2>Login</h2>
</div>
<div class="card-body">
{% if not gitea_enabled %}
<form method="post" action="/login">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
<input
type="email"
class="form-control"
id="email"
name="email"
required
/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
<label for="password" class="form-label"
>Password</label
>
<input
type="password"
class="form-control"
id="password"
name="password"
required
/>
</div>
<button type="submit" class="btn btn-primary">Login</button>
<button type="submit" class="btn btn-primary">
Login
</button>
</form>
{% else %}
<hr>
<hr />
{% if gitea_enabled %}
<div class="text-center">
<p>Or login with:</p>
<a href="/auth/gitea" class="btn btn-secondary">
<img src="/static/images/gitea-logo.svg" alt="Gitea" width="20" height="20"
style="margin-right: 5px;">
<img
src="/static/images/gitea-logo.svg"
alt="Gitea"
width="20"
height="20"
style="margin-right: 5px"
/>
Login with Gitea
</a>
</div>
{% endif %}
<hr>
<hr />
{% if not gitea_enabled %}
<div class="text-center">
<p>Don't have an account? <a href="/register">Register</a></p>
<p>
Don't have an account?
<a href="/register">Register</a>
</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -1,7 +1,4 @@
{% extends "base.html" %}
{% block title %}Register - Hostbasket{% endblock %}
{% extends "base.html" %} {% block title %}Register - Hostbasket{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
@ -14,45 +11,65 @@
<form method="post" action="/register">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required>
<input
type="text"
class="form-control"
id="name"
name="name"
required
/>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
<input
type="email"
class="form-control"
id="email"
name="email"
required
/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
<label for="password" class="form-label"
>Password</label
>
<input
type="password"
class="form-control"
id="password"
name="password"
required
/>
</div>
<div class="mb-3">
<label for="password_confirmation" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="password_confirmation"
name="password_confirmation" required>
<label
for="password_confirmation"
class="form-label"
>Confirm Password</label
>
<input
type="password"
class="form-control"
id="password_confirmation"
name="password_confirmation"
required
/>
</div>
<button type="submit" class="btn btn-primary">Register</button>
<button type="submit" class="btn btn-primary">
Register
</button>
</form>
<hr>
{% if gitea_enabled %}
<div class="text-center">
<p>Or register with:</p>
<a href="/auth/gitea" class="btn btn-secondary">
<img src="/static/images/gitea-logo.svg" alt="Gitea" width="20" height="20"
style="margin-right: 5px;">
Register with Gitea
</a>
</div>
{% endif %}
<hr>
<hr />
<div class="text-center">
<p>Already have an account? <a href="/login">Login</a></p>
<p>
Already have an account? <a href="/login">Login</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -43,11 +43,13 @@
<a class="nav-link {{ active_class(current=active_page, page=" login") }}"
href="/login">Login</a>
</li>
{% if gitea_enabled == false %}
<li class="nav-item">
<a class="nav-link {{ active_class(current=active_page, page=" register") }}"
href="/register">Register</a>
</li>
{% endif %}
{% endif %}
</ul>
</div>
</div>
@ -61,6 +63,7 @@
<summary>Debug Info</summary>
<pre>user_json: {{ user_json }}</pre>
<pre>user object exists: {{ user is defined }}</pre>
<pre>is_gitea_flow_active: {{ gitea_enabled }}</pre>
{% if user is defined %}
<pre>user.name: {{ user.name }}</pre>
{% endif %}
@ -81,4 +84,4 @@
{% block scripts %}{% endblock %}
</body>
</html>
</html>

4
start.sh Normal file → Executable file
View File

@ -4,5 +4,7 @@
cd "$(dirname "$0")"
export SECRET_KEY=1234
export GITEA_CLIENT_ID=""
export GITEA_CLIENT_SECRET=""
export GITEA_INSTANCE_URL="https://git.ourworld.tf"
cargo run

View File

@ -3,8 +3,8 @@
# Get the directory of the script and change to it
cd "$(dirname "$0")"
export GITEA_CLIENT_ID="your_client_id"
export GITEA_CLIENT_SECRET="your_client_secret"
export GITEA_INSTANCE_URL="https://gitea.example.com"
export GITEA_CLIENT_ID="9f409b35-6258-4ac3-8370-05adc187c1f5"
export GITEA_CLIENT_SECRET="gto_4s77ae33m5ernlf2423wx6wjyyqatqoe567rym7fcu3sqmu5azea"
export GITEA_INSTANCE_URL="https://git.ourworld.tf"
export APP_URL="http://localhost:9999"
cargo run --features gitea
cargo run