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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user