diff options
| -rw-r--r-- | .env.example | 66 | ||||
| -rw-r--r-- | LICENSE | 28 | ||||
| -rw-r--r-- | README.ja.md | 347 | ||||
| -rw-r--r-- | README.md | 158 | ||||
| -rw-r--r-- | cmd/server/main.go | 63 | ||||
| -rw-r--r-- | config/config.go | 2 | ||||
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | handlers/php_proxy.go | 128 | ||||
| -rw-r--r-- | logging/logging.go | 9 | ||||
| -rw-r--r-- | middleware/auth.go | 5 | ||||
| -rw-r--r-- | templates/php/account.php | 29 | ||||
| -rw-r--r-- | templates/php/account_password.php | 42 | ||||
| -rw-r--r-- | templates/php/login.php | 42 | ||||
| -rw-r--r-- | templates/php/register.php | 46 |
15 files changed, 933 insertions, 39 deletions
diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0d60870 --- /dev/null +++ b/.env.example @@ -0,0 +1,66 @@ +# --- Required --- +# The URL of the backend service you want to protect +TARGET_URL=http://localhost:8081 + +# A long, random string for securing session cookies and JWTs +# For production, generate a new key using: openssl rand -base64 45 +SESSION_SECRET= + +# --- Database --- +# Use ONE of the following options: + +# Option 1: PostgreSQL or MySQL (Recommended) +# Example: postgres://user:password@host:port/dbname?sslmode=disable +# Example: mysql://user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=true +# DATABASE_URL= + +# Option 2: SQLite (Default if DATABASE_URL is not set) +DATABASE_PATH=./auth.db + +# --- Frontend & Proxy --- +# Frontend mode: "js" (default) or "php" +FRONTEND_TYPE=js +# If true, protects the frontend path with session authentication +PROTECT_FRONTEND=false +# If true, protects the API path with bearer token authentication +PROTECT_API=false +# Base path for frontend routes to be protected +FRONT_PATH=/ +# Base path for API routes to be protected +API_PATH=/api/ + +# --- Security Policies --- + +# Brute-force protection settings +MAX_LOGIN_ATTEMPTS=5 +LOCKOUT_DURATION_MINUTES=10 + +# User creation rate limit settings +USER_CREATION_RATE_LIMIT_MAX_REQUESTS=5 +USER_CREATION_RATE_LIMIT_WINDOW_SECONDS=3600 + +# Password policy (none, standard, enhanced, strict) +PASSWORD_POLICY=standard + +# --- Token Durations --- +# Duration for the web session cookie +TOKEN_DURATION_HOURS=24 +# Duration for the API JWT access token +ACCESS_TOKEN_DURATION_MINUTES=15 +# Duration for the API JWT refresh token +REFRESH_TOKEN_DURATION_DAYS=7 + +# --- PHP Frontend Settings (only needed if FRONTEND_TYPE=php) --- +# PHP_FPM_SOCKET=/run/php-fpm/php-fpm.sock +# PHP_DOC_ROOT= + +# --- Optional: Path Overrides --- +# Uncomment and change these to avoid URL conflicts with your backend application. +# AUTH_PATH_LOGIN=/login +# AUTH_PATH_REGISTER=/register +# AUTH_PATH_LOGOUT=/logout +# AUTH_PATH_ACCOUNT=/account +# AUTH_PATH_ACCOUNT_PASSWORD=/account/password +# AUTH_PATH_ADMIN=/admin +# AUTH_PATH_ADMIN_USERS_API=/api/admin/users +# AUTH_ASSETS_PATH=/assets @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, haturau + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 0000000..683519a --- /dev/null +++ b/README.ja.md @@ -0,0 +1,347 @@ +# Auth Proxy (認証プロキシ) + +これはGoで書かれたシンプルな認証リバースプロキシです。あらゆるバックエンドウェブサービスの前に配置できる、柔軟な認証レイヤーを提供します。 + +ユーザー登録、ログイン、セッション管理を処理し、認証されたリクエストを設定済みのバックエンドにプロキシします。また、ユーザー管理のための管理ダッシュボードも含まれています。 + +## 主な機能 + +- **認証**: ログイン、ログアウト、登録ページを提供します。 +- **リバースプロキシ**: 認証されたユーザーをバックエンドサービスにプロキシします。 +- **管理ダッシュボード**: ユーザーを管理(ロールの更新、有効化/無効化、削除)するためのシンプルなUI。 +- **柔軟なデータベースサポート**: PostgreSQL, MySQL, SQLiteに対応。 +- **プラグイン可能なフロントエンド**: 複数のフロントエンドモード(JS駆動またはPHP)をサポート。 +- **セキュリティ強化**: + - **ブルートフォース攻撃からの保護**: 設定可能な回数ログインに失敗したユーザーアカウントをロックします。 + - **ユーザー作成レート制限**: 同一IPアドレスから短時間に多くのユーザーが作成されるのを防ぎます。 + - **設定可能なパスワードポリシー**: パスワードの強度要件を強制します。詳細は下記を参照。 + - **セキュアなセッションクッキー**: セッション管理に `HttpOnly` のセキュアなクッキーを使用します。 +- **高い設定可能性**: データベース接続、セキュリティポリシー、内部URLパスなど、ほぼ全ての側面を `.env` ファイルで設定可能です。 +- **構造化ロギング**: `slog` を使用した詳細なアプリケーションログとセキュリティログ。 +- **管理者作成用CLI**: 初期管理者ユーザーを簡単に作成するためのコマンドラインツール。 + +## セキュリティと認証の詳細 + +### パスワードの暗号化 + +ユーザーのパスワードは、平文(プレーンテキスト)では一切保存されません。パスワード専用に設計された強力で適応性のあるハッシュ関数である **bcrypt** アルゴリズムを使用して、安全にハッシュ化されます。ユーザーがログインする際、入力されたパスワードはハッシュ化され、保存されているハッシュ値と比較されます。これにより、万が一データベースが侵害された場合でも、元のパスワードが漏洩するのを防ぎます。 + +### 認証フロー + +このプロキシは、異なるユースケースに対応するため、2つの主要な認証フローをサポートしています。 + +#### 1. Web UI(セッションベース) + +このフローは、Webブラウザを介してアプリケーションを操作するユーザー向けに設計されています。 + +1. ユーザーがログインページからユーザー名とパスワードを送信します。 +2. サーバーは、データベース内の情報と照合して認証情報を検証します。 +3. 成功すると、暗号学的に安全なランダムトークンが生成され、ユーザーに関連付けられてデータベースに保存されます。 +4. このトークンは、`auth_token` という名前の安全な `HttpOnly` クッキーとしてユーザーのブラウザに送信され、セッションが確立されます。 + +#### 2. API(JWTベース) + +このフローは、プログラムによるクライアントやサービス向けに設計されています。 + +1. クライアントが、ユーザーのユーザー名とパスワードを含む`POST`リクエストを`/api/auth/token`エンドポイントに送信します。 +2. サーバーは認証情報を検証します。 +3. 成功すると、2種類のトークンを生成して返します。 + * ユーザー情報(ID、ロール)と有効期限を含む、短命の**JWTアクセストークン**。このトークンは改ざんを防ぐために署名されています。 + * 新しいアクセストークンを取得するために使用できる、長命の**リフレッシュトークン**。これはデータベースに保存されます。 + +### セッション管理 + +#### Web UIセッション + +ブラウザからの後続のリクエストでは、`auth_token`クッキーが自動的にサーバーに送信されます。ミドルウェアがこのトークンをデータベースで検索して検証します。トークンが有効で期限切れでなければ、リクエストは認証され、処理が続行されます。ユーザーがログアウトすると、セッショントークンはデータベースから削除され、セッションが無効になります。 + +#### APIセッション(JWTによるステートレス管理) + +APIリクエストの場合、クライアントは`Authorization: Bearer <token>`ヘッダーにJWTアクセストークンを含める必要があります。サーバー上のミドルウェアが、トークンの署名と有効期限を検証します。このプロセスはステートレスであり、リクエストごとにデータベースを検索する必要がないため、非常に効率的です。アクセストークンが期限切れになった場合、クライアントはリフレッシュトークンを使用して、再認証することなく新しいアクセストークンを要求できます。 + +**認証の優先順位に関する注意:** APIエンドポイントが保護されている場合(`PROTECT_API=true`)、ミドルウェアはまず`Authorization: Bearer`ヘッダーを探します。このヘッダーが存在しない場合、フォールバックとして`auth_token`セッションクッキーの検証を試みます。これにより、Web UIにログインしているユーザーは、ブラウザセッションを使用してAPIに対しても認証できます。しかし、これはAPIエンドポイントが有効なJWTまたは有効なセッションクッキーのいずれかによってアクセス可能であることを意味します。 + +## はじめに + +### 前提条件 + +- Go 1.21 以降 +- (任意) PHPフロントエンドを使用する場合は、実行中のPHP-FPMサービス。 +- (任意) PostgreSQL または MySQL データベースサーバー。 + +### インストール + +1. **リポジトリをクローン:** + ```sh + git clone <repository-url> + cd auth-proxy + ``` + +2. **依存関係をインストール:** + ```sh + go mod tidy + ``` + +3. **バイナリをビルド:** + ```sh + go build -o auth-proxy-server ./cmd/server && go build -o admin-cli ./cmd/admin-cli + ``` + +### 設定 + +設定はプロジェクトのルートにある `.env` ファイルで管理されます。`.env` という名前のファイルを作成し、必要な変数を追加してください。 + +**`.env` ファイルの例:** + +```dotenv +# --- 必須 --- +# 保護したいバックエンドサービスのURL +TARGET_URL=http://localhost:8081 + +# セッションクッキーとJWTを保護するための長くてランダムな文字列 +# 本番環境では、openssl rand -base64 45 を使って新しいキーを生成してください +SESSION_SECRET=my-super-secret-key + +# --- データベース --- +# 以下のいずれかのオプションを使用してください: + +# オプション1: PostgreSQL または MySQL (推奨) +# 例: postgres://user:password@host:port/dbname?sslmode=disable +# 例: mysql://user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=true +DATABASE_URL=mysql://test:test@127.0.0.1:3306/auth + +# オプション2: SQLite (DATABASE_URLが設定されていない場合のデフォルト) +# DATABASE_PATH=./auth.db + +# --- フロントエンド & プロキシ --- +# フロントエンドモード: "js" (デフォルト) または "php" +FRONTEND_TYPE=js +# trueの場合、フロントエンドパスをセッション認証で保護します +PROTECT_FRONTEND=false +# trueの場合、APIパスをベアラートークン認証で保護します +PROTECT_API=false +# 保護対象のフロントエンドのベースパス +FRONT_PATH=/ +# 保護対象のAPIのベースパス +API_PATH=/api/ + +# --- セキュリティポリシー --- + +# ブルートフォース攻撃からの保護設定 +MAX_LOGIN_ATTEMPTS=5 +LOCKOUT_DURATION_MINUTES=10 + +# ユーザー作成レート制限設定 +USER_CREATION_RATE_LIMIT_MAX_REQUESTS=5 +USER_CREATION_RATE_LIMIT_WINDOW_SECONDS=3600 + +# パスワードポリシー (none, standard, enhanced, strict) +PASSWORD_POLICY=standard + +# --- トークン有効期間 --- +# Webセッションクッキーの有効期間 +TOKEN_DURATION_HOURS=24 +# API JWTアクセストークンの有効期間 +ACCESS_TOKEN_DURATION_MINUTES=15 +# API JWTリフレッシュトークンの有効期間 +REFRESH_TOKEN_DURATION_DAYS=7 + +# --- 任意: パスのオーバーライド --- +# バックエンドアプリケーションとのURL競合を避けるためにコメントを解除して変更してください。 +# AUTH_PATH_LOGIN=/login +# AUTH_PATH_REGISTER=/register +# AUTH_PATH_LOGOUT=/logout +# AUTH_PATH_ACCOUNT=/account +# AUTH_PATH_ACCOUNT_PASSWORD=/account/password +# AUTH_PATH_ADMIN=/admin +# AUTH_PATH_ADMIN_USERS_API=/api/admin/users +# AUTH_ASSETS_PATH=/assets +``` + +### フロントエンドの選択 + +`FRONTEND_TYPE` 環境変数を使用して、2つのフロントエンドモードから選択できます。 + +- **`js` (または空)**: これがデフォルトモードです。サーバーは基本的なHTMLテンプレートを描画し、すべての認証ロジック(フォームの描画、ユーザー入力の処理など)は、GoバックエンドのAPIエンドポイントと通信するフロントエンドのJavaScriptコードによって管理されることが想定されています。 +- **`php`**: PHPベースのビューを使用するには、実行中のPHP-FPMサービスが必要です。このモードは、認証ページのGETリクエストをPHP-FPMサービスにプロキシし、POSTリクエスト(ログイン送信など)はGoバックエンドで処理します。 + +#### PHPフロントエンドのセットアップ + +`FRONTEND_TYPE=php` を選択した場合は、以下の手順に従ってください。 + +1. **`.env`でフロントエンドタイプを設定**: + ```dotenv + FRONTEND_TYPE=php + ``` + +2. **`.env`でPHP-FPM接続を設定**: + PHP-FPMソケットへのパスとドキュメントルートを提供する必要があります。 + - `PHP_FPM_SOCKET`: PHP-FPMソケットファイルへのパス(例: `/run/php-fpm/php-fpm.sock`)。 + - `PHP_DOC_ROOT`: PHPビューファイル(例: `login.php`)を含むディレクトリへの**絶対パス**。このパスは、*PHP-FPMプロセスからアクセス可能*である必要があります。 + +3. **PHPファイルのコピーと権限設定**: + PHP-FPMサービスを実行するユーザーは、PHPテンプレートファイルへの読み取りアクセス権が必要です。 + + まず、PHP-FPMの実行ユーザーを特定します。これは、PHP-FPMプール設定ファイル(例: `/etc/php/8.3/fpm/pool.d/www.conf`)で確認するか、実行中のプロセスを調べることで確認できます。 + ```sh + # PHP-FPMユーザーを見つけるためのコマンド例 + ps aux | grep php-fpm + ``` + 一般的なユーザーは `www-data`、`http`、`apache` です。 + + 次に、`templates/php` ディレクトリをPHP-FPMがアクセス可能な場所(例: `/var/www/html/auth-proxy`)にコピーし、正しい所有権を設定します。 + + ```sh + # セットアップ例(ユーザーが 'http' の場合) + sudo cp -r templates/php /var/www/html/auth-proxy/php + sudo chown -R http:http /var/www/html/auth-proxy + ``` + + 最後に、`.env` ファイルを正しいパスで更新します。 + ```dotenv + # --- PHP Frontend Settings --- + FRONTEND_TYPE=php + PHP_FPM_SOCKET=/run/php-fpm/php-fpm.sock + PHP_DOC_ROOT=/var/www/html/auth-proxy/php + ``` + +### データベース設定例 + +PostgreSQLまたはMySQLを使用している場合は、プロキシ用のデータベースとユーザーを作成する必要があります。 + +#### PostgreSQL + +`psql` で以下のコマンドを実行します: + +```sql +-- 専用データベースを作成 +CREATE DATABASE auth_proxy; + +-- 専用ユーザーを作成 +CREATE USER auth_user WITH PASSWORD 'your_strong_password'; + +-- ユーザーにデータベースの全権限を付与 +GRANT ALL PRIVILEGES ON DATABASE auth_proxy TO auth_user; +``` + +その場合、`.env` ファイルの `DATABASE_URL` は次のようになります: +`DATABASE_URL=postgres://auth_user:your_strong_password@localhost:5432/auth_proxy?sslmode=disable` + +#### MySQL / MariaDB + +MySQLクライアントで以下のコマンドを実行します: + +```sql +-- 専用データベースを作成 +CREATE DATABASE auth_proxy CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 専用ユーザーを作成 +CREATE USER 'auth_user'@'localhost' IDENTIFIED BY 'your_strong_password'; + +-- ユーザーにデータベースの全権限を付与 +GRANT ALL PRIVILEGES ON auth_proxy.* TO 'auth_user'@'localhost'; + +-- 変更を適用 +FLUSH PRIVILEGES; +``` + +その場合、`.env` ファイルの `DATABASE_URL` は次のようになります: +`DATABASE_URL=mysql://auth_user:your_strong_password@tcp(127.0.0.1:3306)/auth_proxy?parseTime=true` + +## 使い方 + +1. **管理者ユーザーの作成**: + + サーバーを起動する前に、`admin-cli` ツールを使用して最初の管理者ユーザーを作成します。CLIはデータベース接続に `.env` ファイルを使用するため、正しく設定されていることを確認してください。 + + `--username` と `--password` フラグは必須です。 + + *成功時のコマンド:* + ```sh + ./admin-cli --username <your-admin-username> --password <your-strong-password> + ``` + + *フラグが不足している場合の例:* + ```sh + $ ./admin-cli + time=2025-10-12T04:28:09.014+09:00 level=ERROR msg="Both --username and --password flags are required" + ``` + +2. **プロキシサーバーの実行**: + + ```sh + ./auth-proxy-server + ``` + + サーバーはデフォルトでポート `:8080` で起動します。 + +3. **アプリケーションへのアクセス**: + + - ブラウザを開き、`http://localhost:8080` にアクセスします。 + - ログインページにリダイレクトされます。 + - 作成した管理者認証情報でログインします。 + - 認証されると、`TARGET_URL` にシームレスにプロキシされます。 + - 管理ダッシュボードには `http://localhost:8080/admin` (または設定したパス)でアクセスできます。 + +## APIトークン(ベアラートークン)の発行 + +Webブラウザ向けのセッションベースの認証に加えて、プロキシはプログラムによるAPIアクセスのためにJWT(JSON Web Token)を発行できます。これらのトークンは、保護されたバックエンドAPIへのリクエストを認証するために、`Authorization` ヘッダーでベアラートークンとして使用できます。 + +アクセストークンとリフレッシュトークンを取得するには、`/api/auth/token` エンドポイントに、JSONボディにユーザーの認証情報を入れて `POST` リクエストを送信します。 + +**`curl` を使用した例:** + +```sh +curl -X POST -H "Content-Type: application/json" -d '{ + "username": "your-username", + "password": "your-password" +}' http://localhost:8080/api/auth/token +``` + +**成功時のレスポンス:** + +認証情報が有効な場合、サーバーは `access_token` と `refresh_token` を含むJSONオブジェクトで応答します。 + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE2Njc4OTg5ODl9.abcdef123456...", + "refresh_token": "def456..." +} +``` + +`access_token` は短命のJWTであり、APIリクエストの `Authorization` ヘッダーで送信する必要があります: + +``` +Authorization: Bearer <access_token> +``` + +`refresh_token` は長命のトークンで、古いアクセストークンが期限切れになったときに新しいアクセストークンを取得するために使用できます。これを行うには、`/auth/refresh` エンドポイントに `POST` リクエストを送信します。 + +**注意:** トークンの有効期間は `.env` ファイルで設定できます。詳細は設定表を参照してください。 + +## 設定詳細 + +| 環境変数 | 説明 | デフォルト値 | +| --- | --- | --- | +| `TARGET_URL` | **(必須)** プロキシ先のバックエンドサービスのURL。 | - | +| `SESSION_SECRET` | **(必須)** セッションクッキーの暗号化とJWTの署名に使用する、長くてランダムな秘密鍵。 | `default-secret-key-for-dev` | +| `DATABASE_URL` | PostgreSQLまたはMySQLの接続文字列。設定されている場合、`DATABASE_PATH`より優先されます。 | - | +| `DATABASE_PATH` | SQLiteデータベースのファイルパス。`DATABASE_URL`が設定されていない場合のみ使用されます。 | `./auth.db` | +| `LOG_LEVEL` | ログレベルを設定します。`DEBUG`, `INFO`, `WARN`, `ERROR`が指定できます。 | `INFO` | +| `FRONTEND_TYPE` | フロントエンドのレンダリングモードを切り替えます。`js`または`php`が指定できます。 | `js` | +| `PHP_FPM_SOCKET` | PHP-FPMソケットへのファイルパス。`FRONTEND_TYPE`が`php`の場合のみ使用されます。 | `/run/php-fpm/php-fpm.sock` | +| `PHP_DOC_ROOT` | PHPファイルディレクトリへの絶対パス。`FRONTEND_TYPE`が`php`の場合のみ使用されます。 | - | +| `MAX_LOGIN_ATTEMPTS` | アカウントがロックされるまでのログイン失敗回数。 | `5` | +| `LOCKOUT_DURATION_MINUTES` | アカウントがロックされている期間(分)。 | `10` | +| `USER_CREATION_RATE_LIMIT_MAX_REQUESTS` | 時間枠内に単一IPから許可されるユーザー登録の最大数。 | `5` | +| `USER_CREATION_RATE_LIMIT_WINDOW_SECONDS` | ユーザー作成レート制限の時間枠(秒)。 | `3600` (1時間) | +| `PASSWORD_POLICY` | パスワード強度要件を設定します (`none`, `standard`, `enhanced`, `strict`)。 | `standard` | +| `TOKEN_DURATION_HOURS` | Webセッションクッキーの有効期間(時間)。 | `24` | +| `ACCESS_TOKEN_DURATION_MINUTES` | APIクライアント用のJWTアクセストークンの有効期間(分)。 | `15` | +| `REFRESH_TOKEN_DURATION_DAYS` | APIクライアント用のJWTリフレッシュトークンの有効期間(日)。 | `7` | +| `PROTECT_FRONTEND` | `true`の場合、すべてのフロントエンドパスをセッション認証経由でプロキシします。 | `false` | +| `PROTECT_API` | `true`の場合、すべてのAPIパスをベアラートークン認証経由でプロキシします。 | `false` | +| `FRONT_PATH` | `PROTECT_FRONTEND`で保護されるフロントエンドのベースパス。 | `/` | +| `API_PATH` | `PROTECT_API`で保護されるAPIルートのベースパス。 | `/api/` | +| `AUTH_PATH_*` | ログイン、管理者ページなどの内部URLをカスタマイズするための変数セット。 | 様々、例: `/login`| +| `AUTH_ASSETS_PATH` | 内部静的アセット(CSS, JS)を提供するためのURLパス。 | `/assets` | @@ -10,6 +10,7 @@ It handles user registration, login, and session management, and proxies authent - **Reverse Proxy**: Proxies authenticated users to a backend service. - **Admin Dashboard**: A simple UI to manage users (update roles, activate/deactivate, delete). - **Flexible Database Support**: Works with PostgreSQL, MySQL, and SQLite. +- **Pluggable Frontend**: Supports multiple frontend modes (JS-driven or PHP). - **Security Hardening**: - **Brute-force Protection**: Locks user accounts after a configurable number of failed login attempts. - **User Creation Rate Limiting**: Prevents the same IP from creating too many users in a short period. @@ -19,20 +20,54 @@ It handles user registration, login, and session management, and proxies authent - **Structured Logging**: Detailed application and security logs using `slog`. - **CLI for Admin Creation**: A command-line tool to easily create initial admin users. -### Password Policies +## Security and Authentication Details -The password strength requirements can be configured by setting the `PASSWORD_POLICY` variable in the `.env` file. The available levels are: +### Password Encryption -- **`none`**: No requirements. Any password is allowed. -- **`standard`** (Default): A minimum length of 6 characters is required. -- **`enhanced`**: A minimum length of 8 characters is required. The password must contain at least one of each of the following: an uppercase letter, a lowercase letter, a number, and a symbol (e.g., `!@#$%^&*`). -- **`strict`**: A minimum length of 12 characters is required. The password must meet the same complexity requirements as `enhanced` (uppercase, lowercase, number, and symbol). +User passwords are never stored in plaintext. They are securely hashed using the **bcrypt** algorithm, a strong, adaptive hashing function designed specifically for passwords. When a user logs in, the provided password is +hashed and then compared to the stored hash, preventing plaintext password exposure even if the database is compromised. + +### Authentication Flow + +The proxy supports two distinct authentication flows for different use cases: + +#### 1. Web UI (Session-Based) + +This flow is designed for users interacting with the application through a web browser. + +1. A user submits their username and password via the login page. +2. The server validates the credentials against the database. +3. On success, a cryptographically secure, random token is generated and stored in the database, associated with the user. +4. This token is sent to the user's browser in a secure, `HttpOnly` cookie named `auth_token`, establishing a session. + +#### 2. API (JWT-Based) + +This flow is designed for programmatic clients and services. + +1. A client sends a `POST` request with the user's username and password to the `/api/auth/token` endpoint. +2. The server validates the credentials. +3. On success, it generates and returns two tokens: + * A short-lived **JWT Access Token** containing user claims (ID, role) and an expiration time. This token is signed to prevent tampering. + * A long-lived **Refresh Token** that is stored in the database and can be used to obtain a new access token. + +### Session Management + +#### Web UI Sessions + +For subsequent requests from a browser, the `auth_token` cookie is automatically sent to the server. A middleware validates this token by looking it up in the database. If the token is valid and has not expired, the request is authenticated and allowed to proceed. When a user logs out, the session token is deleted from the database, effectively invalidating the session. + +#### API Sessions (Stateless with JWT) + +For API requests, the client must include the JWT Access Token in the `Authorization: Bearer <token>` header. A middleware on the server validates the token's signature and expiration time. This process is stateless, meaning it does not require a database lookup for every request, making it highly efficient. If the access token has expired, the client can use the refresh token to request a new access token without re-authenticating. + +**Note on Authentication Precedence:** When an API endpoint is protected (i.e., `PROTECT_API=true`), the middleware first looks for an `Authorization: Bearer` header. If this header is not present, it will fall back to validating the `auth_token` session cookie. This allows users who are logged into the web UI to also authenticate to the API using their browser session, but it means that API endpoints are accessible via either a valid JWT or a valid session cookie. ## Getting Started ### Prerequisites - Go 1.21 or later +- (Optional) A running PHP-FPM service if you intend to use the PHP frontend. - (Optional) PostgreSQL or MySQL database server. ### Installation @@ -64,7 +99,7 @@ Configuration is managed via a `.env` file in the root of the project. Create a # The URL of the backend service you want to protect TARGET_URL=http://localhost:8081 -# A long, random string for securing session cookies +# A long, random string for securing session cookies and JWTs # For production, generate a new key using: openssl rand -base64 45 SESSION_SECRET=my-super-secret-key @@ -79,6 +114,18 @@ DATABASE_URL=mysql://test:test@127.0.0.1:3306/auth # Option 2: SQLite (Default if DATABASE_URL is not set) # DATABASE_PATH=./auth.db +# --- Frontend & Proxy --- +# Frontend mode: "js" (default) or "php" +FRONTEND_TYPE=js +# If true, protects the frontend path with session authentication +PROTECT_FRONTEND=false +# If true, protects the API path with bearer token authentication +PROTECT_API=false +# Base path for frontend routes to be protected +FRONT_PATH=/ +# Base path for API routes to be protected +API_PATH=/api/ + # --- Security Policies --- # Brute-force protection settings @@ -92,6 +139,14 @@ USER_CREATION_RATE_LIMIT_WINDOW_SECONDS=3600 # Password policy (none, standard, enhanced, strict) PASSWORD_POLICY=standard +# --- Token Durations --- +# Duration for the web session cookie +TOKEN_DURATION_HOURS=24 +# Duration for the API JWT access token +ACCESS_TOKEN_DURATION_MINUTES=15 +# Duration for the API JWT refresh token +REFRESH_TOKEN_DURATION_DAYS=7 + # --- Optional: Path Overrides --- # Uncomment and change these to avoid URL conflicts with your backend application. # AUTH_PATH_LOGIN=/login @@ -101,9 +156,56 @@ PASSWORD_POLICY=standard # AUTH_PATH_ACCOUNT_PASSWORD=/account/password # AUTH_PATH_ADMIN=/admin # AUTH_PATH_ADMIN_USERS_API=/api/admin/users -# AUTH_ASSETS_PATH=/auth-proxy-assets +# AUTH_ASSETS_PATH=/assets ``` +### Frontend Selection + +You can choose between two frontend modes using the `FRONTEND_TYPE` environment variable: + +- **`js` (or empty)**: This is the default mode. The server renders the basic HTML templates, and all authentication logic (rendering forms, handling user input) is expected to be managed by your frontend JavaScript code, which will interact with the Go backend's API endpoints. +- **`php`**: To use the PHP-based views, you must have a running PHP-FPM service. This mode proxies GET requests for authentication pages to your PHP-FPM service, while POST requests (like login submissions) are handled by the Go backend. + +#### PHP Frontend Setup + +If you choose `FRONTEND_TYPE=php`, follow these steps: + +1. **Set Frontend Type in `.env`**: + ```dotenv + FRONTEND_TYPE=php + ``` + +2. **Configure PHP-FPM Connection in `.env`**: + You need to provide the path to the PHP-FPM socket and the document root. + - `PHP_FPM_SOCKET`: The path to the PHP-FPM socket file (e.g., `/run/php-fpm/php-fpm.sock`). + - `PHP_DOC_ROOT`: The **absolute path** to the directory containing the PHP view files (e.g., `login.php`). This path must be accessible *by the PHP-FPM process*. + +3. **Copy PHP Files and Set Permissions**: + The user that the PHP-FPM service runs as needs read access to the PHP template files. + + First, determine the PHP-FPM user. You can often find this in your PHP-FPM pool configuration file (e.g., `/etc/php/8.3/fpm/pool.d/www.conf`) or by checking the running processes: + ```sh + # Example command to find the PHP-FPM user + ps aux | grep php-fpm + ``` + Common users are `www-data`, `http`, or `apache`. + + Next, copy the `templates/php` directory to a location accessible by PHP-FPM (e.g., `/var/www/html/auth-proxy`) and set the correct ownership. + + ```sh + # Example setup, assuming the user is 'http' + sudo cp -r templates/php /var/www/html/auth-proxy/php + sudo chown -R http:http /var/www/html/auth-proxy + ``` + + Finally, update your `.env` file with the correct paths: + ```dotenv + # --- PHP Frontend Settings --- + FRONTEND_TYPE=php + PHP_FPM_SOCKET=/run/php-fpm/php-fpm.sock + PHP_DOC_ROOT=/var/www/html/auth-proxy/php + ``` + ### Database Setup Examples If you are using PostgreSQL or MySQL, you will need to create a database and a user for the proxy. @@ -216,21 +318,31 @@ Authorization: Bearer <access_token> The `refresh_token` is a long-lived token that can be used to obtain a new access token once the old one expires. To do this, send a `POST` request to the `/auth/refresh` endpoint. -**Note:** The token lifetimes (`ACCESS_TOKEN_DURATION_MINUTES`, `REFRESH_TOKEN_DURATION_DAYS`) can be configured in your `.env` file. +**Note:** The token lifetimes can be configured in your `.env` file. See the configuration table for details. ## Configuration Details -| Environment Variable | Description | Default Value | -| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| `TARGET_URL` | **(Required)** The URL of the backend service to proxy to. | - | -| `SESSION_SECRET` | **(Required)** A long, random secret key for encrypting session cookies. | `default-secret-key-for-dev` | -| `DATABASE_URL` | The connection string for PostgreSQL or MySQL. If set, this overrides `DATABASE_PATH`. | - | -| `DATABASE_PATH` | The file path for the SQLite database. Used only if `DATABASE_URL` is not set. | `./auth.db` | -| `MAX_LOGIN_ATTEMPTS` | Number of failed login attempts before an account is locked. | `5` | -| `LOCKOUT_DURATION_MINUTES` | Duration in minutes for how long an account remains locked. | `10` | -| `USER_CREATION_RATE_LIMIT_MAX_REQUESTS` | Max number of user registrations allowed from a single IP within the time window. | `5` | -| `USER_CREATION_RATE_LIMIT_WINDOW_SECONDS` | The time window in seconds for the user creation rate limit. | `3600` (1 hour) | -| `PASSWORD_POLICY` | Sets the password strength requirements. See the "Password Policies" section for details. | `standard` | -| `AUTH_PATH_*` | A set of variables to customize the internal URLs for login, admin, etc., to prevent conflicts with the backend. See `.env` example. | Various, e.g., `/login`| -| `AUTH_ASSETS_PATH` | The URL path for serving internal static assets (CSS, JS). | `/auth-proxy-assets` | - +| Environment Variable | Description | Default Value | +| --- | --- | --- | +| `TARGET_URL` | **(Required)** The URL of the backend service to proxy to. | - | +| `SESSION_SECRET` | **(Required)** A long, random secret key for encrypting session cookies and signing JWTs. | `default-secret-key-for-dev` | +| `DATABASE_URL` | The connection string for PostgreSQL or MySQL. If set, this overrides `DATABASE_PATH`. | - | +| `DATABASE_PATH` | The file path for the SQLite database. Used only if `DATABASE_URL` is not set. | `./auth.db` | +| `LOG_LEVEL` | Sets the logging level. Can be `DEBUG`, `INFO`, `WARN`, `ERROR`. | `INFO` | +| `FRONTEND_TYPE` | Switches the frontend rendering mode. Can be `js` or `php`. | `js` | +| `PHP_FPM_SOCKET` | The file path to the PHP-FPM socket. Used only when `FRONTEND_TYPE` is `php`. | `/run/php-fpm/php-fpm.sock` | +| `PHP_DOC_ROOT` | The absolute path to the PHP files directory. Used only when `FRONTEND_TYPE` is `php`. | - | +| `MAX_LOGIN_ATTEMPTS` | Number of failed login attempts before an account is locked. | `5` | +| `LOCKOUT_DURATION_MINUTES` | Duration in minutes for how long an account remains locked. | `10` | +| `USER_CREATION_RATE_LIMIT_MAX_REQUESTS` | Max number of user registrations allowed from a single IP within the time window. | `5` | +| `USER_CREATION_RATE_LIMIT_WINDOW_SECONDS` | The time window in seconds for the user creation rate limit. | `3600` (1 hour) | +| `PASSWORD_POLICY` | Sets the password strength requirements (`none`, `standard`, `enhanced`, `strict`). | `standard` | +| `TOKEN_DURATION_HOURS` | The duration in hours for the web session cookie. | `24` | +| `ACCESS_TOKEN_DURATION_MINUTES` | The duration in minutes for a JWT access token for API clients. | `15` | +| `REFRESH_TOKEN_DURATION_DAYS` | The duration in days for a JWT refresh token for API clients. | `7` | +| `PROTECT_FRONTEND` | If `true`, proxies all frontend paths through session authentication. | `false` | +| `PROTECT_API` | If `true`, proxies all API paths through bearer token authentication. | `false` | +| `FRONT_PATH` | The base path for frontend routes to be protected by `PROTECT_FRONTEND`. | `/` | +| `API_PATH` | The base path for API routes to be protected by `PROTECT_API`. | `/api/` | +| `AUTH_PATH_*` | A set of variables to customize the internal URLs for login, admin, etc. | Various, e.g., `/login`| +| `AUTH_ASSETS_PATH` | The URL path for serving internal static assets (CSS, JS). | `/assets` |
\ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 098f6b2..15ba62b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -40,23 +40,64 @@ func main() { mux := http.NewServeMux() // --- Static file serving --- - assetsPath := config.Paths.Assets + "/" + assetsPath := strings.TrimRight(config.Paths.Assets, "/") + "/" assetsFS := http.FileServer(http.Dir("templates")) mux.Handle(assetsPath, http.StripPrefix(assetsPath, assetsFS)) - // --- Public Auth Routes --- + // --- Frontend selection and routing --- + frontendType := os.Getenv("FRONTEND_TYPE") rateLimiter := middleware.NewRateLimiter() - mux.HandleFunc(config.Paths.Login, handlers.LoginHandler) - mux.Handle(config.Paths.Register, middleware.RateLimitMiddleware(rateLimiter)(http.HandlerFunc(handlers.RegisterHandler))) + + // Helper to switch handler based on HTTP method + methodSwitch := func(get, post http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + post.ServeHTTP(w, r) + } else { + get.ServeHTTP(w, r) + } + }) + } + + if frontendType == "php" { + // Create specific handlers for each PHP page + loginPageHandler := handlers.NewPhpProxyHandler("login.php") + registerPageHandler := handlers.NewPhpProxyHandler("register.php") + accountPageHandler := handlers.NewPhpProxyHandler("account.php") + accountPasswordPageHandler := handlers.NewPhpProxyHandler("account_password.php") + + // For /login, GET goes to PHP, POST goes to Go's login logic + loginHandler := methodSwitch(loginPageHandler, http.HandlerFunc(handlers.LoginHandler)) + mux.Handle(config.Paths.Login, loginHandler) + + // For /register, GET goes to PHP, POST goes to Go's register logic + registerPostHandler := middleware.RateLimitMiddleware(rateLimiter)(http.HandlerFunc(handlers.RegisterHandler)) + registerHandler := methodSwitch(registerPageHandler, registerPostHandler) + mux.Handle(config.Paths.Register, registerHandler) + + // For account pages, GET goes to PHP, POST (for password change) goes to Go + accountPasswordPostHandler := http.HandlerFunc(handlers.ChangePasswordHandler) + accountPasswordHandler := methodSwitch(accountPasswordPageHandler, accountPasswordPostHandler) + mux.Handle(config.Paths.AccountPassword, middleware.SessionAuth(accountPasswordHandler)) + mux.Handle(config.Paths.Account, middleware.SessionAuth(accountPageHandler)) // This page is GET only + + // Admin page still uses Go templates + mux.Handle(config.Paths.Admin, middleware.SessionAuth(middleware.AdminMiddleware(http.HandlerFunc(handlers.AdminPageHandler)))) + } else { + // --- Public Auth Routes (Go templates) --- + mux.HandleFunc(config.Paths.Login, handlers.LoginHandler) + mux.Handle(config.Paths.Register, middleware.RateLimitMiddleware(rateLimiter)(http.HandlerFunc(handlers.RegisterHandler))) + // --- Auth HTML pages (Protected by SessionAuth, Go templates) --- + mux.Handle(config.Paths.Account, middleware.SessionAuth(http.HandlerFunc(handlers.AccountPageHandler))) + mux.Handle(config.Paths.Admin, middleware.SessionAuth(middleware.AdminMiddleware(http.HandlerFunc(handlers.AdminPageHandler)))) + mux.Handle(config.Paths.AccountPassword, middleware.SessionAuth(http.HandlerFunc(handlers.ChangePasswordHandler))) + } + + // --- Common Routes --- mux.HandleFunc(config.Paths.Logout, handlers.LogoutHandler) mux.HandleFunc("/auth/refresh", handlers.RefreshTokenHandler) mux.HandleFunc("/api/auth/token", handlers.TokenHandler) - // --- Auth HTML pages (Protected by SessionAuth) --- - mux.Handle(config.Paths.Account, middleware.SessionAuth(http.HandlerFunc(handlers.AccountPageHandler))) - mux.Handle(config.Paths.Admin, middleware.SessionAuth(middleware.AdminMiddleware(http.HandlerFunc(handlers.AdminPageHandler)))) - mux.Handle(config.Paths.AccountPassword, middleware.SessionAuth(http.HandlerFunc(handlers.ChangePasswordHandler))) - // --- Protected API Endpoints (for Admin UI, protected by SessionAuth) --- adminUsersAPI := http.HandlerFunc(handlers.GetUsersHandler) adminUpdateRoleAPI := http.HandlerFunc(handlers.UpdateUserRoleHandler) @@ -107,8 +148,8 @@ func main() { }) // --- Start Server --- - logging.AppLog.Info("Server starting on :8081") - if err := http.ListenAndServe(":8081", finalHandler); err != nil { + logging.AppLog.Info("Server starting on :8080") + if err := http.ListenAndServe(":8080", finalHandler); err != nil { logging.AppLog.Error("Server failed to start", "error", err) os.Exit(1) } diff --git a/config/config.go b/config/config.go index 236ed41..a73bf23 100644 --- a/config/config.go +++ b/config/config.go @@ -42,7 +42,7 @@ func Init() { AccountPassword: getEnv("AUTH_PATH_ACCOUNT_PASSWORD", "/account/password"), Admin: getEnv("AUTH_PATH_ADMIN", "/admin"), AdminUsersAPI: getEnv("AUTH_PATH_ADMIN_USERS_API", "/api/admin/users"), - Assets: getEnv("AUTH_ASSETS_PATH", "/auth-proxy-assets"), + Assets: getEnv("AUTH_ASSETS_PATH", "/assets"), ProtectFrontend: getEnvAsBool("PROTECT_FRONTEND", false), ProtectAPI: getEnvAsBool("PROTECT_API", false), FrontPath: getEnv("FRONT_PATH", "/"), @@ -10,4 +10,7 @@ require ( golang.org/x/crypto v0.43.0 ) -require filippo.io/edwards25519 v1.1.0 // indirect +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19 // indirect +) @@ -8,5 +8,7 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19 h1:ZCmSnT6CLGhfoQ2lPEhL4nsJstKDCw1F1RfN8/smTCU= +github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19/go.mod h1:SXTY+QvI+KTTKXQdg0zZ7nx0u94QWh8ZAwBQYsW9cqk= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= diff --git a/handlers/php_proxy.go b/handlers/php_proxy.go new file mode 100644 index 0000000..98b62ba --- /dev/null +++ b/handlers/php_proxy.go @@ -0,0 +1,128 @@ +package handlers + +import ( + "auth-proxy/config" + "auth-proxy/logging" + "bytes" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + + fcgiclient "github.com/tomasen/fcgi_client" +) + +// NewPhpProxyHandler creates a handler that proxies requests to a specific PHP script. +func NewPhpProxyHandler(scriptName string) http.Handler { + phpFpmSocket := os.Getenv("PHP_FPM_SOCKET") + if phpFpmSocket == "" { + phpFpmSocket = "/run/php-fpm/php-fpm.sock" // Default value + } + + // Get doc root from env for container support. + docRoot := os.Getenv("PHP_DOC_ROOT") + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fcgi, err := fcgiclient.Dial("unix", phpFpmSocket) + if err != nil { + http.Error(w, "Failed to connect to php-fpm: "+err.Error(), http.StatusInternalServerError) + return + } + defer fcgi.Close() + + env := make(map[string]string) + + var scriptFilename string + var finalDocRoot string + + if docRoot != "" { + // PHP_DOC_ROOT is set, for containerized environments. + finalDocRoot = docRoot + scriptFilename = filepath.Join(finalDocRoot, scriptName) + } else { + // Fallback to local development logic. + localDocRoot, err := filepath.Abs("templates/php") + if err != nil { + http.Error(w, "Could not resolve templates/php path", http.StatusInternalServerError) + return + } + finalDocRoot = localDocRoot + scriptFilename = filepath.Join(finalDocRoot, scriptName) + + // Verify file exists for local dev. + if _, err := os.Stat(scriptFilename); os.IsNotExist(err) { + http.NotFound(w, r) + return + } + } + + env["SCRIPT_FILENAME"] = scriptFilename + env["DOCUMENT_ROOT"] = finalDocRoot + + // Pass path config to PHP + env["AUTH_PATH_LOGIN"] = config.Paths.Login + env["AUTH_PATH_REGISTER"] = config.Paths.Register + env["AUTH_PATH_LOGOUT"] = config.Paths.Logout + env["AUTH_PATH_ACCOUNT"] = config.Paths.Account + env["AUTH_PATH_ACCOUNT_PASSWORD"] = config.Paths.AccountPassword + env["AUTH_PATH_ADMIN"] = config.Paths.Admin + env["AUTH_PATH_ADMIN_USERS_API"] = config.Paths.AdminUsersAPI + env["AUTH_ASSETS_PATH"] = config.Paths.Assets + + + logging.AppLog.Debug("Proxying to PHP-FPM", "script_filename", scriptFilename, "doc_root", finalDocRoot) + + env["REQUEST_METHOD"] = r.Method + env["SERVER_SOFTWARE"] = "go / auth-proxy" + env["REMOTE_ADDR"] = r.RemoteAddr + env["SERVER_PROTOCOL"] = r.Proto + env["QUERY_STRING"] = r.URL.RawQuery + env["REQUEST_URI"] = r.RequestURI + env["SCRIPT_NAME"] = r.URL.Path + for name, headers := range r.Header { + name = "HTTP_" + strings.ToUpper(strings.Replace(name, "-", "_", -1)) + env[name] = strings.Join(headers, ", ") + } + + var body io.Reader + if r.Body != nil { + bodyBytes, _ := ioutil.ReadAll(r.Body) + env["CONTENT_LENGTH"] = strconv.Itoa(len(bodyBytes)) + body = bytes.NewReader(bodyBytes) + } else { + env["CONTENT_LENGTH"] = "0" + } + env["CONTENT_TYPE"] = r.Header.Get("Content-Type") + + resp, err := fcgi.Request(env, body) + if err != nil { + http.Error(w, "FastCGI request failed: "+err.Error(), http.StatusInternalServerError) + return + } + + // Copy headers + for k, vv := range resp.Header { + for _, v := range vv { + w.Header().Add(k, v) + } + } + + // Check for PHP errors communicated via status header (a common pattern) + if status := resp.Header.Get("Status"); strings.HasPrefix(status, "404") { + http.NotFound(w,r) + return + } + + statusCode := resp.StatusCode + if statusCode == 0 { + statusCode = http.StatusOK // Default to 200 OK if not specified + } + w.WriteHeader(statusCode) + + // Copy body + io.Copy(w, resp.Body) + }) +}
\ No newline at end of file diff --git a/logging/logging.go b/logging/logging.go index 8a2d0ee..9e6f8bf 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -20,13 +20,18 @@ func InitLoggers() { appWriter := os.Stdout securityWriter := os.Stdout + logLevel := slog.LevelInfo + if strings.ToUpper(os.Getenv("LOG_LEVEL")) == "DEBUG" { + logLevel = slog.LevelDebug + } + AppLog = slog.New(slog.NewTextHandler(appWriter, &slog.HandlerOptions{ - Level: slog.LevelInfo, + Level: logLevel, AddSource: false, // Include source file and line number for app logs })) SecurityLog = slog.New(slog.NewTextHandler(securityWriter, &slog.HandlerOptions{ - Level: slog.LevelInfo, + Level: logLevel, // The default TextHandler format is `time=... level=... msg=... key=value`. // This is structured and easily parsable, so we'll stick with it. })) diff --git a/middleware/auth.go b/middleware/auth.go index 45fa864..e4f096b 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -52,7 +52,10 @@ func SessionAuth(next http.Handler) http.Handler { return } + // Add user info to context and headers for downstream handlers ctx := context.WithValue(r.Context(), types.SessionDataContextKey, user) + r.Header.Set("X-Username", user.Username) + r.Header.Set("X-User-Role", user.Role) next.ServeHTTP(w, r.WithContext(ctx)) }) } @@ -151,4 +154,4 @@ func AdminMiddleware(next http.Handler) http.Handler { logging.SecurityLog.Info("ADMIN ACCESS GRANTED", "user", user.Username, "path", r.URL.Path, "ip", ip) next.ServeHTTP(w, r) }) -} +}
\ No newline at end of file diff --git a/templates/php/account.php b/templates/php/account.php new file mode 100644 index 0000000..c57455e --- /dev/null +++ b/templates/php/account.php @@ -0,0 +1,29 @@ +<?php + $paths = [ + 'AccountPassword' => $_SERVER['AUTH_PATH_ACCOUNT_PASSWORD'] ?? '/account/password', + 'Logout' => $_SERVER['AUTH_PATH_LOGOUT'] ?? '/logout', + 'Assets' => $_SERVER['AUTH_ASSETS_PATH'] ?? '/assets', + ]; +?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>My Account</title> + <link rel="stylesheet" href="<?php echo htmlspecialchars($paths['Assets']); ?>/css/style.css"> + <link rel="stylesheet" href="<?php echo htmlspecialchars($paths['Assets']); ?>/css/account.css"> +</head> +<body> + <div class="container content-container"> + <h2>My Account</h2> + <p>Welcome, <?php echo htmlspecialchars($_SERVER['HTTP_X_USERNAME']); ?>!</p> + <ul> + <li><a href="<?php echo htmlspecialchars($paths['AccountPassword']); ?>">Change Password</a></li> + </ul> + <div class="logout-link"> + <a href="<?php echo htmlspecialchars($paths['Logout']); ?>">Logout</a> + </div> + </div> +</body> +</html>
\ No newline at end of file diff --git a/templates/php/account_password.php b/templates/php/account_password.php new file mode 100644 index 0000000..b287c90 --- /dev/null +++ b/templates/php/account_password.php @@ -0,0 +1,42 @@ +<?php + $paths = [ + 'Account' => $_SERVER['AUTH_PATH_ACCOUNT'] ?? '/account', + 'AccountPassword' => $_SERVER['AUTH_PATH_ACCOUNT_PASSWORD'] ?? '/account/password', + 'Assets' => $_SERVER['AUTH_ASSETS_PATH'] ?? '/assets', + ]; +?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Change Password</title> + <link rel="stylesheet" href="<?php echo htmlspecialchars($paths['Assets']); ?>/css/style.css"> + <link rel="stylesheet" href="<?php echo htmlspecialchars($paths['Assets']); ?>/css/account.css"> +</head> +<body> + <div class="container content-container"> + <h2>Change Password</h2> + <form id="change-password-form" action="<?php echo htmlspecialchars($paths['AccountPassword']); ?>" method="post"> + <div class="form-group"> + <label for="current_password">Current Password</label> + <input type="password" id="current_password" name="current_password" required> + </div> + <div class="form-group"> + <label for="new_password">New Password</label> + <input type="password" id="new_password" name="new_password" required> + </div> + <div class="form-group"> + <label for="confirm_new_password">Confirm New Password</label> + <input type="password" id="confirm_new_password" name="confirm_new_password" required> + </div> + <button type="submit">Update Password</button> + </form> + <p id="message-placeholder" class="message"></p> + <div class="back-link"> + <a href="<?php echo htmlspecialchars($paths['Account']); ?>">Back to Account</a> + </div> + </div> + <script src="<?php echo htmlspecialchars($paths['Assets']); ?>/js/auth.js"></script> +</body> +</html>
\ No newline at end of file diff --git a/templates/php/login.php b/templates/php/login.php new file mode 100644 index 0000000..f3e5113 --- /dev/null +++ b/templates/php/login.php @@ -0,0 +1,42 @@ +<?php + $paths = [ + 'Login' => $_SERVER['AUTH_PATH_LOGIN'] ?? '/login', + 'Register' => $_SERVER['AUTH_PATH_REGISTER'] ?? '/register', + 'Assets' => $_SERVER['AUTH_ASSETS_PATH'] ?? '/assets', + ]; +?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Login</title> + <link rel="stylesheet" href="<?php echo htmlspecialchars($paths['Assets']); ?>/css/style.css"> +</head> +<body> + <div class="container form-container"> + <h2>Login Required</h2> + <form id="login-form" action="<?php echo htmlspecialchars($paths['Login']); ?>" method="post"> + <div class="form-group"> + <label for="username">Username</label> + <input type="text" id="username" name="username" required> + </div> + <div class="form-group"> + <label for="password">Password</label> + <input type="password" id="password" name="password" required> + </div> + <button type="submit">Login</button> + </form> + <p class="error"> + <?php + if (!empty($_GET['error'])) { + echo htmlspecialchars($_GET['error']); + } + ?> + </p> + <div style="text-align: center; margin-top: 1rem;"> + <a href="<?php echo htmlspecialchars($paths['Register']); ?>">Don't have an account? Register</a> + </div> + </div> +</body> +</html>
\ No newline at end of file diff --git a/templates/php/register.php b/templates/php/register.php new file mode 100644 index 0000000..956467a --- /dev/null +++ b/templates/php/register.php @@ -0,0 +1,46 @@ +<?php + $paths = [ + 'Login' => $_SERVER['AUTH_PATH_LOGIN'] ?? '/login', + 'Register' => $_SERVER['AUTH_PATH_REGISTER'] ?? '/register', + 'Assets' => $_SERVER['AUTH_ASSETS_PATH'] ?? '/assets', + ]; +?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Register</title> + <link rel="stylesheet" href="<?php echo htmlspecialchars($paths['Assets']); ?>/css/style.css"> +</head> +<body> + <div class="container form-container"> + <h2>Create Account</h2> + <form id="register-form" action="<?php echo htmlspecialchars($paths['Register']); ?>" method="post"> + <div class="form-group"> + <label for="username">Username</label> + <input type="text" id="username" name="username" required> + </div> + <div class="form-group"> + <label for="password">Password</label> + <input type="password" id="password" name="password" required> + </div> + <div class="form-group"> + <label for="confirm_password">Confirm Password</label> + <input type="password" id="confirm_password" name="confirm_password" required> + </div> + <button type="submit" class="btn-success">Register</button> + </form> + <p class="error"> + <?php + if (!empty($_GET['error'])) { + echo htmlspecialchars($_GET['error']); + } + ?> + </p> + <div class="form-link"> + <a href="<?php echo htmlspecialchars($paths['Login']); ?>">Already have an account? Login</a> + </div> + </div> +</body> +</html>
\ No newline at end of file |
