fix(oidc): 修复登录会话落库兼容问题
部分已配对环境在 0090 首次执行后缺少 oidc_client_id,导致统一认证完成投影后无法写入 Gateway 服务端会话。新增幂等前向迁移补齐该列,并为 Token 处理及会话创建增加不泄露凭据的稳定失败分类和关联诊断。\n\n验证:OIDC Session 全量单测、HTTP 回调定向测试、迁移升级集成测试、隔离 PostgreSQL 跨仓库 OIDC E2E 和本地 Chrome 真实登录均通过。
This commit is contained in:
@@ -3,6 +3,7 @@ package httpapi
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -211,7 +212,7 @@ func TestOIDCTokenFailureReportsOnlyCorrelatedSafeStage(t *testing.T) {
|
||||
}
|
||||
diagnosticID := location.Query().Get("oidcDiagnosticId")
|
||||
if location.Query().Get("oidcError") != errorCodeOIDCTokenExchangeFailed ||
|
||||
location.Query().Get("oidcErrorReason") != "" ||
|
||||
location.Query().Get("oidcErrorReason") != "REQUIRED_SCOPE_MISSING" ||
|
||||
diagnosticID == "" {
|
||||
t.Fatalf("unsafe callback diagnostic: %q", location.RawQuery)
|
||||
}
|
||||
@@ -224,6 +225,100 @@ func TestOIDCTokenFailureReportsOnlyCorrelatedSafeStage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCSessionFailureReportsOnlyCorrelatedSafeCategory(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.writeOIDCSessionFailure(
|
||||
recorder,
|
||||
request,
|
||||
errors.New("SESSION_REPOSITORY_FAILED"),
|
||||
)
|
||||
|
||||
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") != errorCodeOIDCSessionStoreUnavailable ||
|
||||
location.Query().Get("oidcErrorReason") != "SESSION_CREATE_FAILED" ||
|
||||
diagnosticID == "" {
|
||||
t.Fatalf("unsafe callback diagnostic: %q", location.RawQuery)
|
||||
}
|
||||
if !strings.Contains(logs.String(), `"diagnosticId":"`+diagnosticID+`"`) ||
|
||||
!strings.Contains(logs.String(), `"category":"SESSION_CREATE_FAILED"`) ||
|
||||
strings.Contains(logs.String(), "sensitive-code-marker") ||
|
||||
strings.Contains(location.RawQuery, "sensitive-code-marker") {
|
||||
t.Fatalf("OIDC session diagnostic was missing or leaked callback material")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyOIDCTokenExchangeFailure(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
tokens auth.OIDCTokenResponse
|
||||
err error
|
||||
wantStage string
|
||||
wantCategory string
|
||||
wantFailed bool
|
||||
}{
|
||||
{
|
||||
name: "endpoint request failed",
|
||||
err: errors.New("sensitive provider detail"),
|
||||
wantStage: "TOKEN_ENDPOINT_REJECTED",
|
||||
wantCategory: "TOKEN_ENDPOINT_REQUEST_FAILED",
|
||||
wantFailed: true,
|
||||
},
|
||||
{
|
||||
name: "access token missing",
|
||||
tokens: auth.OIDCTokenResponse{RefreshToken: "refresh", IDToken: "id"},
|
||||
wantStage: "TOKEN_RESPONSE_INVALID",
|
||||
wantCategory: "ACCESS_TOKEN_MISSING",
|
||||
wantFailed: true,
|
||||
},
|
||||
{
|
||||
name: "refresh token missing",
|
||||
tokens: auth.OIDCTokenResponse{AccessToken: "access", IDToken: "id"},
|
||||
wantStage: "TOKEN_RESPONSE_INVALID",
|
||||
wantCategory: "REFRESH_TOKEN_MISSING",
|
||||
wantFailed: true,
|
||||
},
|
||||
{
|
||||
name: "id token missing",
|
||||
tokens: auth.OIDCTokenResponse{AccessToken: "access", RefreshToken: "refresh"},
|
||||
wantStage: "TOKEN_RESPONSE_INVALID",
|
||||
wantCategory: "ID_TOKEN_MISSING",
|
||||
wantFailed: true,
|
||||
},
|
||||
{
|
||||
name: "complete response",
|
||||
tokens: auth.OIDCTokenResponse{AccessToken: "access", RefreshToken: "refresh", IDToken: "id"},
|
||||
wantFailed: false,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
stage, category, failed := classifyOIDCTokenExchangeFailure(test.tokens, test.err)
|
||||
if stage != test.wantStage || category != test.wantCategory || failed != test.wantFailed {
|
||||
t.Fatalf("classification=(%q, %q, %v), want (%q, %q, %v)", stage, category, failed, test.wantStage, test.wantCategory, test.wantFailed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteOIDCBrowserSessionIsIdempotentAndExpiresCookie(t *testing.T) {
|
||||
sessions := &fakeOIDCSessions{}
|
||||
server := &Server{oidcSessions: sessions, identityTestCookieSecure: true}
|
||||
|
||||
Reference in New Issue
Block a user