You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/internal/api/oauth/oidc.go

225 lines
6.5 KiB
Go

2 weeks ago
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
}