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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
}