e51666c52e
- 新增 web_sessions 表(sqlc):refresh_token 以 AES-256-GCM 落盘,密钥取自 CDROP_SESSION_SECRET(env、不在库内);cookie 仅存不透明随机串,表 id 存其 SHA-256,故 DB 单独泄露既换不出可用 cookie、也解不出 token - /auth/exchange 建 session 并下发 HttpOnly; Secure; SameSite=Lax; Path=/api/auth cookie,响应体不再回传 refresh_token - /auth/refresh 改为 cookie 驱动(7 天滑动失活),同时即开机静默免重登; 新增 /auth/logout(删 session + 清 cookie)、/auth/device(写设备名入 session) - device_name 随 session 存,开机 refresh 带回前端,抗 iOS PWA 存储清除; setup / 改名时 syncWebDeviceName 推送服务端 - striped per-session 锁串行化同会话并发 refresh,防一次性 refresh_token 被 花两次而把用户从所有 tab 登出 - 前端 store 移除 refreshToken 字段(长寿命凭据彻底不进 JS);main.tsx 首屏前 bootstrapAuth 用 cookie 静默续期,命中直接进已登录态、避免登录页闪现 - config:prod 强制 CDROP_SESSION_SECRET,缺失拒启动 - 桌面端不受影响(自有 loopback flow + Go keyring,不碰这两个端点)
90 lines
2.6 KiB
Go
90 lines
2.6 KiB
Go
package config
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestValidate_DevRequiresDevToken(t *testing.T) {
|
|
c := &Config{AuthMode: "dev", DevToken: ""}
|
|
err := c.validate()
|
|
if err == nil {
|
|
t.Fatal("dev mode without DevToken must reject; got nil error")
|
|
}
|
|
if !strings.Contains(err.Error(), "CDROP_DEV_TOKEN") {
|
|
t.Errorf("error must mention CDROP_DEV_TOKEN; got %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestValidate_DevWithDevTokenOK(t *testing.T) {
|
|
c := &Config{AuthMode: "dev", DevToken: "x"}
|
|
if err := c.validate(); err != nil {
|
|
t.Fatalf("dev with token should pass; got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidate_ProdOK(t *testing.T) {
|
|
c := &Config{AuthMode: "prod", OIDCAudience: "cdrop-web,cdrop-desktop", SessionSecret: "s3cr3t"}
|
|
if err := c.validate(); err != nil {
|
|
t.Fatalf("prod mode with audience + session secret should pass; got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidate_ProdRequiresSessionSecret(t *testing.T) {
|
|
// SessionSecret keys at-rest encryption of browser refresh_tokens; without it
|
|
// passwordless re-login can't run safely. Refuse to boot in prod.
|
|
c := &Config{AuthMode: "prod", OIDCAudience: "cdrop-web", SessionSecret: ""}
|
|
err := c.validate()
|
|
if err == nil {
|
|
t.Fatal("prod without CDROP_SESSION_SECRET must reject; got nil error")
|
|
}
|
|
if !strings.Contains(err.Error(), "CDROP_SESSION_SECRET") {
|
|
t.Errorf("error must mention CDROP_SESSION_SECRET; got %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestValidate_ProdRequiresAudience(t *testing.T) {
|
|
// R6: prod with an empty audience leaves RS256 audience checking off, so a
|
|
// token minted for any sibling app sharing the JWKS would validate. Refuse.
|
|
c := &Config{AuthMode: "prod", OIDCAudience: ""}
|
|
err := c.validate()
|
|
if err == nil {
|
|
t.Fatal("prod without CDROP_OIDC_AUDIENCE must reject; got nil error")
|
|
}
|
|
if !strings.Contains(err.Error(), "CDROP_OIDC_AUDIENCE") {
|
|
t.Errorf("error must mention CDROP_OIDC_AUDIENCE; got %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestValidate_UnknownMode(t *testing.T) {
|
|
c := &Config{AuthMode: "rogue"}
|
|
err := c.validate()
|
|
if err == nil {
|
|
t.Fatal("unknown auth mode must reject")
|
|
}
|
|
}
|
|
|
|
func TestLoad_EnvOverridesDefaults(t *testing.T) {
|
|
t.Setenv("CDROP_AUTH_MODE", "dev")
|
|
t.Setenv("CDROP_DEV_TOKEN", "abc")
|
|
t.Setenv("CDROP_LISTEN", ":9090")
|
|
t.Setenv("CDROP_DB_PATH", "/tmp/x.db")
|
|
|
|
cfg, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.AuthMode != "dev" {
|
|
t.Errorf("AuthMode: got %q, want %q", cfg.AuthMode, "dev")
|
|
}
|
|
if cfg.DevToken != "abc" {
|
|
t.Errorf("DevToken: got %q, want %q", cfg.DevToken, "abc")
|
|
}
|
|
if cfg.Listen != ":9090" {
|
|
t.Errorf("Listen: got %q, want %q", cfg.Listen, ":9090")
|
|
}
|
|
if cfg.DBPath != "/tmp/x.db" {
|
|
t.Errorf("DBPath: got %q, want %q", cfg.DBPath, "/tmp/x.db")
|
|
}
|
|
}
|