|
|
package oauth
|
|
|
|
|
|
import (
|
|
|
"github.com/veypi/vbase/internal/model"
|
|
|
"github.com/veypi/vigo"
|
|
|
)
|
|
|
|
|
|
// UserInfoResponse 用户信息响应 (OIDC标准格式)
|
|
|
type UserInfoResponse struct {
|
|
|
Sub string `json:"sub"` // 用户唯一标识
|
|
|
Name string `json:"name,omitempty"` // 全名
|
|
|
Nickname string `json:"nickname,omitempty"` // 昵称
|
|
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
|
|
Profile string `json:"profile,omitempty"`
|
|
|
Picture string `json:"picture,omitempty"` // 头像
|
|
|
Website string `json:"website,omitempty"`
|
|
|
Email string `json:"email,omitempty"`
|
|
|
EmailVerified bool `json:"email_verified,omitempty"`
|
|
|
Phone string `json:"phone,omitempty"`
|
|
|
PhoneVerified bool `json:"phone_verified,omitempty"`
|
|
|
Gender string `json:"gender,omitempty"`
|
|
|
Birthdate string `json:"birthdate,omitempty"`
|
|
|
Zoneinfo string `json:"zoneinfo,omitempty"`
|
|
|
Locale string `json:"locale,omitempty"`
|
|
|
UpdatedAt int64 `json:"updated_at,omitempty"`
|
|
|
Orgs []OrgClaim `json:"orgs,omitempty"` // 扩展字段:用户所属组织
|
|
|
}
|
|
|
|
|
|
// OrgClaim 组织声明
|
|
|
type OrgClaim struct {
|
|
|
OrgID string `json:"org_id"`
|
|
|
Name string `json:"name"`
|
|
|
Code string `json:"code"`
|
|
|
Roles []string `json:"roles"`
|
|
|
Status int `json:"status"`
|
|
|
}
|
|
|
|
|
|
// UserInfo 用户信息端点 (OIDC)
|
|
|
// GET /oauth/userinfo
|
|
|
func UserInfo(x *vigo.X) (*UserInfoResponse, error) {
|
|
|
// 从context获取当前用户
|
|
|
userID, ok := x.Get("oauth_user_id").(string)
|
|
|
if !ok || userID == "" {
|
|
|
return nil, vigo.ErrNotAuthorized.WithString("invalid_token")
|
|
|
}
|
|
|
|
|
|
// 获取scope决定返回哪些字段
|
|
|
scope, _ := x.Get("oauth_scope").(string)
|
|
|
scopes := parseScopes(scope)
|
|
|
|
|
|
var user model.User
|
|
|
if err := model.DB.First(&user, "id = ?", userID).Error; err != nil {
|
|
|
return nil, vigo.ErrNotFound.WithString("user not found")
|
|
|
}
|
|
|
|
|
|
resp := &UserInfoResponse{
|
|
|
Sub: user.ID,
|
|
|
}
|
|
|
|
|
|
// 根据scope返回相应字段
|
|
|
if contains(scopes, "profile") {
|
|
|
resp.Name = user.Nickname
|
|
|
resp.Nickname = user.Nickname
|
|
|
resp.PreferredUsername = user.Username
|
|
|
resp.Picture = user.Avatar
|
|
|
resp.UpdatedAt = user.UpdatedAt.Unix()
|
|
|
}
|
|
|
|
|
|
if contains(scopes, "email") {
|
|
|
resp.Email = user.Email
|
|
|
resp.EmailVerified = user.EmailVerified
|
|
|
}
|
|
|
|
|
|
if contains(scopes, "phone") {
|
|
|
resp.Phone = user.Phone
|
|
|
resp.PhoneVerified = user.PhoneVerified
|
|
|
}
|
|
|
|
|
|
// 如果请求了org scope,返回组织信息
|
|
|
if contains(scopes, "org") {
|
|
|
resp.Orgs = getUserOrgClaims(userID)
|
|
|
}
|
|
|
|
|
|
return resp, nil
|
|
|
}
|
|
|
|
|
|
// DiscoveryResponse OIDC发现文档响应
|
|
|
type DiscoveryResponse struct {
|
|
|
Issuer string `json:"issuer"`
|
|
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
|
|
TokenEndpoint string `json:"token_endpoint"`
|
|
|
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
|
|
RevocationEndpoint string `json:"revocation_endpoint"`
|
|
|
IntrospectionEndpoint string `json:"introspection_endpoint"`
|
|
|
JWKSUri string `json:"jwks_uri"`
|
|
|
ScopesSupported []string `json:"scopes_supported"`
|
|
|
ResponseTypesSupported []string `json:"response_types_supported"`
|
|
|
GrantTypesSupported []string `json:"grant_types_supported"`
|
|
|
SubjectTypesSupported []string `json:"subject_types_supported"`
|
|
|
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
|
|
|
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
|
|
|
ClaimsSupported []string `json:"claims_supported"`
|
|
|
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
|
|
|
}
|
|
|
|
|
|
// Discovery OIDC发现文档端点
|
|
|
// GET /.well-known/openid-configuration
|
|
|
func Discovery(x *vigo.X) (*DiscoveryResponse, error) {
|
|
|
baseURL := "https://" + x.Request.Host // 生产环境应该使用配置
|
|
|
|
|
|
return &DiscoveryResponse{
|
|
|
Issuer: baseURL,
|
|
|
AuthorizationEndpoint: baseURL + "/oauth/authorize",
|
|
|
TokenEndpoint: baseURL + "/oauth/token",
|
|
|
UserinfoEndpoint: baseURL + "/oauth/userinfo",
|
|
|
RevocationEndpoint: baseURL + "/oauth/revoke",
|
|
|
IntrospectionEndpoint: baseURL + "/oauth/introspect",
|
|
|
JWKSUri: baseURL + "/oauth/jwks",
|
|
|
ScopesSupported: []string{
|
|
|
"openid",
|
|
|
"profile",
|
|
|
"email",
|
|
|
"phone",
|
|
|
"org",
|
|
|
"roles",
|
|
|
"offline_access",
|
|
|
},
|
|
|
ResponseTypesSupported: []string{"code", "token", "id_token"},
|
|
|
GrantTypesSupported: []string{
|
|
|
"authorization_code",
|
|
|
"refresh_token",
|
|
|
"client_credentials",
|
|
|
},
|
|
|
SubjectTypesSupported: []string{"public"},
|
|
|
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
|
|
TokenEndpointAuthMethodsSupported: []string{
|
|
|
"client_secret_basic",
|
|
|
"client_secret_post",
|
|
|
},
|
|
|
ClaimsSupported: []string{
|
|
|
"sub",
|
|
|
"name",
|
|
|
"nickname",
|
|
|
"preferred_username",
|
|
|
"picture",
|
|
|
"email",
|
|
|
"email_verified",
|
|
|
"phone",
|
|
|
"phone_verified",
|
|
|
"updated_at",
|
|
|
"orgs",
|
|
|
},
|
|
|
CodeChallengeMethodsSupported: []string{"S256", "plain"},
|
|
|
}, nil
|
|
|
}
|
|
|
|
|
|
// JWKSResponse JWKS响应
|
|
|
type JWKSResponse struct {
|
|
|
Keys []JWK `json:"keys"`
|
|
|
}
|
|
|
|
|
|
// JWK JSON Web Key
|
|
|
type JWK struct {
|
|
|
Kty string `json:"kty"`
|
|
|
Kid string `json:"kid"`
|
|
|
Use string `json:"use"`
|
|
|
N string `json:"n"`
|
|
|
E string `json:"e"`
|
|
|
Alg string `json:"alg"`
|
|
|
}
|
|
|
|
|
|
// JWKS 公钥端点
|
|
|
// GET /oauth/jwks
|
|
|
func JWKS(x *vigo.X) (*JWKSResponse, error) {
|
|
|
// 返回JWT签名公钥
|
|
|
// 实际实现需要从配置的私钥中提取公钥信息
|
|
|
// 这里简化返回
|
|
|
return &JWKSResponse{
|
|
|
Keys: []JWK{
|
|
|
{
|
|
|
Kty: "RSA",
|
|
|
Kid: "default",
|
|
|
Use: "sig",
|
|
|
Alg: "RS256",
|
|
|
// N和E应该从实际公钥计算得出
|
|
|
},
|
|
|
},
|
|
|
}, nil
|
|
|
}
|
|
|
|
|
|
// helper functions
|
|
|
func getUserOrgClaims(userID string) []OrgClaim {
|
|
|
var members []model.OrgMember
|
|
|
if err := model.DB.Where("user_id = ? AND status = ?", userID, model.MemberStatusActive).Find(&members).Error; err != nil {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
if len(members) == 0 {
|
|
|
return []OrgClaim{}
|
|
|
}
|
|
|
|
|
|
result := make([]OrgClaim, 0, len(members))
|
|
|
for _, m := range members {
|
|
|
var org model.Org
|
|
|
if err := model.DB.First(&org, "id = ?", m.OrgID).Error; err != nil {
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
roles := []string{}
|
|
|
if m.RoleIDs != "" {
|
|
|
roles = parseScopes(m.RoleIDs) // 复用parseScopes来split
|
|
|
}
|
|
|
|
|
|
result = append(result, OrgClaim{
|
|
|
OrgID: m.OrgID,
|
|
|
Name: org.Name,
|
|
|
Code: org.Code,
|
|
|
Roles: roles,
|
|
|
Status: m.Status,
|
|
|
})
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
}
|