Files
alterminal/README.md
T
2026-10-03 12:37:38 +08:00

392 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# alterminal
輕量級單一登入(SSO)服務,實作 [OpenID Connect](https://openid.net/connect/) 協定。alterminal 扮演 **OpenID Provider(OP / Identity Provider)**,讓多個應用程式(Relying Party, RP)透過標準協定完成身分認證,實現「登入一次,處處可用」。
> **狀態:開發中。** 目前完成專案骨架(HTTP 服務、資料庫連線、健康檢查)、使用者帳號(CLI 建帳與重設密碼、argon2id 密碼雜湊、網頁自助更新密碼)、登入/登出(HTML 登入頁+JSON API、Session Cookie)、應用程式與金鑰管理頁,以及 OIDC 核心(Discovery、Authorization Code Flow+PKCE、Token 端點、UserInfo、Refresh Token 輪替、RP-Initiated Logout),其餘功能依下方 Roadmap 推進。
## 特色
- **標準 OIDC Provider**
- Discovery(`/.well-known/openid-configuration`)與 JWKS(`/.well-known/jwks.json`)
- 以 RS256 簽發 ID Token,支援金鑰輪替(rotation)
- **OAuth 2.0 / OIDC 授權流程**
- Authorization Code Flow + PKCE(RFC 7636),支援機密式(confidential)與公開式(public)Client
- Refresh Token(含輪替與撤銷)
- Client Credentials Grant(機器對機器情境)
- **單一登入/單一登出**
- 已登入使用者在瀏覽器 Session 有效期間內,可直接通過新 RP 的授權請求
- RP-Initiated Logout(OIDC Front-/Back-Channel Logout 列於 Roadmap)
- **技術棧單純**:Go + [chi](https://github.com/go-chi/chi) + [GORM](https://gorm.io) + PostgreSQL + [Tailwind CSS](https://tailwindcss.com)(建置產物內嵌),單一執行檔即可部署
## 支援的 Scope
| Scope | 說明 |
| --- | --- |
| `openid` | 必選。要求簽發 ID Token |
| `profile` | 使用者基本資料(`name` 等 claim) |
| `email` | 使用者 Email(`email`、`email_verified`) |
| `offline_access` | 簽發 Refresh Token |
## 端點一覽
| 端點 | 方法 | 說明 | 狀態 |
| --- | --- | --- | --- |
| `/health` | GET | 健康檢查(含資料庫連線檢測) | ✅ 已完成 |
| `/.well-known/openid-configuration` | GET | OIDC Discovery 文件(端點與能力中繼資料,`Cache-Control: max-age=3600`) | ✅ 已完成 |
| `/.well-known/jwks.json` | GET | Token 簽署用公開金鑰(JWKS,僅發佈使用中金鑰,`Cache-Control: max-age=3600`) | ✅ 已完成 |
| `/login` | POST | 使用者登入(JSON API 與 HTML 表單提交;表單支援 `next` 返回路徑) | ✅ 已完成 |
| `/login` | GET | 使用者登入頁(HTML 表單,供 `/authorize` 導向並以 `next` 攜回授權請求) | ✅ 已完成 |
| `/logout` | POST | 使用者登出(HTML 表單與 JSON API,冪等) | ✅ 已完成 |
| `/logout` | GET | RP-Initiated Logout(`id_token_hint` 驗證、無 hint 或不符目前 Session 時顯示確認頁;`post_logout_redirect_uri` 精確比對註冊值後重導並回填 `state`) | ✅ 已完成 |
| `/password` | GET | 更新密碼頁(登入者自助變更;未登入導向 `/login?next=/password`) | ✅ 已完成 |
| `/password` | POST | 更新密碼(驗證目前密碼;表單+CSRF 與 JSON API。成功後於交易內撤銷全部 Session,再輪替目前瀏覽器的 Session——其他裝置立即登出) | ✅ 已完成 |
| `/static/*` | GET | 靜態檔(Tailwind 建置輸出的 CSS,`go:embed` 內嵌) | ✅ 已完成 |
| `/authorize` | GET | 授權端點(Authorization Code Flow+PKCE;未登入導向 `/login?next=...`,首次授權顯示同意頁) | ✅ 已完成 |
| `/authorize` | POST | 同意頁決定(同意記錄於 `consents`,同範圍之後靜默通過;拒絕回 `access_denied`) | ✅ 已完成 |
| `/token` | POST | 權杖端點(`authorization_code`+PKCE 與 `refresh_token` 兩種 grant;Basic/POST client 認證) | ✅ 已完成 |
| `/userinfo` | GET/POST | 以 Bearer Access Token 取得使用者 claims(依授權 scope) | ✅ 已完成 |
| `/admin/applications` | GET | 應用程式管理頁(RP 註冊列表;僅 admin) | ✅ 已完成 |
| `/admin/applications/new` | GET | 註冊新應用程式頁(獨立表單頁;僅 admin) | ✅ 已完成 |
| `/admin/applications/new` | POST | 註冊應用程式(明文 client_secret 僅於本次回應顯示一次,表單+CSRF) | ✅ 已完成 |
| `/admin/applications/{id}` | GET | 編輯應用程式頁(預填既有註冊內容;client_id 唯讀,僅 admin) | ✅ 已完成 |
| `/admin/applications/{id}` | POST | 更新應用程式註冊內容(表單+CSRF,PRG;client_id 與 secret 不變,改為公開式時清除 secret 雜湊) | ✅ 已完成 |
| `/admin/applications/{id}/secret` | POST | 輪替 client secret(舊 secret 立即失效,明文僅顯示一次) | ✅ 已完成 |
| `/admin/applications/{id}/delete` | POST | 刪除應用程式註冊(表單+CSRF,PRG) | ✅ 已完成 |
| `/admin/keys` | GET | 金鑰管理頁(kid、狀態、輪替操作;僅 admin) | ✅ 已完成 |
| `/admin/keys` | POST | 產生新 RSA 簽章金鑰(表單+CSRF,PRG) | ✅ 已完成 |
| `/admin/keys/{id}/retire` | POST | 退休金鑰(最後一把使用中金鑰不可退休) | ✅ 已完成 |
| `*`(未匹配路徑) | 任意 | 自訂 404 頁(HTML,不分方法;`/static/` 下不存在的檔案仍由檔案伺服器回純文字 404) | ✅ 已完成 |
## 快速開始
### 前置需求
- Go 1.26+
- PostgreSQL 14+(或直接用 Docker)
### 啟動資料庫
```bash
docker run -d --name alterminal-db \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=alterminal \
-p 5432:5432 \
postgres:17
```
### 啟動服務
```bash
go run ./cmd/alterminal
# 服務啟動於 http://localhost:8080
```
### 驗證
```bash
curl http://localhost:8080/health
# => ok
```
### 編譯執行檔
```bash
go build -o alterminal ./cmd/alterminal
./alterminal
# 服務啟動於 http://localhost:8080
```
- HTML 模板與 Tailwind 建置輸出(`main.css`)皆以 `go:embed` 內嵌,產出為**單一執行檔**,部署時不需連同模板與樣式檔一併安裝
- 依賴全為純 Go(PostgreSQL 驅動採 pgx,無 CGO),可直接交叉編譯。部署至 Linux 伺服器:
```bash
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o alterminal ./cmd/alterminal
# ARM 伺服器改用 GOARCH=arm64
```
- 編譯前建議先跑測試:`go test ./...`
### 帳號管理 CLI
帳號相關操作由 CLI 子指令完成,共通行為:
- 不需啟動伺服器,指令會自行連線資料庫(連線參數同 `DB_*` 環境變數)
- 密碼一律以 argon2id 重新雜湊(每次產生新 salt),明文不落地
- 密碼至少 8 字元;省略 `-password` 旗標時於終端機無回顯輸入兩次,非互動環境(cron、CI)必須提供旗標
- 失敗時以 `log.Fatal` 結束(exit code 1);未知子指令 exit code 2 並列出用法
| 子指令 | 說明 |
| --- | --- |
| `create-account` | 建立使用者帳號 |
| `update-password` | 重設帳號密碼(管理用途,不驗證舊密碼),成功後撤銷該帳號所有 Session |
### 建立使用者帳號
```bash
go run ./cmd/alterminal create-account -username alice -email alice@example.com -name "Alice"
輸入密碼: ********
再次輸入密碼: ********
帳號建立成功:id=1 username=alice email=alice@example.com role=user
```
| 旗標 | 說明 |
| --- | --- |
| `-username` | 登入帳號(必填、唯一;僅英數與 `. _ -`,最長 64) |
| `-email` | Email(必填、唯一) |
| `-name` | 顯示名稱(選填) |
| `-role` | 角色(選填,`admin` 或 `user`,預設 `user`) |
| `-email-verified` | 將 Email 標記為已驗證(選填,預設 `false`) |
| `-password` | 密碼(至少 8 字元;省略時於終端機無回顯輸入兩次,非互動環境必須提供) |
### 更新密碼
管理用密碼重設(不驗證舊密碼)。密碼更新與 Session 撤銷於**同一資料庫交易**內完成,成功後該帳號所有 Session 立即失效——對 SSO Provider 而言,若密碼重設(例如帳號外洩的處置)後既有 Session 仍繼續有效,重設便失去意義:
```bash
go run ./cmd/alterminal update-password -username alice
輸入密碼: ********
再次輸入密碼: ********
密碼更新成功:id=1 username=alice(已撤銷 2 個 Session)
```
| 旗標 | 說明 |
| --- | --- |
| `-username` | 要重設密碼的帳號(必填) |
| `-password` | 新密碼(至少 8 字元;省略時於終端機無回顯輸入兩次,非互動環境必須提供) |
錯誤情境(exit code 1,訊息前綴 `update-password: `):
| 錯誤訊息 | 情境 |
| --- | --- |
| `username 不可為空` | 未提供 `-username`,或值僅空白 |
| `username "alice" 不存在` | 查無該帳號 |
| `密碼長度至少 8 字元` | 新密碼過短 |
| `兩次輸入的密碼不一致` | 終端機兩次輸入不同 |
| `非互動環境無法提示輸入密碼,請以 -password 提供` | 省略 `-password` 且 stdin 非終端機 |
### 登入 API
`POST /login` 以 JSON 驗證帳密,成功時建立瀏覽器 Session 並以 `Set-Cookie` 下發(`alterminal_session`,HttpOnly、SameSite=Lax,效期 24 小時):
```bash
curl -i -X POST http://localhost:8080/login \
-H 'Content-Type: application/json' \
-d '{"username":"alice","password":"sup3r-secret"}'
```
成功(200):
```json
{
"user": {"id": 1, "username": "alice", "email": "alice@example.com", "email_verified": false, "name": "Alice"},
"expires_at": "2026-10-03T09:00:00Z"
}
```
錯誤回應皆為 `{"error": "..."}`:
| 狀態碼 | 情境 |
| --- | --- |
| `400` | JSON 無法解析、`username`/`password` 缺漏 |
| `401` | 帳號不存在或密碼錯誤(訊息一致,不洩漏帳號是否存在;查無帳號時仍執行等時的 argon2 比對) |
| `415` | `Content-Type` 非 `application/json` |
### 登出 API
`POST /logout` 刪除資料庫中的 Session 並以 `Max-Age=0` 清除瀏覽器 Cookie。與 `/login` 相同依 `Content-Type` 分流,登出為**冪等**操作——查無有效 Session 亦視為成功:
```bash
curl -i -X POST http://localhost:8080/logout \
-H 'Content-Type: application/json' \
-b 'alterminal_session=<Session ID>'
# => 204 No Content,Set-Cookie 以 Max-Age=0 清除 alterminal_session
```
- **JSON 流程**:成功回 `204`,無回應內容
- **表單流程**(瀏覽器):需通過 double-submit CSRF 驗證(與登入表單同一機制),成功後 `303` 導向 `/login`(PRG);CSRF 不符回 `403` 並重繪目前狀態頁
- 資料庫刪除失敗僅記錄,仍完成 Cookie 清除(Session 最遲於效期到期自動失效)
- 跨應用程式單一登出(Front-/Back-Channel Logout)列於 Roadmap
### 更新密碼
`GET/POST /password` 為登入者自助更新密碼(`/` 帳號首頁有入口連結),須持有效 Session;未登入時表單流程導向 `/login?next=/password`,JSON 流程回 `401`。與 `/login`、`/logout` 相同依 `Content-Type` 分流(JSON 與表單+CSRF):
```bash
curl -i -X POST http://localhost:8080/password \
-H 'Content-Type: application/json' \
-b 'alterminal_session=<Session ID>' \
-d '{"current_password":"sup3r-secret","new_password":"n3w-secret!"}'
# => 204 No Content,Set-Cookie 下發輪替後的新 Session
```
- 驗證目前密碼無誤後,以 argon2id 重新雜湊新密碼(新 salt),密碼更新與撤銷該帳號**全部 Session** 於同一資料庫交易內完成(與 CLI `update-password` 共用邏輯)
- 交易成功後為目前瀏覽器重建 Session(輪替 Session ID,避免 fixation)——其他裝置立即登出、本瀏覽器保持登入
- 表單流程成功後 `303` 導回 `/password?saved=1` 顯示成功訊息(PRG);驗證失敗以對應狀態碼重繪表單,密碼欄位不回填
錯誤回應 JSON 流程為 `{"error": "..."}`(表單流程顯示於頁面):
| 狀態碼 | 情境 |
| --- | --- |
| `400` | 欄位缺漏、新密碼短於 8 字元、表單兩次輸入的新密碼不一致、新密碼與目前密碼相同 |
| `401` | 未登入(JSON 流程)或目前的密碼錯誤 |
| `403` | 表單 CSRF 驗證失敗 |
| `415` | `Content-Type` 非 `application/json` 或表單 |
### RP-Initiated Logout
`GET /logout` 實作 [OpenID Connect RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)(`POST` 亦支援,§2 要求),端點發佈於 Discovery 的 `end_session_endpoint`。RP 將使用者導向本端點並攜帶參數:
| 參數 | 說明 |
| --- | --- |
| `id_token_hint` | 先前取得的 ID Token。本服務驗證簽章與 `iss`(不檢查 `exp`——§2 要求 session 存在或近期存在時接受過期值),其 `aud` 用於辨識發起登出的 client;`sub` 用於比對目前 Session |
| `post_logout_redirect_uri` | 登出後返回 URI,須與該 client 註冊的 post_logout_redirect_uris **精確比對**(§3:未註冊值 MUST NOT 重導),於管理頁「登出後返回 URI」欄位註冊 |
| `client_id` | 無 `id_token_hint` 時辨識 client 用(供驗證返回 URI);與 hint 併用時須與其 `aud` 相符(§2) |
| `state` | 原值回填於重導查詢字串(§2) |
行為要點:
- **確認頁**(§2 MUST):未提供 `id_token_hint`、或 hint 的 `sub` 不屬於目前 Session 時,先顯示「您確定要登出嗎?」確認頁(CSRF 保護),避免跨站偽造的強迫登出;hint 屬於目前 Session 時直接登出。本服務 ID token 不含 `sid` claim,以 `sub` 比對 Session 使用者近似判斷
- **重導**:通過驗證時 `303` 至 `post_logout_redirect_uri`(附 `state`);指定但未通過註冊比對時**不重導**(§3),登出仍完成並顯示已登出頁與說明;未指定時 `303` 導向 `/login`(與本站既有登出一致)
- **錯誤**(§4):hint 無效、`client_id` 與 `aud` 不符等硬錯誤回 `400`,不執行登出也不重導
- 未登入時造訪為冪等操作——仍清除 Cookie 並依驗證結果重導
- 不帶 RP 參數的 `POST /logout`(側欄登出表單、JSON API)行為不變,由 `auth.LogoutHandler` 處理
### 登入頁面
瀏覽器開啟 <http://localhost:8080/login> 即為 HTML 登入頁(Go `html/template`,模板位於 `internal/auth/templates/`,以 `go:embed` 打進執行檔):
- 表單提交(`application/x-www-form-urlencoded`)與 JSON API 共用同一套帳密驗證與 Session 流程
- 表單附 double-submit CSRF token(Cookie 與隱藏欄位比對),不符時回 `403` 並重新輸出表單
- 帳密錯誤時重繪表單(`401`),保留帳號輸入並顯示錯誤訊息
- 登入成功採 PRG 模式:`303` 導向 `/login`,持有效 Session 時該頁顯示帳號資訊(帳號、Email、Session 到期時間),並套用側邊導覽版面(`internal/auth/templates/layout.html`,之後的頁面可重用):桌面版側欄固定展開,手機版以純 CSS checkbox 開合(CSP 不允許 JavaScript),側欄頁尾為使用者資訊與「登出」按鈕(`POST /logout`,同樣受 CSRF 驗證保護)
### 前端樣式(Tailwind CSS)
頁面樣式使用 Tailwind CSS v4。模板(`internal/auth/templates/*.html`)直接寫 utility class,樣式進入點在 `internal/auth/assets/css/input.css`(含 `@theme` 自訂品牌色與中文字型),建置輸出 `internal/auth/assets/css/main.css` 已提交並以 `go:embed` 內嵌,經 `/static/css/main.css` 提供——**一般開發與部署不需 Node**。
調整樣式後重新建置:
```bash
tools/tailwindcss -i internal/auth/assets/css/input.css -o internal/auth/assets/css/main.css --minify
```
Tailwind 為官方 [standalone CLI](https://tailwindlabs.github.io/tailwindcss/)(版本見 `tools/tailwindcss-version.txt`,`tools/` 不納入版本控制),首次取得方式:
```bash
mkdir -p tools
curl -sL -o tools/tailwindcss \
https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-macos-arm64
chmod +x tools/tailwindcss
```
### 環境變數
| 變數 | 說明 | 預設值 | 狀態 |
| --- | --- | --- | --- |
| `DB_HOST` | PostgreSQL 位址 | `localhost` | ✅ |
| `DB_PORT` | PostgreSQL 埠號 | `5432` | ✅ |
| `DB_USER` | 資料庫使用者 | `postgres` | ✅ |
| `DB_PASSWORD` | 資料庫密碼 | `postgres` | ✅ |
| `DB_NAME` | 資料庫名稱 | `alterminal` | ✅ |
| `PORT` | 服務監聽埠號 | `8080` | 🚧 規劃中 |
| `ISSUER` | OIDC Issuer URL(對外完整網址,須含 scheme,不可帶尾斜線;簽入 token 的 `iss` claim 與 Discovery 的 `issuer` 欄位須完全一致) | `http://localhost:8080` | ✅ |
## 授權流程(Authorization Code Flow + PKCE)
```mermaid
sequenceDiagram
participant U as 使用者(瀏覽器)
participant RP as 應用程式(RP)
participant OP as alterminal(OP)
RP->>U: 重新導向至 /authorize(附 client_id、redirect_uri、scope、code_challenge)
U->>OP: GET /authorize
OP->>U: 未登入 → 導向 /login
U->>OP: 輸入帳密,完成驗證並建立 Session
OP->>U: 確認授權後,攜帶 code 重新導向回 redirect_uri
U->>RP: 回呼 redirect_uri?code=...
RP->>OP: POST /token(附 code、client_id、client_secret、code_verifier)
OP-->>RP: Access Token、ID Token(JWT / RS256)、(可選)Refresh Token
RP->>OP: GET /userinfo(附 Access Token)
OP-->>RP: 使用者 Claims
RP-->>U: 登入完成
```
之後其他 RP 發起授權時,因瀏覽器 Session 仍有效,使用者無須再次輸入帳密,即為單一登入(SSO)。
### /authorize
- 參數:`response_type=code`、`client_id`、`redirect_uri`、`scope`(必含 `openid` 且不得超出應用程式註冊範圍)、`state`、`nonce`、`code_challenge`+`code_challenge_method=S256`(RFC 7636;公開式 Client 必須提供,不支援 `plain`)
- `client_id`/`redirect_uri` 無法確認時直接回 `400` 錯誤頁、不重導(RFC 6749 §4.1.2.1,防止作為開放重導向器);其餘錯誤以 `302` 重導回 `redirect_uri?error=...&state=...`
- 未登入:`303` 導向 `/login?next=<完整授權請求>`,登入成功後回到本端點繼續(`next` 經站內路徑驗證,阻擋 open redirect)
- 同意頁:首次授權顯示應用程式名稱與 scope 清單(表單+CSRF);同意記錄為 scope 聯集,之後請求範圍未擴大時靜默通過,範圍擴大時再次詢問;拒絕以 `access_denied` 重導回 RP
- 授權碼:32 bytes 亂數(資料庫僅存 SHA-256 雜湊)、效期 5 分鐘、一次性,發行當下的 redirect URI/scope/nonce/PKCE challenge/auth_time 隨碼凍結供兌換時逐項比對
### /token
- 僅接受 `POST`+`application/x-www-form-urlencoded`(RFC 6749 §2.3.1)
- Client 認證:HTTP Basic(`client_id:client_secret`)或表單欄位(`client_secret_post`);機密式 Client 必驗 secret(argon2id 比對),公開式 Client 以 client_id 識別、由 PKCE 承擔防護;認證失敗回 `401 invalid_client`
- `grant_type=authorization_code`:逐項比對授權碼綁定內容(client、redirect_uri、PKCE `S256(verifier)`);重用授權碼除回 `invalid_grant` 外並撤銷其發行的所有 refresh token
- `grant_type=refresh_token`:兌換即輪替(舊 token 作廢、發新 token),scope 僅可縮小不可擴大;偵測到重用已輪替的 token 時撤銷該使用者於該應用程式的全部 refresh token(OAuth 2.0 Security BCP)
- 回應:`access_token`(JWT/RS256,效期 15 分鐘、自包含不落庫)、`id_token`(scope 含 `openid` 時;含 `iss`/`sub`/`aud`/`nonce`/`auth_time` 及依 scope 的 `name`/`preferred_username`/`email`/`email_verified`)、`refresh_token`(scope 含 `offline_access` 時,效期 30 天)、`scope`(正規化排序形式);成功與錯誤回應皆附 `Cache-Control: no-store`
### /userinfo
- Bearer Access Token 取自 `Authorization` 標頭(RFC 6750;POST 亦接受表單 `access_token` 欄位)
- 驗證:RS256 簽章(header `kid` 對應 `signing_keys`,拒絕其他 `alg`)、`iss`、`exp`;無效回 `401`+`WWW-Authenticate: Bearer error="invalid_token"`,缺少 `openid` scope 回 `403 insufficient_scope`
- 回應依授權 scope:`sub` 恆有;`profile` 加 `name`/`preferred_username`;`email` 加 `email`/`email_verified`(OIDC Core §5.4)
## 資料模型
Access Token 採用自包含的 JWT,不落庫儲存;其餘狀態儲存於 PostgreSQL(GORM 自動遷移)。
| 資料表 | 說明 | 狀態 |
| --- | --- | --- |
| `users` | 使用者帳號(帳號、Email、密碼雜湊) | ✅ 已完成(含 argon2id 密碼雜湊) |
| `applications` | 已註冊的 RP 應用程式(client_id、client secret 雜湊、redirect URIs、登出後返回 URIs、grant types、scope、confidential/public) | 🚧 模型與管理頁已完成(`Application`:argon2id secret 雜湊、redirect URI 格式驗證;`/admin/applications` 註冊/編輯/輪替/刪除),註冊 API 規劃中 |
| `sessions` | 使用者瀏覽器 Session(SSO 核心,HttpOnly Cookie,效期 24 小時) | ✅ 已完成 |
| `authorization_codes` | 授權碼(SHA-256 雜湊儲存、一次性、效期 5 分鐘、凍結授權當下的 redirect URI/scope/nonce/PKCE challenge/auth_time) | ✅ 已完成 |
| `refresh_tokens` | Refresh Token(SHA-256 雜湊儲存、效期 30 天、兌換即輪替、重用時整鏈撤銷) | ✅ 已完成 |
| `consents` | 使用者×應用程式的同意記錄(scope 聯集;同範圍靜默通過) | ✅ 已完成 |
| `signing_keys` | RSA 簽章金鑰(供 JWKS 輪替) | ✅ 模型與管理頁已完成(`internal/jwk`:PKCS#8 儲存、RFC 7638 kid、RFC 7517 JWK/JWKS 公開形式;`/admin/keys` 產生/退休;`/.well-known/jwks.json` 已上線),輪替排程規劃中 |
## Roadmap
- [x] 專案骨架:chi 路由、GORM + PostgreSQL 連線、`/health` 健康檢查
- [x] 使用者系統:註冊、登入/登出、密碼重設(撤銷 Session)、密碼雜湊(argon2id)、瀏覽器 Session
- [ ] Client 管理:RP 註冊 API(管理頁 `/admin/applications` 已完成:註冊、編輯、client_secret 發配與輪替、刪除,僅 admin)
- [x] 金鑰管理:RSA 金鑰產生、`/.well-known/jwks.json`、金鑰輪替(手動:產生新鑰後退休舊鑰;自動排程規劃中)
- [x] OIDC Discovery:`/.well-known/openid-configuration`
- [x] Authorization Code Flow + PKCE(`/authorize`,含同意頁與首次同意後記住)
- [x] Token 端點:Access Token(JWT/RS256)、ID Token、Refresh Token 簽發與驗證
- [x] UserInfo 端點(`/userinfo`)
- [x] Refresh Token 輪替與撤銷(重用偵測、整鏈撤銷)
- [x] RP-Initiated Logout(`/logout`:`id_token_hint` 驗證、確認頁、`post_logout_redirect_uri` 註冊比對與 `state` 回填)
- [ ] Client Credentials Grant
- [ ] Front-Channel / Back-Channel Logout(跨 RP 單一登出)
- [ ] 管理 API 與簡易管理介面
## 專案結構
```
alterminal/
├── cmd/alterminal/
│ └── main.go # 程式進入點、路由裝配、CLI 分派
└── internal/
├── auth/ # 認證領域與非管理頁 HTTP:User/Session 模型、
│ │ # argon2id 密碼雜湊、登入/登出、模板與 CSRF、
│ │ # 靜態檔、自訂 404
│ ├── templates/ # HTML 模板(Tailwind utility class)
│ └── assets/css/ # Tailwind 進入點(input.css)與建置輸出(main.css)
├── application/ # Application 模型(RP 註冊與驗證)
├── store/ # 資料庫連線與自動遷移
├── admin/ # 管理頁:/admin/keys 金鑰、/admin/applications 應用程式
├── cli/ # create-account / update-password 子指令
├── testdb/ # 整合測試共用資料庫(alterminal_test)
├── jwk/ # 簽章金鑰與 JWKS
└── oidc/ # OIDC 核心端點與憑證模型:Discovery、/authorize
# (同意頁)、/token、/userinfo、JWKS,以及
# authorization_codes/refresh_tokens/consents
```