forked from alterminal/alterminal
116 lines
3.6 KiB
Go
116 lines
3.6 KiB
Go
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)
|
||
}
|