forked from alterminal/alterminal
first commit
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user