Files
2026-10-03 10:44:29 +08:00

116 lines
3.6 KiB
Go
Raw Permalink 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.
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)
}