Improve youth mode action token flow

This commit is contained in:
richjohnrambo
2026-09-02 11:25:25 +08:00
parent 7f6995d8fe
commit 79947a2c6f
5 changed files with 387 additions and 13 deletions
+15 -3
View File
@@ -3021,7 +3021,7 @@ func serveIMYouthModeEnable(wrt http.ResponseWriter, req *http.Request) {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
return
}
if _, err := store.Security.ConsumeActionToken(principal.TenantID, principal.Uid, strings.TrimSpace(body.ActionToken), "set_independent_password", "youth_mode", time.Now().UTC().Round(time.Millisecond)); err != nil {
if _, err := store.Security.ConsumeActionToken(principal.TenantID, principal.Uid, strings.TrimSpace(body.ActionToken), "enable_youth_mode", "youth_mode", time.Now().UTC().Round(time.Millisecond)); err != nil {
writeIMError(wrt, req, http.StatusForbidden, 30042, imReasonActionToken, "action token invalid", nil)
return
}
@@ -6081,7 +6081,19 @@ func serveIMIndependentPassword(wrt http.ResponseWriter, req *http.Request) {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return
}
writeIMSuccess(wrt, req, map[string]any{"status": "ok", "purpose": purpose})
enableToken, enableExpiresAt, err := issueIMActionToken(principal.TenantID, principal.Uid, "enable_youth_mode", purpose)
if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return
}
writeIMSuccess(wrt, req, map[string]any{
"status": "ok",
"password_set": true,
"purpose": purpose,
"enable_action_token": enableToken,
"enable_action_expires_at": enableExpiresAt.Format(time.RFC3339Nano),
"next_action": "enable_youth_mode",
})
}
func serveIMIndependentPasswordVerify(wrt http.ResponseWriter, req *http.Request) {
@@ -6338,7 +6350,7 @@ func issueIMActionToken(tenantID types.TenantID, uid types.Uid, action, target s
func defaultIMActionTarget(action, fallback string) string {
switch action {
case "set_independent_password", "close_youth_mode", "reset_independent_password":
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password":
return "youth_mode"
default:
return fallback
+12 -2
View File
@@ -2132,6 +2132,13 @@ func TestServeIMIndependentPasswordSetAndVerify(t *testing.T) {
bcrypt.CompareHashAndPassword(security.independentPassword.PasswordHash, []byte("2794")) != nil {
t.Fatalf("password was not stored as a valid hash: %#v", security.independentPassword)
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["password_set"] != true || data["next_action"] != "enable_youth_mode" || data["enable_action_token"] == "" {
t.Fatalf("unexpected set response: %#v", data)
}
if security.actionToken == nil || security.actionToken.Action != "enable_youth_mode" || security.actionToken.Target != "youth_mode" {
t.Fatalf("unexpected enable token after set: %#v", security.actionToken)
}
expectIMBearer(t, ctrl, "token-verify", 7, 42)
req = httptest.NewRequest(http.MethodPost, "/v1/im/settings/independent-password/verify",
@@ -2144,7 +2151,7 @@ func TestServeIMIndependentPasswordSetAndVerify(t *testing.T) {
if rec.Code != http.StatusOK {
t.Fatalf("verify status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
data = decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["action"] != "close_youth_mode" || data["action_token"] == "" {
t.Fatalf("unexpected verify response: %#v", data)
}
@@ -3156,7 +3163,7 @@ func TestServeIMYouthModeEnableDisable(t *testing.T) {
defer ctrl.Finish()
oldSecurity := store.Security
security := &testSecurityStore{
actionToken: &types.ActionToken{Action: "set_independent_password", Target: "youth_mode"},
actionToken: &types.ActionToken{Action: "enable_youth_mode", Target: "youth_mode"},
independentPassword: &types.IndependentPassword{PasswordHash: []byte("hash")},
}
store.Security = security
@@ -3172,6 +3179,9 @@ func TestServeIMYouthModeEnableDisable(t *testing.T) {
if rec.Code != http.StatusOK {
t.Fatalf("enable status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
if security.consumedAction != "enable_youth_mode" || security.consumedTarget != "youth_mode" {
t.Fatalf("unexpected enable token consumption: action=%q target=%q", security.consumedAction, security.consumedTarget)
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["enabled"] != true || data["policy_version"].(float64) != 2 {
t.Fatalf("unexpected youth enable payload: %#v", data)
+4
View File
@@ -223,6 +223,8 @@ type testSecurityStore struct {
realName *types.RealNameVerification
realNameAppeal *types.RealNameAppeal
actionToken *types.ActionToken
consumedAction string
consumedTarget string
independentPassword *types.IndependentPassword
youthMode *types.YouthModeSettings
attempts int
@@ -294,6 +296,8 @@ func (s *testSecurityStore) CreateActionToken(_ types.TenantID, token *types.Act
}
func (s *testSecurityStore) ConsumeActionToken(_ types.TenantID, _ types.Uid, _ string, action string, target string, _ time.Time) (*types.ActionToken, error) {
s.consumedAction = action
s.consumedTarget = target
if s.err != nil {
return nil, s.err
}