forked from baron/baron-sso
fix(headless-login): honor public base url for audience checks
- resolve headless audience against BACKEND_PUBLIC_URL first - keep forwarded header support for https absolute audiences - add regression tests for https success and http mismatch rejection - write BACKEND_PUBLIC_URL into staging workflow env generation
This commit is contained in:
@@ -61,6 +61,7 @@ ADMIN_PASSWORD=adminPasswordIsNotSimple
|
||||
USERFRONT_URL=https://sso.hmac.kr
|
||||
|
||||
# Services proxied via Nginx
|
||||
BACKEND_PUBLIC_URL=${USERFRONT_URL}
|
||||
BACKEND_URL=${USERFRONT_URL}
|
||||
OATHKEEPER_PUBLIC_URL=${USERFRONT_URL}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ jobs:
|
||||
USERFRONT_URL=${{ vars.USERFRONT_URL }}
|
||||
ADMINFRONT_URL=${{ vars.ADMINFRONT_URL }}
|
||||
DEVFRONT_URL=${{ vars.DEVFRONT_URL }}
|
||||
BACKEND_PUBLIC_URL=${{ vars.BACKEND_URL }}
|
||||
BACKEND_URL=${{ vars.BACKEND_URL }}
|
||||
OATHKEEPER_PUBLIC_URL=${{ vars.OATHKEEPER_PUBLIC_URL }}
|
||||
ORY_POSTGRES_TAG=${{ vars.ORY_POSTGRES_TAG }}
|
||||
|
||||
@@ -98,6 +98,7 @@ jobs:
|
||||
ADMIN_EMAIL=${{ vars.ADMIN_EMAIL }}
|
||||
ADMIN_PASSWORD=${{ secrets.STG_ADMIN_PASSWORD }}
|
||||
USERFRONT_URL=${{ vars.USERFRONT_URL }}
|
||||
BACKEND_PUBLIC_URL=${{ vars.BACKEND_URL }}
|
||||
BACKEND_URL=${{ vars.BACKEND_URL }}
|
||||
OATHKEEPER_PUBLIC_URL=${{ vars.OATHKEEPER_PUBLIC_URL }}
|
||||
ORY_POSTGRES_TAG=${{ vars.ORY_POSTGRES_TAG }}
|
||||
|
||||
@@ -248,6 +248,28 @@ func runHeadlessPasswordLoginE2E(
|
||||
jwks map[string]any,
|
||||
clientAssertion string,
|
||||
) (*http.Response, string) {
|
||||
return runHeadlessPasswordLoginE2ERequest(
|
||||
t,
|
||||
logger,
|
||||
appEnv,
|
||||
jwks,
|
||||
clientAssertion,
|
||||
"http://example.com/api/v1/auth/headless/password/login",
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func runHeadlessPasswordLoginE2ERequest(
|
||||
t *testing.T,
|
||||
logger *slog.Logger,
|
||||
appEnv string,
|
||||
jwks map[string]any,
|
||||
clientAssertion string,
|
||||
requestURL string,
|
||||
headers map[string]string,
|
||||
) (*http.Response, string) {
|
||||
t.Helper()
|
||||
|
||||
t.Helper()
|
||||
|
||||
logBuffer := &bytes.Buffer{}
|
||||
@@ -324,8 +346,11 @@ func runHeadlessPasswordLoginE2E(
|
||||
"password": "password",
|
||||
"login_challenge": "challenge-123",
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
|
||||
req := httptest.NewRequest(http.MethodPost, requestURL, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
@@ -417,3 +442,91 @@ func TestHeadlessPasswordLogin_E2E_DebugLogsIncludeDiagnostics(t *testing.T) {
|
||||
t.Fatalf("expected debug logs to include login challenge prefix, got=%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadlessPasswordLogin_E2E_AcceptsForwardedHTTPSAudience(t *testing.T) {
|
||||
privateKey, jwks := mustE2EHeadlessRSAJWK(t)
|
||||
const receivedAudience = "https://sso.hmac.kr/api/v1/auth/headless/password/login"
|
||||
clientAssertion := mustE2EHeadlessClientAssertion(
|
||||
t,
|
||||
privateKey,
|
||||
"headless-login-client",
|
||||
receivedAudience,
|
||||
)
|
||||
|
||||
logBuffer := &bytes.Buffer{}
|
||||
logger := slog.New(slog.NewJSONHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
|
||||
resp, output := runHeadlessPasswordLoginE2ERequest(
|
||||
t,
|
||||
logger,
|
||||
"production",
|
||||
jwks,
|
||||
clientAssertion,
|
||||
"http://sso.hmac.kr/api/v1/auth/headless/password/login",
|
||||
map[string]string{
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-Host": "sso.hmac.kr",
|
||||
},
|
||||
)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 200 for forwarded https audience, got %d, body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("failed to decode response body: %v", err)
|
||||
}
|
||||
if got["redirectTo"] != "http://rp/cb" {
|
||||
t.Fatalf("expected redirectTo, got=%v", got["redirectTo"])
|
||||
}
|
||||
|
||||
if strings.Contains(output, "\"reason_code\":\"invalid_client_assertion_audience\"") {
|
||||
t.Fatalf("did not expect audience mismatch log, got=%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadlessPasswordLogin_E2E_AcceptsConfiguredPublicHTTPSAudience(t *testing.T) {
|
||||
t.Setenv("BACKEND_PUBLIC_URL", "https://sso.hmac.kr")
|
||||
|
||||
privateKey, jwks := mustE2EHeadlessRSAJWK(t)
|
||||
const receivedAudience = "https://sso.hmac.kr/api/v1/auth/headless/password/login"
|
||||
clientAssertion := mustE2EHeadlessClientAssertion(
|
||||
t,
|
||||
privateKey,
|
||||
"headless-login-client",
|
||||
receivedAudience,
|
||||
)
|
||||
|
||||
logBuffer := &bytes.Buffer{}
|
||||
logger := slog.New(slog.NewJSONHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
|
||||
resp, output := runHeadlessPasswordLoginE2ERequest(
|
||||
t,
|
||||
logger,
|
||||
"production",
|
||||
jwks,
|
||||
clientAssertion,
|
||||
"http://sso.hmac.kr/api/v1/auth/headless/password/login",
|
||||
nil,
|
||||
)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 200 for configured public https audience, got %d, body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("failed to decode response body: %v", err)
|
||||
}
|
||||
if got["redirectTo"] != "http://rp/cb" {
|
||||
t.Fatalf("expected redirectTo, got=%v", got["redirectTo"])
|
||||
}
|
||||
if strings.Contains(output, "\"reason_code\":\"invalid_client_assertion_audience\"") {
|
||||
t.Fatalf("did not expect audience mismatch log, got=%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -703,30 +703,124 @@ func (h *AuthHandler) getBearerToken(c *fiber.Ctx) string {
|
||||
return parts[1]
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveUserfrontURL(c *fiber.Ctx) string {
|
||||
// 1. Try to use the Host header from the request
|
||||
host := c.Get("X-Forwarded-Host")
|
||||
func firstForwardedValue(raw string) string {
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
value := strings.TrimSpace(part)
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func forwardedDirective(raw, key string) string {
|
||||
for _, group := range strings.Split(raw, ",") {
|
||||
for _, directive := range strings.Split(group, ";") {
|
||||
pair := strings.SplitN(strings.TrimSpace(directive), "=", 2)
|
||||
if len(pair) != 2 {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(pair[0]), key) {
|
||||
continue
|
||||
}
|
||||
return strings.Trim(strings.TrimSpace(pair[1]), "\"")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizedAbsoluteBaseURL(raw string) string {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(trimmed)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parsed.RawQuery = ""
|
||||
parsed.Fragment = ""
|
||||
return strings.TrimRight(parsed.String(), "/")
|
||||
}
|
||||
|
||||
func forwardedRequestHost(c *fiber.Ctx) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
if host := firstForwardedValue(c.Get("X-Forwarded-Host")); host != "" {
|
||||
return host
|
||||
}
|
||||
if host := forwardedDirective(c.Get("Forwarded"), "host"); host != "" {
|
||||
return host
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func forwardedRequestProto(c *fiber.Ctx) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
if proto := firstForwardedValue(c.Get("X-Forwarded-Proto")); proto != "" {
|
||||
return strings.ToLower(proto)
|
||||
}
|
||||
if proto := forwardedDirective(c.Get("Forwarded"), "proto"); proto != "" {
|
||||
return strings.ToLower(proto)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolvePublicRequestBaseURL(c *fiber.Ctx, configuredBaseURL string) string {
|
||||
if base := normalizedAbsoluteBaseURL(configuredBaseURL); base != "" {
|
||||
return base
|
||||
}
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
host := forwardedRequestHost(c)
|
||||
proto := forwardedRequestProto(c)
|
||||
if host != "" && proto != "" {
|
||||
return fmt.Sprintf("%s://%s", proto, host)
|
||||
}
|
||||
|
||||
base := strings.TrimRight(strings.TrimSpace(c.BaseURL()), "/")
|
||||
if base != "" {
|
||||
return base
|
||||
}
|
||||
|
||||
host = strings.TrimSpace(c.Get("Host"))
|
||||
if host == "" {
|
||||
host = c.Hostname()
|
||||
host = strings.TrimSpace(c.Hostname())
|
||||
}
|
||||
|
||||
// 2. Determine scheme
|
||||
scheme := "https"
|
||||
if os.Getenv("APP_ENV") == "dev" || os.Getenv("APP_ENV") == "" || c.Protocol() == "http" {
|
||||
scheme = "http"
|
||||
proto = strings.ToLower(strings.TrimSpace(c.Protocol()))
|
||||
if host == "" || proto == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s://%s", proto, host)
|
||||
}
|
||||
|
||||
// 3. Fallback to env if host is not available or is localhost (and not in dev)
|
||||
func (h *AuthHandler) resolveUserfrontURL(c *fiber.Ctx) string {
|
||||
envURL := os.Getenv("USERFRONT_URL")
|
||||
if envURL == "" {
|
||||
envURL = "http://sso.hmac.kr"
|
||||
}
|
||||
|
||||
baseURL := resolvePublicRequestBaseURL(c, "")
|
||||
host := strings.TrimSpace(forwardedRequestHost(c))
|
||||
if host == "" {
|
||||
host = strings.TrimSpace(c.Hostname())
|
||||
}
|
||||
|
||||
if host == "" || (host == "localhost" && os.Getenv("APP_ENV") != "dev") {
|
||||
return strings.TrimRight(envURL, "/")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s", scheme, host)
|
||||
if baseURL == "" {
|
||||
return strings.TrimRight(envURL, "/")
|
||||
}
|
||||
return baseURL
|
||||
}
|
||||
|
||||
func (h *AuthHandler) GetTenantInfo(c *fiber.Ctx) error {
|
||||
@@ -1751,7 +1845,7 @@ func headlessAssertionAudiences(c *fiber.Ctx) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
base := strings.TrimRight(strings.TrimSpace(c.BaseURL()), "/")
|
||||
base := resolvePublicRequestBaseURL(c, os.Getenv("BACKEND_PUBLIC_URL"))
|
||||
if base == "" {
|
||||
return []string{path}
|
||||
}
|
||||
|
||||
@@ -336,6 +336,16 @@ func mustHeadlessClientAssertionWithAlgorithm(t *testing.T, privateKey any, alg
|
||||
}
|
||||
|
||||
func runHeadlessPasswordLoginWithAssertion(t *testing.T, jwks map[string]any, clientAssertion string) *http.Response {
|
||||
return runHeadlessPasswordLoginWithAssertionRequest(t, jwks, clientAssertion, "http://example.com/api/v1/auth/headless/password/login", nil)
|
||||
}
|
||||
|
||||
func runHeadlessPasswordLoginWithAssertionRequest(
|
||||
t *testing.T,
|
||||
jwks map[string]any,
|
||||
clientAssertion string,
|
||||
requestURL string,
|
||||
headers map[string]string,
|
||||
) *http.Response {
|
||||
t.Helper()
|
||||
|
||||
mockIdp := new(MockIdentityProvider)
|
||||
@@ -399,8 +409,11 @@ func runHeadlessPasswordLoginWithAssertion(t *testing.T, jwks map[string]any, cl
|
||||
"password": "password",
|
||||
"login_challenge": "challenge-123",
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
|
||||
req := httptest.NewRequest(http.MethodPost, requestURL, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
@@ -414,6 +427,24 @@ func runHeadlessPasswordLoginWithAssertionAndLogger(
|
||||
jwks map[string]any,
|
||||
clientAssertion string,
|
||||
logger *slog.Logger,
|
||||
) *http.Response {
|
||||
return runHeadlessPasswordLoginWithAssertionAndLoggerRequest(
|
||||
t,
|
||||
jwks,
|
||||
clientAssertion,
|
||||
"http://example.com/api/v1/auth/headless/password/login",
|
||||
nil,
|
||||
logger,
|
||||
)
|
||||
}
|
||||
|
||||
func runHeadlessPasswordLoginWithAssertionAndLoggerRequest(
|
||||
t *testing.T,
|
||||
jwks map[string]any,
|
||||
clientAssertion string,
|
||||
requestURL string,
|
||||
headers map[string]string,
|
||||
logger *slog.Logger,
|
||||
) *http.Response {
|
||||
t.Helper()
|
||||
|
||||
@@ -478,9 +509,12 @@ func runHeadlessPasswordLoginWithAssertionAndLogger(
|
||||
"password": "password",
|
||||
"login_challenge": "challenge-123",
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/headless/password/login", bytes.NewReader(body))
|
||||
req := httptest.NewRequest(http.MethodPost, requestURL, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Request-Id", "req-headless-test-123")
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
previous := slog.Default()
|
||||
@@ -1060,6 +1094,109 @@ func TestHeadlessPasswordLogin_AudienceMismatchReturnsDetailedCode(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadlessPasswordLogin_AcceptsForwardedHTTPSAudience(t *testing.T) {
|
||||
privateKey, jwks := mustHeadlessRSAJWK(t)
|
||||
clientAssertion := mustHeadlessClientAssertion(
|
||||
t,
|
||||
privateKey,
|
||||
"headless-login-client",
|
||||
"https://sso.hmac.kr/api/v1/auth/headless/password/login",
|
||||
)
|
||||
|
||||
resp := runHeadlessPasswordLoginWithAssertionRequest(
|
||||
t,
|
||||
jwks,
|
||||
clientAssertion,
|
||||
"http://sso.hmac.kr/api/v1/auth/headless/password/login",
|
||||
map[string]string{
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-Host": "sso.hmac.kr",
|
||||
},
|
||||
)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 200 for forwarded https audience, got %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if got["redirectTo"] != "http://rp/cb" {
|
||||
t.Fatalf("expected redirectTo, got=%v", got["redirectTo"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadlessPasswordLogin_AcceptsConfiguredPublicHTTPSAudience(t *testing.T) {
|
||||
t.Setenv("BACKEND_PUBLIC_URL", "https://sso.hmac.kr")
|
||||
|
||||
privateKey, jwks := mustHeadlessRSAJWK(t)
|
||||
clientAssertion := mustHeadlessClientAssertion(
|
||||
t,
|
||||
privateKey,
|
||||
"headless-login-client",
|
||||
"https://sso.hmac.kr/api/v1/auth/headless/password/login",
|
||||
)
|
||||
|
||||
resp := runHeadlessPasswordLoginWithAssertionRequest(
|
||||
t,
|
||||
jwks,
|
||||
clientAssertion,
|
||||
"http://sso.hmac.kr/api/v1/auth/headless/password/login",
|
||||
nil,
|
||||
)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 200 for configured public https audience, got %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if got["redirectTo"] != "http://rp/cb" {
|
||||
t.Fatalf("expected redirectTo, got=%v", got["redirectTo"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadlessPasswordLogin_RejectsHTTPAudienceWhenConfiguredPublicURLIsHTTPS(t *testing.T) {
|
||||
t.Setenv("BACKEND_PUBLIC_URL", "https://sso.hmac.kr")
|
||||
|
||||
privateKey, jwks := mustHeadlessRSAJWK(t)
|
||||
clientAssertion := mustHeadlessClientAssertion(
|
||||
t,
|
||||
privateKey,
|
||||
"headless-login-client",
|
||||
"http://sso.hmac.kr/api/v1/auth/headless/password/login",
|
||||
)
|
||||
|
||||
resp := runHeadlessPasswordLoginWithAssertionRequest(
|
||||
t,
|
||||
jwks,
|
||||
clientAssertion,
|
||||
"http://sso.hmac.kr/api/v1/auth/headless/password/login",
|
||||
nil,
|
||||
)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 401 for mismatched http audience, got %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if got["code"] != "invalid_client_assertion_audience" {
|
||||
t.Fatalf("expected invalid_client_assertion_audience, got=%v", got["code"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadlessPasswordLogin_IssSubMismatchReturnsDetailedCode(t *testing.T) {
|
||||
privateKey, jwks := mustHeadlessRSAJWK(t)
|
||||
now := time.Now()
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
| `backend/cmd/server/error_handler_test.go:48` | `TestNewErrorHandler_ProductionPassesClientError` | 오류/예외/거부 경로 검증 |
|
||||
| `backend/cmd/server/error_handler_test.go:73` | `TestNewErrorHandler_DevelopmentReturnsOriginalServerError` | 오류/예외/거부 경로 검증 |
|
||||
| `backend/cmd/server/error_handler_test.go:98` | `TestNewErrorHandler_MapsUnauthorizedCode` | 오류/예외/거부 경로 검증 |
|
||||
| `backend/cmd/server/headless_login_e2e_test.go:338` | `TestHeadlessPasswordLogin_E2E_ResponseIncludesDetailedCodeAndLogs` | 실제 app 경로에서 headless login 401 응답 본문과 구조화 로그를 함께 검증 |
|
||||
| `backend/cmd/server/headless_login_e2e_test.go:382` | `TestHeadlessPasswordLogin_E2E_DebugLogsIncludeDiagnostics` | 실제 app 경로에서 debug 레벨일 때만 headless 진단 필드가 로그에 포함되는지 검증 |
|
||||
| `backend/cmd/server/headless_login_e2e_test.go:363` | `TestHeadlessPasswordLogin_E2E_ResponseIncludesDetailedCodeAndLogs` | 실제 app 경로에서 headless login 401 응답 본문과 구조화 로그를 함께 검증 |
|
||||
| `backend/cmd/server/headless_login_e2e_test.go:407` | `TestHeadlessPasswordLogin_E2E_DebugLogsIncludeDiagnostics` | 실제 app 경로에서 debug 레벨일 때만 headless 진단 필드가 로그에 포함되는지 검증 |
|
||||
| `backend/cmd/server/headless_login_e2e_test.go:446` | `TestHeadlessPasswordLogin_E2E_AcceptsForwardedHTTPSAudience` | forwarded proto/host가 있는 경우 `https` absolute audience가 실제 app 경로에서 성공하는지 검증 |
|
||||
| `backend/cmd/server/headless_login_e2e_test.go:491` | `TestHeadlessPasswordLogin_E2E_AcceptsConfiguredPublicHTTPSAudience` | `BACKEND_PUBLIC_URL` 기준으로 internal `http` 요청에서도 `https` absolute audience가 성공하는지 검증 |
|
||||
| `backend/internal/handler/api_key_handler_test.go:19` | `TestApiKeyHandler_CreateApiKey` | 핵심 CRUD/서비스 동작 검증 |
|
||||
| `backend/internal/handler/api_key_handler_test.go:41` | `TestApiKeyHandler_Validation` | 유효성/정책/유틸 검증 |
|
||||
| `backend/internal/handler/auth_handler_async_test.go:198` | `TestSignup_AsyncDB_Isolation` | 복구/격리/회복 탄력성 검증 |
|
||||
@@ -29,8 +31,11 @@
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1033` | `TestHeadlessPasswordLogin_AudienceMismatchReturnsDetailedCode` | headless 로그인 audience mismatch 분류 검증 |
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1062` | `TestHeadlessPasswordLogin_IssSubMismatchReturnsDetailedCode` | headless 로그인 iss/sub mismatch 분류 검증 |
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1102` | `TestHeadlessPasswordLogin_ExpiredAssertionReturnsDetailedCode` | headless 로그인 만료 assertion 분류 검증 |
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1142` | `TestHeadlessPasswordLogin_DebugLogIncludesDiagnostics` | debug 로그에서만 진단 필드가 노출되는지 검증 |
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1174` | `TestHeadlessPasswordLogin_InfoLogOmitsDebugDiagnostics` | 일반 로그에서 debug 진단 필드가 숨겨지는지 검증 |
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1097` | `TestHeadlessPasswordLogin_AcceptsForwardedHTTPSAudience` | forwarded proto/host가 있는 경우 `https` absolute audience가 성공하는지 검증 |
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1132` | `TestHeadlessPasswordLogin_AcceptsConfiguredPublicHTTPSAudience` | `BACKEND_PUBLIC_URL` 기준으로 internal `http` 요청에서도 `https` absolute audience가 성공하는지 검증 |
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1166` | `TestHeadlessPasswordLogin_RejectsHTTPAudienceWhenConfiguredPublicURLIsHTTPS` | `BACKEND_PUBLIC_URL=https`일 때 잘못된 `http` absolute audience는 계속 거부되는지 검증 |
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1280` | `TestHeadlessPasswordLogin_DebugLogIncludesDiagnostics` | debug 로그에서만 진단 필드가 노출되는지 검증 |
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1312` | `TestHeadlessPasswordLogin_InfoLogOmitsDebugDiagnostics` | 일반 로그에서 debug 진단 필드가 숨겨지는지 검증 |
|
||||
| `backend/internal/handler/auth_handler_login_test.go:1442` | `TestPasswordLogin_InvalidCredentials_ReturnsCode` | 비밀번호 불일치 응답 코드 검증 |
|
||||
| `backend/internal/handler/auth_handler_oidc_test.go:106` | `TestAcceptOidcLoginRequest_TokenFallbackToCookie` | 복구/격리/회복 탄력성 검증 |
|
||||
| `backend/internal/handler/auth_handler_oidc_test.go:21` | `TestAcceptOidcLoginRequest_CookieOnly` | 인증/OIDC 플로우 검증 |
|
||||
|
||||
@@ -17,6 +17,7 @@ do
|
||||
assert_contains "$workflow" "APP_ENV=stage"
|
||||
assert_contains "$workflow" "BACKEND_LOG_LEVEL=debug"
|
||||
assert_contains "$workflow" "CLIENT_LOG_DEBUG=true"
|
||||
assert_contains "$workflow" 'BACKEND_PUBLIC_URL=${{ vars.BACKEND_URL }}'
|
||||
done
|
||||
|
||||
echo "staging workflow env checks passed"
|
||||
|
||||
Reference in New Issue
Block a user