修复多app登录配置
This commit is contained in:
parent
395470f3ba
commit
1ac0cecf63
@ -155,6 +155,47 @@ func TestPinnedRoomListOrdersRegionCountryThenCountryBucketsForNew(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCountryPinOnlyAppliesToSelectedCountryTab(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := roomservice.New(roomservice.Config{
|
||||||
|
NodeID: "node-pin-country-tab-test",
|
||||||
|
LeaseTTL: 10 * time.Second,
|
||||||
|
RankLimit: 20,
|
||||||
|
SnapshotEveryN: 1,
|
||||||
|
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
||||||
|
|
||||||
|
createPinnedListRoom(t, ctx, svc, "room-us-country-pin", "us-country-pin", 3251, 9001, "US")
|
||||||
|
createPinnedListRoom(t, ctx, svc, "room-us-hot", "us-hot", 3252, 9001, "US")
|
||||||
|
createPinnedListRoom(t, ctx, svc, "room-ae-country-pin", "ae-country-pin", 3253, 9001, "AE")
|
||||||
|
createPinnedListRoom(t, ctx, svc, "room-ae-hot", "ae-hot", 3254, 9001, "AE")
|
||||||
|
repository.SetRoomSortScore("room-us-country-pin", 1)
|
||||||
|
repository.SetRoomSortScore("room-us-hot", 10000)
|
||||||
|
repository.SetRoomSortScore("room-ae-country-pin", 2)
|
||||||
|
repository.SetRoomSortScore("room-ae-hot", 20000)
|
||||||
|
repository.SetRoomPresence("room-us-hot", 1, 0)
|
||||||
|
repository.SetRoomPresence("room-ae-hot", 1, 0)
|
||||||
|
expiresAtMS := time.Now().UTC().Add(24 * time.Hour).UnixMilli()
|
||||||
|
repository.PinRoomWithType("room-us-country-pin", "country", 9001, 99, expiresAtMS)
|
||||||
|
repository.PinRoomWithType("room-ae-country-pin", "country", 9001, 88, expiresAtMS)
|
||||||
|
|
||||||
|
page, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
|
||||||
|
ViewerUserId: 4001,
|
||||||
|
VisibleRegionId: 9001,
|
||||||
|
ViewerCountryCode: "US",
|
||||||
|
CountryCode: "AE",
|
||||||
|
Tab: "hot",
|
||||||
|
Limit: 4,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list AE country tab failed: %v", err)
|
||||||
|
}
|
||||||
|
if got := roomIDs(page.GetRooms()); len(got) != 2 || got[0] != "room-ae-country-pin" || got[1] != "room-ae-hot" {
|
||||||
|
t.Fatalf("AE country tab must only apply AE country pin, got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRoomListFiltersByOwnerCountry(t *testing.T) {
|
func TestRoomListFiltersByOwnerCountry(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
|
|||||||
@ -23,6 +23,11 @@ third_party:
|
|||||||
project_id: "lalu-party"
|
project_id: "lalu-party"
|
||||||
allowed_sign_in_providers:
|
allowed_sign_in_providers:
|
||||||
- "google.com"
|
- "google.com"
|
||||||
|
projects:
|
||||||
|
lalu:
|
||||||
|
project_id: "lalu-party"
|
||||||
|
huwaa:
|
||||||
|
project_id: "huwaa-6f3aa"
|
||||||
invite_recharge_worker:
|
invite_recharge_worker:
|
||||||
enabled: true
|
enabled: true
|
||||||
mic_time_worker:
|
mic_time_worker:
|
||||||
|
|||||||
@ -23,6 +23,11 @@ third_party:
|
|||||||
project_id: "lalu-party"
|
project_id: "lalu-party"
|
||||||
allowed_sign_in_providers:
|
allowed_sign_in_providers:
|
||||||
- "google.com"
|
- "google.com"
|
||||||
|
projects:
|
||||||
|
lalu:
|
||||||
|
project_id: "lalu-party"
|
||||||
|
huwaa:
|
||||||
|
project_id: "huwaa-6f3aa"
|
||||||
invite_recharge_worker:
|
invite_recharge_worker:
|
||||||
enabled: true
|
enabled: true
|
||||||
mic_time_worker:
|
mic_time_worker:
|
||||||
|
|||||||
@ -23,6 +23,11 @@ third_party:
|
|||||||
project_id: "lalu-party"
|
project_id: "lalu-party"
|
||||||
allowed_sign_in_providers:
|
allowed_sign_in_providers:
|
||||||
- "google.com"
|
- "google.com"
|
||||||
|
projects:
|
||||||
|
lalu:
|
||||||
|
project_id: "lalu-party"
|
||||||
|
huwaa:
|
||||||
|
project_id: "huwaa-6f3aa"
|
||||||
invite_recharge_worker:
|
invite_recharge_worker:
|
||||||
enabled: true
|
enabled: true
|
||||||
mic_time_worker:
|
mic_time_worker:
|
||||||
|
|||||||
@ -114,10 +114,13 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
|
|
||||||
server := servicegrpc.New("user-service")
|
server := servicegrpc.New("user-service")
|
||||||
thirdPartyVerifier := authservice.NewRoutedThirdPartyVerifier(
|
thirdPartyVerifier := authservice.NewRoutedThirdPartyVerifier(
|
||||||
authservice.NewFirebaseThirdPartyVerifier(authservice.FirebaseVerifierConfig{
|
authservice.NewAppFirebaseThirdPartyVerifier(authservice.AppFirebaseVerifierConfig{
|
||||||
ProjectID: cfg.ThirdParty.Firebase.ProjectID,
|
Default: authservice.FirebaseVerifierConfig{
|
||||||
AllowedSignInProviders: cfg.ThirdParty.Firebase.AllowedSignInProviders,
|
ProjectID: cfg.ThirdParty.Firebase.ProjectID,
|
||||||
CertsURL: cfg.ThirdParty.Firebase.CertsURL,
|
AllowedSignInProviders: cfg.ThirdParty.Firebase.AllowedSignInProviders,
|
||||||
|
CertsURL: cfg.ThirdParty.Firebase.CertsURL,
|
||||||
|
},
|
||||||
|
Projects: firebaseVerifierProjects(cfg.ThirdParty.Firebase),
|
||||||
}),
|
}),
|
||||||
authservice.NewStaticThirdPartyVerifier(cfg.ThirdParty.AllowedProviders),
|
authservice.NewStaticThirdPartyVerifier(cfg.ThirdParty.AllowedProviders),
|
||||||
)
|
)
|
||||||
@ -598,6 +601,19 @@ func (a *App) closeHealthHTTP() {
|
|||||||
_ = a.healthHTTP.Close(ctx)
|
_ = a.healthHTTP.Close(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func firebaseVerifierProjects(cfg config.FirebaseConfig) map[string]authservice.FirebaseVerifierConfig {
|
||||||
|
projects := make(map[string]authservice.FirebaseVerifierConfig, len(cfg.Projects))
|
||||||
|
for appCode, project := range cfg.Projects {
|
||||||
|
// app_code 已在配置加载阶段归一化;这里仍保留 Normalize,避免测试或手动构造配置绕过 Load。
|
||||||
|
projects[appcode.Normalize(appCode)] = authservice.FirebaseVerifierConfig{
|
||||||
|
ProjectID: project.ProjectID,
|
||||||
|
AllowedSignInProviders: project.AllowedSignInProviders,
|
||||||
|
CertsURL: project.CertsURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return projects
|
||||||
|
}
|
||||||
|
|
||||||
type tencentIMAccountImporter struct {
|
type tencentIMAccountImporter struct {
|
||||||
client *tencentim.RESTClient
|
client *tencentim.RESTClient
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/pkg/configx"
|
"hyapp/pkg/configx"
|
||||||
"hyapp/pkg/logx"
|
"hyapp/pkg/logx"
|
||||||
"hyapp/pkg/tencentim"
|
"hyapp/pkg/tencentim"
|
||||||
@ -159,6 +160,18 @@ type FirebaseConfig struct {
|
|||||||
AllowedSignInProviders []string `yaml:"allowed_sign_in_providers"`
|
AllowedSignInProviders []string `yaml:"allowed_sign_in_providers"`
|
||||||
// CertsURL 只用于测试或私有代理;生产为空时使用 Firebase 官方公钥端点。
|
// CertsURL 只用于测试或私有代理;生产为空时使用 Firebase 官方公钥端点。
|
||||||
CertsURL string `yaml:"certs_url"`
|
CertsURL string `yaml:"certs_url"`
|
||||||
|
// Projects 按 app_code 配置 Firebase 项目;Huwaa/Lalu 必须各自校验自己的 aud/iss。
|
||||||
|
Projects map[string]FirebaseProjectConfig `yaml:"projects"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirebaseProjectConfig 保存单个 App 的 Firebase 项目配置。
|
||||||
|
type FirebaseProjectConfig struct {
|
||||||
|
// ProjectID 必须匹配该 App Firebase token 的 aud 和 issuer。
|
||||||
|
ProjectID string `yaml:"project_id"`
|
||||||
|
// AllowedSignInProviders 为空时继承 firebase.allowed_sign_in_providers。
|
||||||
|
AllowedSignInProviders []string `yaml:"allowed_sign_in_providers"`
|
||||||
|
// CertsURL 为空时继承 firebase.certs_url,生产通常不需要配置。
|
||||||
|
CertsURL string `yaml:"certs_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// InviteRechargeWorkerConfig 保存钱包充值事实消费策略。
|
// InviteRechargeWorkerConfig 保存钱包充值事实消费策略。
|
||||||
@ -378,6 +391,8 @@ func Load(path string) (Config, error) {
|
|||||||
if cfg.CPIntimacyLeaderboard.KeyPrefix == "" {
|
if cfg.CPIntimacyLeaderboard.KeyPrefix == "" {
|
||||||
cfg.CPIntimacyLeaderboard.KeyPrefix = "user:cp_intimacy_leaderboard"
|
cfg.CPIntimacyLeaderboard.KeyPrefix = "user:cp_intimacy_leaderboard"
|
||||||
}
|
}
|
||||||
|
cfg.ThirdParty.AllowedProviders = normalizeStringSlice(cfg.ThirdParty.AllowedProviders)
|
||||||
|
cfg.ThirdParty.Firebase = normalizeFirebaseConfig(cfg.ThirdParty.Firebase)
|
||||||
cfg.RoomService.Addr = strings.TrimSpace(cfg.RoomService.Addr)
|
cfg.RoomService.Addr = strings.TrimSpace(cfg.RoomService.Addr)
|
||||||
if cfg.RoomService.Addr == "" {
|
if cfg.RoomService.Addr == "" {
|
||||||
cfg.RoomService.Addr = "127.0.0.1:13001"
|
cfg.RoomService.Addr = "127.0.0.1:13001"
|
||||||
@ -531,6 +546,34 @@ func normalizeRocketMQConfig(cfg RocketMQConfig) (RocketMQConfig, error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeFirebaseConfig(cfg FirebaseConfig) FirebaseConfig {
|
||||||
|
cfg.ProjectID = strings.TrimSpace(cfg.ProjectID)
|
||||||
|
cfg.AllowedSignInProviders = normalizeStringSlice(cfg.AllowedSignInProviders)
|
||||||
|
cfg.CertsURL = strings.TrimSpace(cfg.CertsURL)
|
||||||
|
if len(cfg.Projects) == 0 {
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
projects := make(map[string]FirebaseProjectConfig, len(cfg.Projects))
|
||||||
|
for rawAppCode, project := range cfg.Projects {
|
||||||
|
app := appcode.Normalize(rawAppCode)
|
||||||
|
project.ProjectID = strings.TrimSpace(project.ProjectID)
|
||||||
|
project.CertsURL = strings.TrimSpace(project.CertsURL)
|
||||||
|
project.AllowedSignInProviders = normalizeStringSlice(project.AllowedSignInProviders)
|
||||||
|
if len(project.AllowedSignInProviders) == 0 {
|
||||||
|
// 单 App 没写登录方式时继承全局配置;全局为空时 auth verifier 仍会默认 google.com。
|
||||||
|
project.AllowedSignInProviders = cfg.AllowedSignInProviders
|
||||||
|
}
|
||||||
|
if project.CertsURL == "" {
|
||||||
|
// Firebase 公钥端点对所有项目相同;测试代理或私有代理可以只写一次。
|
||||||
|
project.CertsURL = cfg.CertsURL
|
||||||
|
}
|
||||||
|
projects[app] = project
|
||||||
|
}
|
||||||
|
cfg.Projects = projects
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeStringSlice(values []string) []string {
|
func normalizeStringSlice(values []string) []string {
|
||||||
normalized := make([]string, 0, len(values))
|
normalized := make([]string, 0, len(values))
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
|
|||||||
47
services/user-service/internal/config/firebase_test.go
Normal file
47
services/user-service/internal/config/firebase_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadNormalizesFirebaseProjects(t *testing.T) {
|
||||||
|
// 多 App Firebase 配置必须在加载阶段归一化,避免线上 YAML 的空格或大小写让 app 路由失效。
|
||||||
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||||
|
if err := os.WriteFile(path, []byte(`
|
||||||
|
third_party:
|
||||||
|
allowed_providers:
|
||||||
|
- " firebase "
|
||||||
|
firebase:
|
||||||
|
project_id: " lalu-party "
|
||||||
|
allowed_sign_in_providers:
|
||||||
|
- " google.com "
|
||||||
|
certs_url: " https://certs.example.test "
|
||||||
|
projects:
|
||||||
|
" Huwaa ":
|
||||||
|
project_id: " huwaa-6f3aa "
|
||||||
|
lalu:
|
||||||
|
project_id: " lalu-party "
|
||||||
|
`), 0o600); err != nil {
|
||||||
|
t.Fatalf("write config failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := Load(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load failed: %v", err)
|
||||||
|
}
|
||||||
|
huwaa, ok := cfg.ThirdParty.Firebase.Projects["huwaa"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("huwaa Firebase project must be keyed by normalized app_code: %+v", cfg.ThirdParty.Firebase.Projects)
|
||||||
|
}
|
||||||
|
if huwaa.ProjectID != "huwaa-6f3aa" {
|
||||||
|
t.Fatalf("huwaa project_id mismatch: %+v", huwaa)
|
||||||
|
}
|
||||||
|
if len(huwaa.AllowedSignInProviders) != 1 || huwaa.AllowedSignInProviders[0] != "google.com" {
|
||||||
|
t.Fatalf("huwaa sign-in providers should inherit normalized global allowlist: %+v", huwaa.AllowedSignInProviders)
|
||||||
|
}
|
||||||
|
if huwaa.CertsURL != "https://certs.example.test" {
|
||||||
|
t.Fatalf("huwaa certs_url should inherit global certs_url: %q", huwaa.CertsURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
jwt "github.com/golang-jwt/jwt/v5"
|
jwt "github.com/golang-jwt/jwt/v5"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
authdomain "hyapp/services/user-service/internal/domain/auth"
|
authdomain "hyapp/services/user-service/internal/domain/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,6 +44,14 @@ type FirebaseVerifierConfig struct {
|
|||||||
Now func() time.Time
|
Now func() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppFirebaseVerifierConfig 保存多 App Firebase 项目的路由配置。
|
||||||
|
type AppFirebaseVerifierConfig struct {
|
||||||
|
// Default 是旧版单 project_id 配置的兼容入口;未配置 app 专属项目时才使用。
|
||||||
|
Default FirebaseVerifierConfig
|
||||||
|
// Projects 按 app_code 持有 Firebase verifier 配置,避免 Huwaa token 被 Lalu 项目规则误拒。
|
||||||
|
Projects map[string]FirebaseVerifierConfig
|
||||||
|
}
|
||||||
|
|
||||||
// FirebaseThirdPartyVerifier 校验 Firebase Auth ID token 并抽取稳定 Firebase uid。
|
// FirebaseThirdPartyVerifier 校验 Firebase Auth ID token 并抽取稳定 Firebase uid。
|
||||||
type FirebaseThirdPartyVerifier struct {
|
type FirebaseThirdPartyVerifier struct {
|
||||||
// projectID 是 Firebase project id,参与 aud/iss 双重校验。
|
// projectID 是 Firebase project id,参与 aud/iss 双重校验。
|
||||||
@ -65,6 +74,14 @@ type FirebaseThirdPartyVerifier struct {
|
|||||||
cacheExpiresAt time.Time
|
cacheExpiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppFirebaseThirdPartyVerifier 按当前请求的 app_code 选择 Firebase 项目。
|
||||||
|
type AppFirebaseThirdPartyVerifier struct {
|
||||||
|
// defaultVerifier 兼容历史单项目部署;有明确 app 项目配置时不会走到这里。
|
||||||
|
defaultVerifier *FirebaseThirdPartyVerifier
|
||||||
|
// verifiers 用归一化 app_code 路由到各自 Firebase verifier。
|
||||||
|
verifiers map[string]*FirebaseThirdPartyVerifier
|
||||||
|
}
|
||||||
|
|
||||||
// firebaseIDTokenClaims 是 Firebase ID token 中本服务关心的字段集合。
|
// firebaseIDTokenClaims 是 Firebase ID token 中本服务关心的字段集合。
|
||||||
type firebaseIDTokenClaims struct {
|
type firebaseIDTokenClaims struct {
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
@ -76,6 +93,65 @@ type firebaseIDTokenClaims struct {
|
|||||||
} `json:"firebase"`
|
} `json:"firebase"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAppFirebaseThirdPartyVerifier 创建按 App 隔离的 Firebase verifier。
|
||||||
|
func NewAppFirebaseThirdPartyVerifier(config AppFirebaseVerifierConfig) *AppFirebaseThirdPartyVerifier {
|
||||||
|
var defaultVerifier *FirebaseThirdPartyVerifier
|
||||||
|
if strings.TrimSpace(config.Default.ProjectID) != "" {
|
||||||
|
// 旧配置只有一个 project_id 时仍可启动;新 App 应优先写入 Projects 显式隔离。
|
||||||
|
defaultVerifier = NewFirebaseThirdPartyVerifier(config.Default)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifiers := make(map[string]*FirebaseThirdPartyVerifier, len(config.Projects))
|
||||||
|
for rawAppCode, projectConfig := range config.Projects {
|
||||||
|
app := appcode.Normalize(rawAppCode)
|
||||||
|
projectConfig = inheritFirebaseVerifierConfig(config.Default, projectConfig)
|
||||||
|
if strings.TrimSpace(projectConfig.ProjectID) == "" {
|
||||||
|
// 空 project_id 不能生成有效 aud/iss 校验器;保留 fail-closed 行为。
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
verifiers[app] = NewFirebaseThirdPartyVerifier(projectConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AppFirebaseThirdPartyVerifier{
|
||||||
|
defaultVerifier: defaultVerifier,
|
||||||
|
verifiers: verifiers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify 根据 context 中的 app_code 校验对应 Firebase ID token。
|
||||||
|
func (v *AppFirebaseThirdPartyVerifier) Verify(ctx context.Context, provider string, credential string) (authdomain.ThirdPartyProfile, error) {
|
||||||
|
if v == nil {
|
||||||
|
return authdomain.ThirdPartyProfile{}, authFailed()
|
||||||
|
}
|
||||||
|
if verifier := v.verifiers[appcode.FromContext(ctx)]; verifier != nil {
|
||||||
|
// app 专属配置是强隔离边界;Huwaa token 只能命中 Huwaa project_id。
|
||||||
|
return verifier.Verify(ctx, provider, credential)
|
||||||
|
}
|
||||||
|
if v.defaultVerifier != nil {
|
||||||
|
// 兼容旧部署的单 project_id 配置,避免只配置 Lalu 的环境立即无法登录。
|
||||||
|
return v.defaultVerifier.Verify(ctx, provider, credential)
|
||||||
|
}
|
||||||
|
|
||||||
|
return authdomain.ThirdPartyProfile{}, authFailed()
|
||||||
|
}
|
||||||
|
|
||||||
|
func inheritFirebaseVerifierConfig(defaultConfig FirebaseVerifierConfig, projectConfig FirebaseVerifierConfig) FirebaseVerifierConfig {
|
||||||
|
if len(projectConfig.AllowedSignInProviders) == 0 {
|
||||||
|
// 登录方式默认继承全局 allowlist,减少每个 App 重复写 google.com 的配置噪音。
|
||||||
|
projectConfig.AllowedSignInProviders = defaultConfig.AllowedSignInProviders
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(projectConfig.CertsURL) == "" {
|
||||||
|
projectConfig.CertsURL = defaultConfig.CertsURL
|
||||||
|
}
|
||||||
|
if projectConfig.HTTPClient == nil {
|
||||||
|
projectConfig.HTTPClient = defaultConfig.HTTPClient
|
||||||
|
}
|
||||||
|
if projectConfig.Now == nil {
|
||||||
|
projectConfig.Now = defaultConfig.Now
|
||||||
|
}
|
||||||
|
return projectConfig
|
||||||
|
}
|
||||||
|
|
||||||
// NewFirebaseThirdPartyVerifier 创建 Firebase ID token verifier。
|
// NewFirebaseThirdPartyVerifier 创建 Firebase ID token verifier。
|
||||||
func NewFirebaseThirdPartyVerifier(config FirebaseVerifierConfig) *FirebaseThirdPartyVerifier {
|
func NewFirebaseThirdPartyVerifier(config FirebaseVerifierConfig) *FirebaseThirdPartyVerifier {
|
||||||
projectID := strings.TrimSpace(config.ProjectID)
|
projectID := strings.TrimSpace(config.ProjectID)
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
jwt "github.com/golang-jwt/jwt/v5"
|
jwt "github.com/golang-jwt/jwt/v5"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
authservice "hyapp/services/user-service/internal/service/auth"
|
authservice "hyapp/services/user-service/internal/service/auth"
|
||||||
)
|
)
|
||||||
@ -50,6 +51,45 @@ func TestFirebaseThirdPartyVerifierAcceptsGoogleFirebaseIDToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAppFirebaseThirdPartyVerifierRoutesByAppCode(t *testing.T) {
|
||||||
|
// Firebase ID token 的 aud/iss 必须跟当前 app_code 对应;Huwaa 不能再被 Lalu project_id 误拒。
|
||||||
|
now := time.Unix(2000, 0)
|
||||||
|
privateKey, certPEM := firebaseTestKeyPair(t, now)
|
||||||
|
certsServer := firebaseCertsServer(t, "kid-1", certPEM)
|
||||||
|
verifier := authservice.NewAppFirebaseThirdPartyVerifier(authservice.AppFirebaseVerifierConfig{
|
||||||
|
Default: authservice.FirebaseVerifierConfig{
|
||||||
|
ProjectID: "lalu-party",
|
||||||
|
AllowedSignInProviders: []string{"google.com"},
|
||||||
|
CertsURL: certsServer.URL,
|
||||||
|
Now: func() time.Time { return now },
|
||||||
|
},
|
||||||
|
Projects: map[string]authservice.FirebaseVerifierConfig{
|
||||||
|
"lalu": {
|
||||||
|
ProjectID: "lalu-party",
|
||||||
|
},
|
||||||
|
"huwaa": {
|
||||||
|
ProjectID: "huwaa-6f3aa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
huwaaToken := signFirebaseProjectToken(t, privateKey, "kid-1", "huwaa-6f3aa", "huwaa-firebase-uid", now)
|
||||||
|
laluToken := signFirebaseProjectToken(t, privateKey, "kid-1", "lalu-party", "lalu-firebase-uid", now)
|
||||||
|
|
||||||
|
profile, err := verifier.Verify(appcode.WithContext(context.Background(), "huwaa"), "firebase", huwaaToken)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("huwaa token should verify against huwaa project: %v", err)
|
||||||
|
}
|
||||||
|
if profile.ProviderSubject != "huwaa-firebase-uid" {
|
||||||
|
t.Fatalf("huwaa provider subject mismatch: %+v", profile)
|
||||||
|
}
|
||||||
|
if _, err := verifier.Verify(appcode.WithContext(context.Background(), "huwaa"), "firebase", laluToken); !xerr.IsCode(err, xerr.AuthFailed) {
|
||||||
|
t.Fatalf("huwaa app must reject lalu Firebase token, got %v", err)
|
||||||
|
}
|
||||||
|
if _, err := verifier.Verify(appcode.WithContext(context.Background(), "lalu"), "firebase", huwaaToken); !xerr.IsCode(err, xerr.AuthFailed) {
|
||||||
|
t.Fatalf("lalu app must reject huwaa Firebase token, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFirebaseThirdPartyVerifierRejectsInvalidClaims(t *testing.T) {
|
func TestFirebaseThirdPartyVerifierRejectsInvalidClaims(t *testing.T) {
|
||||||
// AUTH_FAILED 分支必须覆盖 provider、audience、登录方式和过期时间,避免身份枚举和配置泄漏。
|
// AUTH_FAILED 分支必须覆盖 provider、audience、登录方式和过期时间,避免身份枚举和配置泄漏。
|
||||||
now := time.Unix(2000, 0)
|
now := time.Unix(2000, 0)
|
||||||
@ -221,6 +261,19 @@ func signFirebaseTestToken(t *testing.T, privateKey *rsa.PrivateKey, kid string,
|
|||||||
return signed
|
return signed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func signFirebaseProjectToken(t *testing.T, privateKey *rsa.PrivateKey, kid string, projectID string, subject string, now time.Time) string {
|
||||||
|
t.Helper()
|
||||||
|
return signFirebaseTestToken(t, privateKey, kid, map[string]any{
|
||||||
|
"iss": "https://securetoken.google.com/" + projectID,
|
||||||
|
"aud": projectID,
|
||||||
|
"sub": subject,
|
||||||
|
"iat": now.Unix(),
|
||||||
|
"exp": now.Add(time.Hour).Unix(),
|
||||||
|
"auth_time": now.Add(-time.Minute).Unix(),
|
||||||
|
"firebase": map[string]any{"sign_in_provider": "google.com"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func cloneFirebaseClaims(input map[string]any) map[string]any {
|
func cloneFirebaseClaims(input map[string]any) map[string]any {
|
||||||
result := make(map[string]any, len(input))
|
result := make(map[string]any, len(input))
|
||||||
maps.Copy(result, input)
|
maps.Copy(result, input)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user