first commit

This commit is contained in:
2026-10-03 10:44:29 +08:00
parent f373cb8d37
commit bcf3d3769c
58 changed files with 4313 additions and 487 deletions
+115
View File
@@ -0,0 +1,115 @@
package oidc
import (
"errors"
"log"
"net/http"
"strconv"
"strings"
"gorm.io/gorm"
"alterminal/internal/auth"
)
// UserInfoHandler 處理 GET/POST /userinfo(OIDC Core §5.3):以 Bearer
// Access Token 取得已授權的使用者 claims。token 取自 Authorization
// 標頭(RFC 6750 §2.1),POST 另接受表單的 access_token 欄位(§2.2)。
func UserInfoHandler(db *gorm.DB, issuer string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet, http.MethodPost:
default:
w.Header().Set("Allow", "GET, POST")
writeBearerError(w, http.StatusMethodNotAllowed, "", "僅支援 GET 與 POST")
return
}
token := bearerToken(r)
if token == "" {
writeBearerError(w, http.StatusUnauthorized, "", "缺少 Access Token")
return
}
if r.Method == http.MethodPost {
if err := r.ParseForm(); err != nil {
writeBearerError(w, http.StatusBadRequest, "invalid_request", "無法解析表單內容")
return
}
if t := r.PostFormValue("access_token"); t != "" {
token = t
}
}
claims, err := VerifyAccessToken(db, issuer, token)
if err != nil {
if !errors.Is(err, ErrInvalidToken) {
log.Printf("userinfo: %v", err)
}
writeBearerError(w, http.StatusUnauthorized, "invalid_token", "Access Token 無效")
return
}
userID, err := strconv.ParseUint(claims.Sub, 10, 64)
if err != nil {
writeBearerError(w, http.StatusUnauthorized, "invalid_token", "Access Token 無效")
return
}
var u auth.User
if err := db.First(&u, userID).Error; err != nil {
log.Printf("userinfo: 查詢使用者 %d: %v", userID, err)
writeBearerError(w, http.StatusUnauthorized, "invalid_token", "Access Token 無效")
return
}
// claims 依授權 scope 決定(OIDC Core §5.4):sub 恆有;profile
// 加 name 與 preferred_username;email 加 email 與
// email_verified。Access Token 未含 openid scope(非授權碼流程
// 核發)者不得存取(RFC 6750 insufficient_scope)。
if !scopeHas(claims.Scope, "openid") {
writeBearerError(w, http.StatusForbidden, "insufficient_scope", "缺少 openid scope")
return
}
out := struct {
Sub string `json:"sub"`
Name string `json:"name,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified *bool `json:"email_verified,omitempty"`
}{Sub: claims.Sub}
if scopeHas(claims.Scope, "profile") {
out.Name = u.Name
out.PreferredUsername = u.Username
}
if scopeHas(claims.Scope, "email") {
out.Email = u.Email
verified := u.EmailVerified
out.EmailVerified = &verified
}
auth.WriteJSON(w, http.StatusOK, out)
}
}
// bearerToken 剖析 Authorization: Bearer 標頭(RFC 6750 §2.1)。
func bearerToken(r *http.Request) string {
h := r.Header.Get("Authorization")
const scheme = "bearer "
if len(h) < len(scheme) || !strings.EqualFold(h[:len(scheme)], scheme) {
return ""
}
return strings.TrimSpace(h[len(scheme):])
}
// writeBearerError 輸出 /userinfo 的 Bearer 錯誤,並以
// WWW-Authenticate 標頭回報錯誤細節(RFC 6750 §3)。
func writeBearerError(w http.ResponseWriter, status int, code, description string) {
if status != http.StatusBadRequest {
challenge := `Bearer realm="alterminal"`
if code != "" {
challenge += `, error="` + code + `"`
if description != "" {
challenge += `, error_description="` + description + `"`
}
}
w.Header().Set("WWW-Authenticate", challenge)
}
auth.WriteError(w, status, description)
}