- 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`.
363 lines
12 KiB
Markdown
363 lines
12 KiB
Markdown
# Gitea Authentication Guide
|
|
|
|
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.
|
|
|
|
## 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:
|
|
|
|
```bash
|
|
export GITEA_CLIENT_ID=your_client_id
|
|
export GITEA_CLIENT_SECRET=your_client_secret
|
|
export GITEA_INSTANCE_URL=https://your-gitea-instance.com
|
|
```
|
|
|
|
2. Alternatively, add these settings to your configuration file (`config/default.toml`):
|
|
|
|
```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`:
|
|
|
|
```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`:
|
|
|
|
```rust
|
|
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`:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```html
|
|
{% 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.
|