feat(observability): 增加 OIDC 失败关联诊断

将 OIDC Access Token 与 ID Token 校验失败映射为固定安全分类,并生成 diagnosticId 关联浏览器错误与服务端日志。日志不记录授权码、Token 或底层敏感输入,且保留 errors.Is(ErrUnauthorized) 兼容语义。

验证:go test ./... -count=1(apps/api)
This commit is contained in:
2026-07-28 06:19:24 +08:00
parent b51ecc5eed
commit 4a464cacca
5 changed files with 180 additions and 35 deletions
@@ -128,6 +128,51 @@ func TestCompleteOIDCLoginReportsSafeTransactionFailureReason(t *testing.T) {
}
}
func TestOIDCTokenFailureReportsOnlyCorrelatedSafeStage(t *testing.T) {
var logs bytes.Buffer
server := &Server{
identityTestRevision: identity.Revision{
ID: "diagnostic-revision", WebBaseURL: "http://localhost:5178",
},
logger: slog.New(slog.NewJSONHandler(&logs, nil)),
}
request := httptest.NewRequest(
http.MethodGet,
"/api/v1/auth/oidc/callback?code=sensitive-code-marker",
nil,
)
recorder := httptest.NewRecorder()
server.writeOIDCTokenFailure(
recorder,
request,
"ACCESS_TOKEN_INVALID",
"REQUIRED_SCOPE_MISSING",
"认证中心访问令牌校验失败",
)
if recorder.Code != http.StatusSeeOther {
t.Fatalf("callback status=%d, want 303", recorder.Code)
}
location, err := url.Parse(recorder.Header().Get("Location"))
if err != nil {
t.Fatal(err)
}
diagnosticID := location.Query().Get("oidcDiagnosticId")
if location.Query().Get("oidcError") != errorCodeOIDCTokenExchangeFailed ||
location.Query().Get("oidcErrorReason") != "" ||
diagnosticID == "" {
t.Fatalf("unsafe callback diagnostic: %q", location.RawQuery)
}
if !strings.Contains(logs.String(), `"diagnosticId":"`+diagnosticID+`"`) ||
!strings.Contains(logs.String(), `"stage":"ACCESS_TOKEN_INVALID"`) ||
!strings.Contains(logs.String(), `"validationCategory":"REQUIRED_SCOPE_MISSING"`) ||
strings.Contains(logs.String(), "sensitive-code-marker") ||
strings.Contains(location.RawQuery, "sensitive-code-marker") {
t.Fatalf("OIDC token diagnostic was missing or leaked callback material")
}
}
func TestDeleteOIDCBrowserSessionIsIdempotentAndExpiresCookie(t *testing.T) {
sessions := &fakeOIDCSessions{}
server := &Server{oidcSessions: sessions, identityTestCookieSecure: true}