feat: Z34b restore-by-host + harden web nginx DNS race
Add HMAC restore-by-host for phone-less rebind; resolve gateway at request time and recreate web after stack up. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -32,6 +32,7 @@ var Catalog = []Entry{
|
||||
{Method: "POST", Path: "/api/v1/auth/bind/phone-confirm", OperationID: "bindPhoneConfirm", Summary: "同号确认后绑定", Public: true, Group: "auth"},
|
||||
{Method: "GET", Path: "/api/v1/auth/bind/policy", OperationID: "bindPolicy", Summary: "绑定策略(试运行/短信/凭票开关)", Public: true, Group: "auth"},
|
||||
{Method: "POST", Path: "/api/v1/auth/yuheng/ticket-exchange", OperationID: "yuhengTicketExchange", Summary: "宇恒凭票免登录换票(仅宇恒)", Public: true, Group: "auth"},
|
||||
{Method: "POST", Path: "/api/v1/auth/yuheng/restore-by-host", OperationID: "yuhengRestoreByHost", Summary: "按宇恒 host_key 恢复同步绑定(Z34b,phone 可空)", Public: true, Group: "auth"},
|
||||
{Method: "POST", Path: "/api/v1/auth/invites/accept", OperationID: "acceptInvite", Summary: "接受邀请加入租户", Group: "auth"},
|
||||
{Method: "POST", Path: "/api/v1/tenants", OperationID: "createTenant", Summary: "pending 用户创建自己的公司", Group: "auth"},
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ type AgentConf struct {
|
||||
YuhengTicket YuhengTicketConf `json:",optional"`
|
||||
}
|
||||
|
||||
// YuhengTicketConf 宇恒 → 智建 短时凭票(scope=sync_bind)。
|
||||
// YuhengTicketConf 宇恒 → 智建 短时凭票(scope=sync_bind | sync_restore)。
|
||||
type YuhengTicketConf struct {
|
||||
Enabled bool `json:",optional"`
|
||||
Secret string `json:",optional"` // 与宇恒共享;生产必换
|
||||
|
||||
@@ -46,6 +46,7 @@ func RegisterHandlers(server *rest.Server, svcCtx *svc.ServiceContext) {
|
||||
{Method: http.MethodPost, Path: "/api/v1/auth/bind/phone-confirm", Handler: rl(bindPhoneConfirmHandler(svcCtx))},
|
||||
{Method: http.MethodGet, Path: "/api/v1/auth/bind/policy", Handler: rl(bindPolicyHandler(svcCtx))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/auth/yuheng/ticket-exchange", Handler: rl(yuhengTicketExchangeHandler(svcCtx))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/auth/yuheng/restore-by-host", Handler: rl(yuhengRestoreByHostHandler(svcCtx))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/auth/register", Handler: rl(registerHandler(svcCtx))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/auth/login", Handler: rl(loginHandler(svcCtx))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/auth/sms/send", Handler: rl(sendLoginSMSHandler(svcCtx))},
|
||||
@@ -146,8 +147,6 @@ func RegisterHandlers(server *rest.Server, svcCtx *svc.ServiceContext) {
|
||||
{Method: http.MethodGet, Path: "/api/v1/admin/sync/conflicts", Handler: chain(syncConflictsHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/conflicts/:id/resolve", Handler: chain(syncResolveConflictHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/reconcile", Handler: chain(syncReconcileHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodGet, Path: "/api/v1/admin/sync/channels/:id/checkpoint", Handler: chain(syncCheckpointMetaHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/restore", Handler: chain(syncRestoreHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/ingest", Handler: chain(syncIngestHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodGet, Path: "/api/v1/admin/sync/channels/:id/inspect", Handler: chain(syncInspectHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
{Method: http.MethodPost, Path: "/api/v1/admin/sync/channels/:id/preview", Handler: chain(syncPreviewHandler(svcCtx), rl, authMW, tenant, perm(authx.Perm数据同步))},
|
||||
@@ -431,6 +430,34 @@ func yuhengTicketExchangeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func yuhengRestoreByHostHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req applogic.YuhengRestoreByHostReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := applogic.NewAuthLogic(r.Context(), svcCtx).RestoreByYuhengHost(req)
|
||||
if err != nil {
|
||||
if e := applogic.AsRestoreBindError(err); e != nil {
|
||||
body := map[string]any{
|
||||
"code": e.HTTPStatus,
|
||||
"message": e.Message,
|
||||
"ok": false,
|
||||
}
|
||||
if e.NeedBind {
|
||||
body["need_bind"] = true
|
||||
}
|
||||
httpx.WriteJson(w, e.HTTPStatus, body)
|
||||
return
|
||||
}
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkJson(w, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func roleListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := applogic.NewRoleAdminLogic(r.Context(), svcCtx).List()
|
||||
|
||||
248
platform/internal/logic/applogic/yuheng_restore.go
Normal file
248
platform/internal/logic/applogic/yuheng_restore.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"aijianzhan/platform/internal/agentcap"
|
||||
"aijianzhan/platform/internal/agentstore"
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/types"
|
||||
"aijianzhan/platform/internal/yuhticket"
|
||||
)
|
||||
|
||||
// YuhengRestoreByHostReq Z34b:仅凭宇恒账号 host_key 恢复已有同步绑定。
|
||||
type YuhengRestoreByHostReq struct {
|
||||
Ticket string `json:"ticket"`
|
||||
HostKey string `json:"host_key"` // 须与票内一致(= 宇恒用户 _id)
|
||||
}
|
||||
|
||||
// YuhengRestoreByHostResp 与绑定成功对齐,便于宇恒 apply_bind_success。
|
||||
type YuhengRestoreByHostResp struct {
|
||||
*types.TokenResp
|
||||
OK bool `json:"ok"`
|
||||
SyncBound bool `json:"sync_bound"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
ChannelID string `json:"channel_id,omitempty"`
|
||||
OnlineDBID string `json:"online_db_id,omitempty"`
|
||||
TenantID int64 `json:"tenant_id,omitempty"`
|
||||
AgentID int64 `json:"agent_id,omitempty"`
|
||||
HostKey string `json:"host_key,omitempty"`
|
||||
PhoneMasked string `json:"phone_masked,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// RestoreBindError 映射 HTTP 状态(404 need_bind / 401 / 403)。
|
||||
type RestoreBindError struct {
|
||||
HTTPStatus int
|
||||
Message string
|
||||
NeedBind bool
|
||||
}
|
||||
|
||||
func (e *RestoreBindError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func restoreNeedBind(msg string) error {
|
||||
return &RestoreBindError{HTTPStatus: http.StatusNotFound, Message: msg, NeedBind: true}
|
||||
}
|
||||
|
||||
func restoreUnauthorized(msg string) error {
|
||||
return &RestoreBindError{HTTPStatus: http.StatusUnauthorized, Message: msg}
|
||||
}
|
||||
|
||||
func restoreForbidden(msg string) error {
|
||||
return &RestoreBindError{HTTPStatus: http.StatusForbidden, Message: msg}
|
||||
}
|
||||
|
||||
var onlineUserSuffix = regexp.MustCompile(`_u(\d+)$`)
|
||||
|
||||
// RestoreByYuhengHost Z34b:验签凭票 → FindByHostKey → 已 sync_bound 则轮换密钥并回传落点。
|
||||
func (l *AuthLogic) RestoreByYuhengHost(req YuhengRestoreByHostReq) (*YuhengRestoreByHostResp, error) {
|
||||
cfg := l.svcCtx.Config.Agent.YuhengTicket
|
||||
if !cfg.Enabled {
|
||||
return nil, restoreUnauthorized("宇恒凭票未启用(Agent.YuhengTicket.Enabled)")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Secret) == "" {
|
||||
return nil, restoreUnauthorized("宇恒凭票 Secret 未配置")
|
||||
}
|
||||
if l.svcCtx.Agents == nil {
|
||||
return nil, fmt.Errorf("bind service unavailable")
|
||||
}
|
||||
claims, err := yuhticket.Verify(req.Ticket, yuhticket.VerifyOpts{
|
||||
Secret: cfg.Secret,
|
||||
Issuer: cfg.Issuer,
|
||||
Audience: cfg.Audience,
|
||||
AllowEmptyPhone: true,
|
||||
AllowedScopes: []string{"", "sync_bind", "sync_restore"},
|
||||
})
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "expired") || strings.Contains(msg, "signature") ||
|
||||
strings.Contains(msg, "already used") || strings.Contains(msg, "invalid") ||
|
||||
strings.Contains(msg, "not allowed") || strings.Contains(msg, "mismatch") ||
|
||||
strings.Contains(msg, "未配置") {
|
||||
return nil, restoreUnauthorized(msg)
|
||||
}
|
||||
return nil, restoreUnauthorized(msg)
|
||||
}
|
||||
hostKey := strings.TrimSpace(req.HostKey)
|
||||
if hostKey == "" {
|
||||
hostKey = strings.TrimSpace(claims.HostKey)
|
||||
}
|
||||
if hostKey == "" || hostKey != strings.TrimSpace(claims.HostKey) {
|
||||
return nil, restoreForbidden("host_key 与凭票不一致")
|
||||
}
|
||||
if yid := strings.TrimSpace(claims.YuhengUserID); yid != "" && yid != hostKey {
|
||||
return nil, restoreForbidden("yuheng_user_id 与 host_key 不一致")
|
||||
}
|
||||
if l.svcCtx.YuhengJTI != nil {
|
||||
if err := l.svcCtx.YuhengJTI.Consume(claims.JTI, claims.Exp); err != nil {
|
||||
return nil, restoreUnauthorized(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
acc, err := l.svcCtx.Agents.FindByHostKey(l.ctx, hostKey)
|
||||
if err != nil || acc == nil {
|
||||
return nil, restoreNeedBind("无此 host_key 同步绑定;请先绑定码/手机号绑定")
|
||||
}
|
||||
// 通道误删时先自愈再判断 sync_bound
|
||||
if _, hErr := HealTenantSyncBind(l.ctx, l.svcCtx, acc.TenantID); hErr == nil {
|
||||
if refreshed, gErr := l.svcCtx.Agents.Get(l.ctx, acc.TenantID, acc.AgentID); gErr == nil && refreshed != nil {
|
||||
acc = refreshed
|
||||
}
|
||||
}
|
||||
syncBound := strings.TrimSpace(acc.ChannelID) != "" && strings.TrimSpace(acc.OnlineDBID) != ""
|
||||
if acc.Status != agentstore.StatusActive || !syncBound {
|
||||
return nil, restoreNeedBind("host_key 尚未完成同步绑定;请先绑定码/手机号绑定")
|
||||
}
|
||||
|
||||
if err := l.ensureAgentSyncPerm(acc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acc, err = l.svcCtx.Agents.Get(l.ctx, acc.TenantID, acc.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secret, err := l.svcCtx.Agents.RotateSecret(l.ctx, acc.TenantID, acc.AgentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rotate client_secret: %w", err)
|
||||
}
|
||||
|
||||
token, exp, err := authx.IssueAgentToken(l.svcCtx.JWT, acc.TenantID, acc.AgentID, acc.Perms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = l.svcCtx.Agents.TouchToken(l.ctx, acc.AgentID)
|
||||
capSecret := l.svcCtx.Config.Agent.CapsuleSecret
|
||||
if capSecret == "" {
|
||||
capSecret = l.svcCtx.JWT.AccessSecret
|
||||
}
|
||||
tr := &types.TokenResp{
|
||||
AccessToken: token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresAt: exp,
|
||||
TenantID: acc.TenantID,
|
||||
UserID: acc.AgentID,
|
||||
Username: acc.ClientID,
|
||||
DisplayName: acc.Name,
|
||||
Role: authx.RoleAgent,
|
||||
AgentKey: agentcap.PublicAgentKey(capSecret, acc.TenantID, acc.AgentID),
|
||||
AgentID: acc.AgentID,
|
||||
Permissions: append([]string{}, acc.Perms...),
|
||||
AppSlugs: append([]string{}, acc.AppSlugs...),
|
||||
}
|
||||
fillAgentSyncOnToken(tr, acc)
|
||||
|
||||
phoneMasked := maskPhoneDigits(strings.TrimSpace(claims.Phone))
|
||||
if phoneMasked == "" {
|
||||
phoneMasked = l.phoneMaskedForAgent(acc)
|
||||
}
|
||||
|
||||
_ = l.writeBindAudit("yuheng_restore_by_host", acc.TenantID, acc.AgentID, map[string]any{
|
||||
"host_key": hostKey, "jti": claims.JTI, "yuheng_user_id": claims.YuhengUserID,
|
||||
})
|
||||
|
||||
return &YuhengRestoreByHostResp{
|
||||
TokenResp: tr,
|
||||
OK: true,
|
||||
SyncBound: true,
|
||||
ClientID: acc.ClientID,
|
||||
ClientSecret: secret,
|
||||
ChannelID: acc.ChannelID,
|
||||
OnlineDBID: acc.OnlineDBID,
|
||||
TenantID: acc.TenantID,
|
||||
AgentID: acc.AgentID,
|
||||
HostKey: hostKey,
|
||||
PhoneMasked: phoneMasked,
|
||||
Message: "已按宇恒账号恢复同步绑定",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func maskPhoneDigits(phone string) string {
|
||||
phone = strings.TrimSpace(phone)
|
||||
if len(phone) < 7 {
|
||||
return ""
|
||||
}
|
||||
// 11 位国内号:135****1944
|
||||
if len(phone) == 11 {
|
||||
return phone[:3] + "****" + phone[7:]
|
||||
}
|
||||
keep := 3
|
||||
if len(phone) < keep*2 {
|
||||
return phone[:1] + "****"
|
||||
}
|
||||
return phone[:keep] + "****" + phone[len(phone)-keep:]
|
||||
}
|
||||
|
||||
func (l *AuthLogic) phoneMaskedForAgent(acc *agentstore.Account) string {
|
||||
if acc == nil || l.svcCtx.Users == nil {
|
||||
return ""
|
||||
}
|
||||
// 优先 online_db_id 后缀 _u{userID}
|
||||
if m := onlineUserSuffix.FindStringSubmatch(strings.TrimSpace(acc.OnlineDBID)); len(m) == 2 {
|
||||
var uid int64
|
||||
_, _ = fmt.Sscanf(m[1], "%d", &uid)
|
||||
if uid > 0 {
|
||||
if u, err := l.svcCtx.Users.GetByID(l.ctx, uid); err == nil && u != nil {
|
||||
return maskPhoneDigits(u.Phone)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 回落:同租户 Binding 上挂该 online_db_id 的用户
|
||||
if l.svcCtx.DBSync == nil {
|
||||
return ""
|
||||
}
|
||||
list, err := l.svcCtx.DBSync.Store().ListBindingsFiltered(acc.TenantID, 0, "")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
online := strings.TrimSpace(acc.OnlineDBID)
|
||||
for _, b := range list {
|
||||
if strings.TrimSpace(b.OnlineDBID) != online || b.UserID <= 0 {
|
||||
continue
|
||||
}
|
||||
if u, err := l.svcCtx.Users.GetByID(l.ctx, b.UserID); err == nil && u != nil {
|
||||
if m := maskPhoneDigits(u.Phone); m != "" {
|
||||
return m
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AsRestoreBindError 供 handler 取 HTTP 状态。
|
||||
func AsRestoreBindError(err error) *RestoreBindError {
|
||||
var e *RestoreBindError
|
||||
if errors.As(err, &e) {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -25,7 +25,7 @@ type Claims struct {
|
||||
LocalDatabaseID string `json:"local_database_id,omitempty"`
|
||||
Exp int64 `json:"exp"`
|
||||
JTI string `json:"jti"`
|
||||
Scope string `json:"scope,omitempty"` // sync_bind
|
||||
Scope string `json:"scope,omitempty"` // sync_bind | sync_restore
|
||||
YuhengUserID string `json:"yuheng_user_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ type VerifyOpts struct {
|
||||
Audience string // default aijianzhan
|
||||
Now time.Time
|
||||
MaxSkew time.Duration // clock skew; default 30s
|
||||
// AllowEmptyPhone:Z34b restore-by-host;phone 可空(仍须 host_key+jti)。
|
||||
AllowEmptyPhone bool
|
||||
// AllowedScopes:空则仅允许 sync_bind(含空 scope);restore 可传 sync_bind+sync_restore。
|
||||
AllowedScopes []string
|
||||
}
|
||||
|
||||
// Sign 供联调/测试;生产由宇恒侧用同一 Secret 签发。
|
||||
@@ -43,9 +47,6 @@ func Sign(secret string, c Claims) (string, error) {
|
||||
if secret == "" {
|
||||
return "", fmt.Errorf("ticket secret empty")
|
||||
}
|
||||
if c.JTI == "" || c.Phone == "" || c.HostKey == "" || c.Exp == 0 {
|
||||
return "", fmt.Errorf("jti/phone/host_key/exp required")
|
||||
}
|
||||
if c.Iss == "" {
|
||||
c.Iss = "yuheng"
|
||||
}
|
||||
@@ -55,6 +56,11 @@ func Sign(secret string, c Claims) (string, error) {
|
||||
if c.Scope == "" {
|
||||
c.Scope = "sync_bind"
|
||||
}
|
||||
// sync_restore:允许无 phone(换机仅凭宇恒账号 ID);其余 scope 仍须 phone。
|
||||
needPhone := c.Scope != "sync_restore"
|
||||
if c.JTI == "" || c.HostKey == "" || c.Exp == 0 || (needPhone && c.Phone == "") {
|
||||
return "", fmt.Errorf("jti/host_key/exp required; phone required unless scope=sync_restore")
|
||||
}
|
||||
raw, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -105,7 +111,19 @@ func Verify(ticket string, opts VerifyOpts) (*Claims, error) {
|
||||
if c.Aud != aud {
|
||||
return nil, fmt.Errorf("ticket audience mismatch")
|
||||
}
|
||||
if c.Scope != "" && c.Scope != "sync_bind" {
|
||||
scope := strings.TrimSpace(c.Scope)
|
||||
allowed := opts.AllowedScopes
|
||||
if len(allowed) == 0 {
|
||||
allowed = []string{"", "sync_bind"}
|
||||
}
|
||||
scopeOK := false
|
||||
for _, a := range allowed {
|
||||
if scope == strings.TrimSpace(a) {
|
||||
scopeOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !scopeOK {
|
||||
return nil, fmt.Errorf("ticket scope not allowed")
|
||||
}
|
||||
now := opts.Now
|
||||
@@ -124,7 +142,10 @@ func Verify(ticket string, opts VerifyOpts) (*Claims, error) {
|
||||
if exp.After(now.Add(10 * time.Minute)) {
|
||||
return nil, fmt.Errorf("ticket exp too far")
|
||||
}
|
||||
if strings.TrimSpace(c.Phone) == "" || strings.TrimSpace(c.HostKey) == "" || strings.TrimSpace(c.JTI) == "" {
|
||||
if strings.TrimSpace(c.HostKey) == "" || strings.TrimSpace(c.JTI) == "" {
|
||||
return nil, fmt.Errorf("ticket missing host_key/jti")
|
||||
}
|
||||
if strings.TrimSpace(c.Phone) == "" && !opts.AllowEmptyPhone && scope != "sync_restore" {
|
||||
return nil, fmt.Errorf("ticket missing phone/host_key/jti")
|
||||
}
|
||||
return &c, nil
|
||||
|
||||
@@ -35,6 +35,43 @@ func TestVerifyBadSig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignVerifyRestoreNoPhone(t *testing.T) {
|
||||
secret := "test-secret"
|
||||
now := time.Unix(1_700_000_000, 0).UTC()
|
||||
c := Claims{
|
||||
Iss: "yuheng", Aud: "aijianzhan", HostKey: "6655aabbccddeeff00112233",
|
||||
Exp: now.Add(90 * time.Second).Unix(), JTI: "jti-restore", Scope: "sync_restore",
|
||||
YuhengUserID: "6655aabbccddeeff00112233",
|
||||
}
|
||||
tok, err := Sign(secret, c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := Verify(tok, VerifyOpts{
|
||||
Secret: secret, Now: now, AllowEmptyPhone: true,
|
||||
AllowedScopes: []string{"sync_bind", "sync_restore"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.HostKey != c.HostKey || got.Phone != "" {
|
||||
t.Fatalf("claims mismatch: %+v", got)
|
||||
}
|
||||
// 默认 Verify(须 phone)应拒绝无 phone 的 sync_bind;sync_restore 在默认 scopes 外也应拒绝
|
||||
if _, err := Verify(tok, VerifyOpts{Secret: secret, Now: now}); err == nil {
|
||||
t.Fatal("expected scope reject without AllowedScopes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignRequiresPhoneForSyncBind(t *testing.T) {
|
||||
_, err := Sign("s", Claims{
|
||||
HostKey: "h", Exp: time.Now().Add(time.Minute).Unix(), JTI: "j", Scope: "sync_bind",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected phone required for sync_bind")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJTIConsume(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
st, err := NewJTIStore(dir)
|
||||
|
||||
Reference in New Issue
Block a user