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) }