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 }