forked from baron/baron-sso
Trusted RP 생성 흐름 테스트 추가
This commit is contained in:
60
backend/internal/domain/hydra_models_test.go
Normal file
60
backend/internal/domain/hydra_models_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestHydraClient_TrustedRPFlags(t *testing.T) {
|
||||||
|
t.Run("inline jwks with private_key_jwt and headless enabled", func(t *testing.T) {
|
||||||
|
client := HydraClient{
|
||||||
|
TokenEndpointAuthMethod: "private_key_jwt",
|
||||||
|
JWKS: map[string]any{
|
||||||
|
"keys": []map[string]any{{
|
||||||
|
"kty": "RSA",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"headless_login_enabled": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !client.IsTrustedRP() {
|
||||||
|
t.Fatalf("expected trusted rp")
|
||||||
|
}
|
||||||
|
if !client.IsHeadlessLoginEnabled() {
|
||||||
|
t.Fatalf("expected headless login enabled")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("jwks uri without private_key_jwt is not trusted", func(t *testing.T) {
|
||||||
|
client := HydraClient{
|
||||||
|
TokenEndpointAuthMethod: "none",
|
||||||
|
JWKSUri: "https://rp.example.com/.well-known/jwks.json",
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"headless_login_enabled": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.IsTrustedRP() {
|
||||||
|
t.Fatalf("expected untrusted rp")
|
||||||
|
}
|
||||||
|
if client.IsHeadlessLoginEnabled() {
|
||||||
|
t.Fatalf("expected headless login disabled when client is not trusted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("trusted rp without boolean metadata flag is not headless enabled", func(t *testing.T) {
|
||||||
|
client := HydraClient{
|
||||||
|
TokenEndpointAuthMethod: "private_key_jwt",
|
||||||
|
JWKSUri: "https://rp.example.com/.well-known/jwks.json",
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"headless_login_enabled": "true",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !client.IsTrustedRP() {
|
||||||
|
t.Fatalf("expected trusted rp")
|
||||||
|
}
|
||||||
|
if client.IsHeadlessLoginEnabled() {
|
||||||
|
t.Fatalf("expected headless login disabled for non-bool metadata")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -610,6 +611,156 @@ func TestDevHandler_NoAuditNoAction(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateClient_TrustedRPPayloadMapping(t *testing.T) {
|
||||||
|
var captured domain.HydraClient
|
||||||
|
|
||||||
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.Method == http.MethodPost && r.URL.Path == "/clients" {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = json.Unmarshal(body, &captured)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
return httpJSONAny(r, http.StatusCreated, map[string]any{
|
||||||
|
"client_id": captured.ClientID,
|
||||||
|
"client_name": captured.ClientName,
|
||||||
|
"redirect_uris": captured.RedirectURIs,
|
||||||
|
"grant_types": captured.GrantTypes,
|
||||||
|
"response_types": captured.ResponseTypes,
|
||||||
|
"scope": captured.Scope,
|
||||||
|
"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
|
||||||
|
"jwks": captured.JWKS,
|
||||||
|
"metadata": captured.Metadata,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
h := &DevHandler{
|
||||||
|
Hydra: &service.HydraAdminService{
|
||||||
|
AdminURL: "http://hydra.test",
|
||||||
|
PublicURL: "http://hydra.public",
|
||||||
|
HTTPClient: &http.Client{Transport: transport},
|
||||||
|
},
|
||||||
|
Keto: new(devMockKetoService),
|
||||||
|
}
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
app.Post("/api/v1/dev/clients", h.CreateClient)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "Trusted RP App",
|
||||||
|
"type": "pkce",
|
||||||
|
"redirectUris": []string{"https://rp.example.com/callback"},
|
||||||
|
"scopes": []string{"openid", "profile"},
|
||||||
|
"tokenEndpointAuthMethod": "private_key_jwt",
|
||||||
|
"jwks": map[string]any{
|
||||||
|
"keys": []map[string]any{{
|
||||||
|
"kty": "RSA",
|
||||||
|
"alg": "RS256",
|
||||||
|
"n": "AQIDBAUGBw",
|
||||||
|
"e": "AQAB",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"headless_login_enabled": true,
|
||||||
|
"request_object_signing_alg": "RS256",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, _ := app.Test(req, -1)
|
||||||
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||||
|
assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
|
||||||
|
assert.NotNil(t, captured.JWKS)
|
||||||
|
assert.True(t, captured.IsTrustedRP())
|
||||||
|
assert.True(t, captured.IsHeadlessLoginEnabled())
|
||||||
|
assert.Equal(t, true, captured.Metadata["headless_login_enabled"])
|
||||||
|
assert.Equal(t, "RS256", captured.Metadata["request_object_signing_alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateClient_TrustedRPPayloadMapping(t *testing.T) {
|
||||||
|
var captured domain.HydraClient
|
||||||
|
|
||||||
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-trusted" {
|
||||||
|
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||||
|
"client_id": "client-trusted",
|
||||||
|
"client_name": "Trusted Before",
|
||||||
|
"redirect_uris": []string{"https://before.example.com/callback"},
|
||||||
|
"grant_types": []string{"authorization_code", "refresh_token"},
|
||||||
|
"response_types": []string{"code"},
|
||||||
|
"scope": "openid profile",
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"status": "active",
|
||||||
|
},
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodPut && r.URL.Path == "/clients/client-trusted" {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = json.Unmarshal(body, &captured)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||||
|
"client_id": captured.ClientID,
|
||||||
|
"client_name": captured.ClientName,
|
||||||
|
"redirect_uris": captured.RedirectURIs,
|
||||||
|
"grant_types": captured.GrantTypes,
|
||||||
|
"response_types": captured.ResponseTypes,
|
||||||
|
"scope": captured.Scope,
|
||||||
|
"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
|
||||||
|
"jwks_uri": captured.JWKSUri,
|
||||||
|
"metadata": captured.Metadata,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
h := &DevHandler{
|
||||||
|
Hydra: &service.HydraAdminService{
|
||||||
|
AdminURL: "http://hydra.test",
|
||||||
|
PublicURL: "http://hydra.public",
|
||||||
|
HTTPClient: &http.Client{Transport: transport},
|
||||||
|
},
|
||||||
|
Keto: new(devMockKetoService),
|
||||||
|
}
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
app.Put("/api/v1/dev/clients/:id", h.UpdateClient)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "Trusted After",
|
||||||
|
"type": "pkce",
|
||||||
|
"tokenEndpointAuthMethod": "private_key_jwt",
|
||||||
|
"jwksUri": "https://rp.example.com/.well-known/jwks.json",
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"headless_login_enabled": true,
|
||||||
|
"request_object_signing_alg": "RS256",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-trusted", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, _ := app.Test(req, -1)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
assert.Equal(t, "private_key_jwt", captured.TokenEndpointAuthMethod)
|
||||||
|
assert.Equal(t, "https://rp.example.com/.well-known/jwks.json", captured.JWKSUri)
|
||||||
|
assert.True(t, captured.IsTrustedRP())
|
||||||
|
assert.True(t, captured.IsHeadlessLoginEnabled())
|
||||||
|
assert.Equal(t, true, captured.Metadata["headless_login_enabled"])
|
||||||
|
}
|
||||||
|
|
||||||
func TestListAuditLogs_TenantMemberForbidden(t *testing.T) {
|
func TestListAuditLogs_TenantMemberForbidden(t *testing.T) {
|
||||||
h := &DevHandler{
|
h := &DevHandler{
|
||||||
Hydra: &service.HydraAdminService{AdminURL: "http://hydra.test"},
|
Hydra: &service.HydraAdminService{AdminURL: "http://hydra.test"},
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
} from "./helpers/devfront-fixtures";
|
} from "./helpers/devfront-fixtures";
|
||||||
|
|
||||||
const appNamePlaceholder = /My Awesome Application|예: 멋진 애플리케이션/i;
|
const appNamePlaceholder = /My Awesome Application|예: 멋진 애플리케이션/i;
|
||||||
|
const sshRsaPublicKey =
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAABwECAwQFBgc= test@example";
|
||||||
|
|
||||||
test.describe("DevFront clients lifecycle", () => {
|
test.describe("DevFront clients lifecycle", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@@ -120,4 +122,69 @@ test.describe("DevFront clients lifecycle", () => {
|
|||||||
page.getByRole("textbox", { name: /인증 콜백 URL|Callback/i }),
|
page.getByRole("textbox", { name: /인증 콜백 URL|Callback/i }),
|
||||||
).toHaveValue(/https:\/\/after\.example\.com\/callback/);
|
).toHaveValue(/https:\/\/after\.example\.com\/callback/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("pkce trusted rp with inline ssh-rsa key should persist mapped payload", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const state = {
|
||||||
|
clients: [makeClient("client-trusted", { name: "Trusted App", type: "pkce" })],
|
||||||
|
consents: [] as Consent[],
|
||||||
|
auditLogsByCursor: undefined,
|
||||||
|
};
|
||||||
|
await installDevApiMock(page, state);
|
||||||
|
|
||||||
|
await page.goto("/clients/client-trusted/settings");
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole("switch", {
|
||||||
|
name: /Trusted RP \(자체 로그인 UI 사용\)|Trusted RP \(Custom Login UI\)/i,
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", {
|
||||||
|
name: /공개키 등록|Public Key Registration/i,
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByPlaceholder(
|
||||||
|
/ssh-rsa AAA\.\.\.|Paste an 'ssh-rsa AAA\.\.\.' public key first/i,
|
||||||
|
)
|
||||||
|
.fill(sshRsaPublicKey);
|
||||||
|
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
|
||||||
|
|
||||||
|
await expect.poll(() => state.clients[0]?.tokenEndpointAuthMethod).toBe(
|
||||||
|
"private_key_jwt",
|
||||||
|
);
|
||||||
|
await expect
|
||||||
|
.poll(() => state.clients[0]?.metadata?.headless_login_enabled)
|
||||||
|
.toBe(true);
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() =>
|
||||||
|
(state.clients[0]?.jwks as { keys?: Array<{ kty?: string; alg?: string }> })
|
||||||
|
?.keys?.[0]?.kty,
|
||||||
|
)
|
||||||
|
.toBe("RSA");
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() =>
|
||||||
|
(state.clients[0]?.jwks as { keys?: Array<{ kty?: string; alg?: string }> })
|
||||||
|
?.keys?.[0]?.alg,
|
||||||
|
)
|
||||||
|
.toBe("RS256");
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", {
|
||||||
|
name: /공개키 등록|Public Key Registration/i,
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByPlaceholder(
|
||||||
|
/ssh-rsa AAA\.\.\.|Paste an 'ssh-rsa AAA\.\.\.' public key first/i,
|
||||||
|
),
|
||||||
|
).toHaveValue(/"kty": "RSA"/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export type Client = {
|
|||||||
scopes: string[];
|
scopes: string[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
clientSecret?: string;
|
clientSecret?: string;
|
||||||
|
tokenEndpointAuthMethod?: string;
|
||||||
|
jwksUri?: string;
|
||||||
|
jwks?: Record<string, unknown> | string;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,6 +217,9 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
|||||||
status?: ClientStatus;
|
status?: ClientStatus;
|
||||||
redirectUris?: string[];
|
redirectUris?: string[];
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
|
tokenEndpointAuthMethod?: string;
|
||||||
|
jwksUri?: string;
|
||||||
|
jwks?: Record<string, unknown> | string;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}) || { name: "created app" };
|
}) || { name: "created app" };
|
||||||
|
|
||||||
@@ -223,6 +229,9 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
|||||||
status: payload.status ?? "active",
|
status: payload.status ?? "active",
|
||||||
redirectUris: payload.redirectUris ?? [],
|
redirectUris: payload.redirectUris ?? [],
|
||||||
scopes: payload.scopes ?? ["openid"],
|
scopes: payload.scopes ?? ["openid"],
|
||||||
|
tokenEndpointAuthMethod: payload.tokenEndpointAuthMethod,
|
||||||
|
jwksUri: payload.jwksUri,
|
||||||
|
jwks: payload.jwks,
|
||||||
metadata: payload.metadata ?? {},
|
metadata: payload.metadata ?? {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -294,6 +303,9 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
|||||||
type?: ClientType;
|
type?: ClientType;
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
redirectUris?: string[];
|
redirectUris?: string[];
|
||||||
|
tokenEndpointAuthMethod?: string;
|
||||||
|
jwksUri?: string;
|
||||||
|
jwks?: Record<string, unknown> | string;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}) || { name: "updated app" };
|
}) || { name: "updated app" };
|
||||||
const found = state.clients.find((client) => client.id === clientId);
|
const found = state.clients.find((client) => client.id === clientId);
|
||||||
@@ -302,6 +314,15 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
|||||||
if (payload.type) found.type = payload.type;
|
if (payload.type) found.type = payload.type;
|
||||||
if (payload.scopes) found.scopes = payload.scopes;
|
if (payload.scopes) found.scopes = payload.scopes;
|
||||||
if (payload.redirectUris) found.redirectUris = payload.redirectUris;
|
if (payload.redirectUris) found.redirectUris = payload.redirectUris;
|
||||||
|
if (payload.tokenEndpointAuthMethod !== undefined) {
|
||||||
|
found.tokenEndpointAuthMethod = payload.tokenEndpointAuthMethod;
|
||||||
|
}
|
||||||
|
if (payload.jwksUri !== undefined) {
|
||||||
|
found.jwksUri = payload.jwksUri;
|
||||||
|
}
|
||||||
|
if (payload.jwks !== undefined) {
|
||||||
|
found.jwks = payload.jwks;
|
||||||
|
}
|
||||||
if (payload.metadata) found.metadata = payload.metadata;
|
if (payload.metadata) found.metadata = payload.metadata;
|
||||||
appendAuditLog("CLIENT_UPDATE", "UPDATE_CLIENT", clientId);
|
appendAuditLog("CLIENT_UPDATE", "UPDATE_CLIENT", clientId);
|
||||||
return json(route, {
|
return json(route, {
|
||||||
|
|||||||
Reference in New Issue
Block a user