rweb_starterkit/docs/gitea-auth.md
Mahmoud Emad 645a387528 feat: Add basic project structure and configuration
- Add `.env.template` file for environment variable configuration.
- Add `.gitignore` file to ignore generated files and IDE artifacts.
- Add `Cargo.toml` file specifying project dependencies.
- Add basic project documentation in `README.md` and configuration
  guide in `docs/configuration.md`.
- Add Gitea authentication guide in `docs/gitea-auth.md`.
- Add installation guide in `docs/installation.md`.
- Add MVC architecture guide in `docs/mvc.md`.
- Add views guide in `docs/views.md`.
2025-05-07 14:03:08 +03:00

12 KiB

Gitea Authentication Guide

This guide provides detailed instructions on how to integrate Gitea authentication into your Hostbasket application.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Setting Up Gitea OAuth Application
  4. Configuring Hostbasket for Gitea Authentication
  5. Implementing the OAuth Flow
  6. Testing the Integration
  7. 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.

Prerequisites

Before you begin, ensure you have:

  • A running Gitea instance
  • Administrative access to your Gitea instance
  • A running Hostbasket application

Setting Up Gitea OAuth Application

  1. Log in to your Gitea instance as an administrator
  2. Navigate to Site Administration > Applications
  3. Click Create a New OAuth2 Application
  4. Fill in the application details:
    • Application Name: Hostbasket
    • Redirect URI: http://localhost:9999/auth/gitea/callback (adjust the URL to match your Hostbasket instance)
    • Confidential Client: Check this box
  5. Click Create Application
  6. Note the Client ID and Client Secret that are generated

Configuring Hostbasket for Gitea Authentication

  1. Add the following environment variables to your Hostbasket configuration:
export GITEA_CLIENT_ID=your_client_id
export GITEA_CLIENT_SECRET=your_client_secret
export GITEA_INSTANCE_URL=https://your-gitea-instance.com
  1. Alternatively, add these settings to your configuration file (config/default.toml):
[oauth.gitea]
client_id = "your_client_id"
client_secret = "your_client_secret"
instance_url = "https://your-gitea-instance.com"

Implementing the OAuth Flow

To implement the OAuth flow, you need to add new routes and controllers to your Hostbasket application.

1. Add Required Dependencies

Add the following dependencies to your Cargo.toml:

[dependencies]
oauth2 = "4.3"
reqwest = { version = "0.11", features = ["json"] }

2. Create a Gitea OAuth Configuration Module

Create a new file src/config/oauth.rs:

use oauth2::{
    AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl,
    basic::BasicClient, AuthorizationCode, CsrfToken, Scope, TokenResponse,
};
use serde::{Deserialize, Serialize};
use std::env;

#[derive(Clone, Debug)]
pub struct GiteaOAuthConfig {
    pub client: BasicClient,
    pub instance_url: String,
}

impl GiteaOAuthConfig {
    pub fn new() -> Self {
        // Get configuration from environment variables
        let client_id = env::var("GITEA_CLIENT_ID")
            .expect("Missing GITEA_CLIENT_ID environment variable");
        let client_secret = env::var("GITEA_CLIENT_SECRET")
            .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)),
            AuthUrl::new(auth_url).unwrap(),
            Some(TokenUrl::new(token_url).unwrap()),
        )
        .set_redirect_uri(
            RedirectUrl::new("http://localhost:9999/auth/gitea/callback".to_string()).unwrap(),
        );
        
        Self {
            client,
            instance_url,
        }
    }
}

// Gitea user information structure
#[derive(Debug, Deserialize, Serialize)]
pub struct GiteaUser {
    pub id: i64,
    pub login: String,
    pub full_name: String,
    pub email: String,
    pub avatar_url: String,
}

3. Create a Gitea Authentication Controller

Create a new file src/controllers/gitea_auth.rs:

use actix_web::{web, 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;
use serde_json::json;

pub struct GiteaAuthController;

impl GiteaAuthController {
    // Initiate the OAuth flow
    pub async fn login(
        oauth_config: web::Data<GiteaOAuthConfig>,
        session: Session,
    ) -> Result<impl Responder> {
        // Generate the authorization URL
        let (auth_url, csrf_token) = oauth_config
            .client
            .authorize_url(CsrfToken::new_random)
            .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>,
        session: Session,
        query: web::Query<CallbackQuery>,
    ) -> Result<impl Responder> {
        // 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
            .exchange_code(AuthorizationCode::new(query.code.clone()))
            .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())
            .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!("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("/")
            .http_only(true)
            .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)
            .append_header((header::LOCATION, "/"))
            .finish())
    }
}

// Query parameters for the OAuth callback
#[derive(serde::Deserialize)]
pub struct CallbackQuery {
    pub code: String,
    pub state: String,
}

4. Update the Routes Configuration

Update your src/routes/mod.rs file to include the new Gitea authentication routes:

use crate::controllers::gitea_auth::GiteaAuthController;
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(),
        SESSION_KEY.clone()
    )
    .cookie_secure(false) // Set to true in production with HTTPS
    .build();

    // Public routes that don't require authentication
    cfg.service(
        web::scope("")
            .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))
    );
}

5. Update the Login Page

Update your login page template (src/views/auth/login.html) to include a "Login with Gitea" button:

{% extends "base.html" %}

{% block title %}Login{% endblock %}

{% block content %}
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card mt-5">
                <div class="card-header">
                    <h2>Login</h2>
                </div>
                <div class="card-body">
                    <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>
                        </div>
                        <div class="mb-3">
                            <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>
                    </form>
                    
                    <hr>
                    
                    <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">
                            Login with Gitea
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

Testing the Integration

  1. Start your Hostbasket application
  2. Navigate to the login page
  3. Click the "Login with Gitea" button
  4. You should be redirected to your Gitea instance's authorization page
  5. Authorize the application
  6. You should be redirected back to your Hostbasket application and logged in

Troubleshooting

Common Issues

  1. Redirect URI Mismatch:

    • Error: The redirect URI provided is missing or does not match
    • Solution: Ensure that the redirect URI in your Gitea OAuth application settings matches the one in your code.
  2. Invalid Client ID or Secret:

    • Error: Invalid client_id or client_secret
    • Solution: Double-check your client ID and secret.
  3. CSRF Token Mismatch:

    • Error: Invalid CSRF token
    • Solution: Ensure that your session is properly configured and that the CSRF token is being stored and retrieved correctly.
  4. API Request Errors:

    • Error: API request error
    • Solution: Check your Gitea instance URL and ensure that the API is accessible.
  5. JSON Parsing Errors:

    • Error: JSON parsing error
    • Solution: Ensure that the API response matches the expected structure.