From 093d2f2af0497c22b47b43e23f8a001cbde91132 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 27 Apr 2026 11:31:14 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20Desco?= =?UTF-8?q?pe=20=EC=97=B0=EB=8F=99=20=EC=BD=94=EB=93=9C=20=EB=B0=8F=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?(resolves=20#519)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.sample | 1 - .gitea/workflows/staging_code_pull.yml | 1 - .gitea/workflows/staging_release.yml | 1 - adminfront/src/locales/en.toml | 1 - adminfront/src/locales/ko.toml | 1 - backend/go.mod | 10 - backend/go.sum | 21 -- backend/internal/idp/factory.go | 73 ++-- backend/internal/service/descope_service.go | 356 -------------------- devfront/src/locales/en.toml | 1 - devfront/src/locales/ko.toml | 1 - docker-compose.yaml | 2 - docker/staging_pull_compose.template.yaml | 2 - userfront/lib/i18n_data.dart | 2 - 14 files changed, 30 insertions(+), 443 deletions(-) delete mode 100644 backend/internal/service/descope_service.go diff --git a/.env.sample b/.env.sample index a1b24ac8..6d647b2d 100644 --- a/.env.sample +++ b/.env.sample @@ -7,7 +7,6 @@ APP_ENV=stage # 애플리케이션 실행 환경 (dev, stage, production) TZ=Asia/Seoul -# IDP_PROVIDER는 우선순위 순으로 콤마 구분 (예: Kratos/Hydra 우선, Descope 백업) IDP_PROVIDER=ory # --- Infrastructure Ports --- diff --git a/.gitea/workflows/staging_code_pull.yml b/.gitea/workflows/staging_code_pull.yml index c3117125..261175ae 100644 --- a/.gitea/workflows/staging_code_pull.yml +++ b/.gitea/workflows/staging_code_pull.yml @@ -77,7 +77,6 @@ jobs: AUDIT_WORKER_COUNT=5 AUDIT_QUEUE_SIZE=2000 PROFILE_CACHE_TTL=${{ vars.PROFILE_CACHE_TTL }} - DESCOPE_TEST_ACCOUNT=${{ vars.DESCOPE_TEST_ACCOUNT }} NAVER_CLOUD_ACCESS_KEY=${{ vars.NAVER_CLOUD_ACCESS_KEY }} NAVER_CLOUD_SECRET_KEY=${{ secrets.NAVER_CLOUD_SECRET_KEY }} NAVER_CLOUD_SERVICE_ID=${{ vars.NAVER_CLOUD_SERVICE_ID }} diff --git a/.gitea/workflows/staging_release.yml b/.gitea/workflows/staging_release.yml index edbb8d90..2b5a561b 100644 --- a/.gitea/workflows/staging_release.yml +++ b/.gitea/workflows/staging_release.yml @@ -86,7 +86,6 @@ jobs: AUDIT_WORKER_COUNT=5 AUDIT_QUEUE_SIZE=2000 PROFILE_CACHE_TTL=${{ vars.PROFILE_CACHE_TTL }} - DESCOPE_TEST_ACCOUNT=${{ vars.DESCOPE_TEST_ACCOUNT }} NAVER_CLOUD_ACCESS_KEY=${{ vars.NAVER_CLOUD_ACCESS_KEY }} NAVER_CLOUD_SECRET_KEY=${{ secrets.NAVER_CLOUD_SECRET_KEY }} NAVER_CLOUD_SERVICE_ID=${{ vars.NAVER_CLOUD_SERVICE_ID }} diff --git a/adminfront/src/locales/en.toml b/adminfront/src/locales/en.toml index 6606c70f..0315f6d9 100644 --- a/adminfront/src/locales/en.toml +++ b/adminfront/src/locales/en.toml @@ -165,7 +165,6 @@ import_success = "Organization chart imported successfully." [msg.admin.overview] description = "Description" -idp_fallback = "Fallback: Descope" idp_primary = "IDP: Ory primary" [msg.admin.overview.playbook] diff --git a/adminfront/src/locales/ko.toml b/adminfront/src/locales/ko.toml index 092c2f13..da26fc7f 100644 --- a/adminfront/src/locales/ko.toml +++ b/adminfront/src/locales/ko.toml @@ -165,7 +165,6 @@ import_success = "조직도가 성공적으로 임포트되었습니다." [msg.admin.overview] description = "모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다." -idp_fallback = "Fallback: Descope" idp_primary = "IDP: Ory primary" [msg.admin.overview.playbook] diff --git a/backend/go.mod b/backend/go.mod index 10b19519..e2ff89a9 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,7 +10,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/ses v1.34.18 github.com/bwmarrin/snowflake v0.3.0 github.com/coreos/go-oidc/v3 v3.17.0 - github.com/descope/go-sdk v1.7.0 github.com/go-jose/go-jose/v4 v4.1.3 github.com/go-redis/redis/v8 v8.11.5 github.com/gofiber/fiber/v2 v2.52.10 @@ -52,7 +51,6 @@ require ( github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.5.2+incompatible // indirect @@ -65,7 +63,6 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/goccy/go-json v0.10.4 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -74,12 +71,6 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/lestrrat-go/blackmagic v1.0.3 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.6 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -124,7 +115,6 @@ require ( go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20220921023135-46d9e7742f1e // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index c7a29557..c7c80565 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -65,10 +65,6 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/descope/go-sdk v1.7.0 h1:DIRmnA4Q8TDtWdGJ9z0I11+AWMrzyNiiozFH557LrgQ= -github.com/descope/go-sdk v1.7.0/go.mod h1:lCwCgYOfrgjANMsR2BVe1yfX0Siwd2NjNAig0myWZqY= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -100,8 +96,6 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY= github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -142,18 +136,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= -github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= -github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= -github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI= github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= @@ -231,7 +213,6 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= @@ -294,8 +275,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20220921023135-46d9e7742f1e h1:Ctm9yurWsg7aWwIpH9Bnap/IdSVxixymIb3MhiMEQQA= -golang.org/x/exp v0.0.0-20220921023135-46d9e7742f1e/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/backend/internal/idp/factory.go b/backend/internal/idp/factory.go index e8ccba55..b4c59782 100644 --- a/backend/internal/idp/factory.go +++ b/backend/internal/idp/factory.go @@ -11,62 +11,49 @@ import ( "strings" ) -// Ory 계열(kratos/hydra)와 Descope 등 공급자 문자열을 정규화하기 위한 매핑. +// Ory 계열(kratos/hydra) 공급자 문자열을 정규화하기 위한 매핑. var providerAliases = map[string]string{ - "ory": "ory", - "hydra": "ory", - "kratos": "ory", - "ory-kratos": "ory", - "ory_hydra": "ory", - "ory_kratos": "ory", - "descope": "descope", - "descope_sso": "descope", + "ory": "ory", + "hydra": "ory", + "kratos": "ory", + "ory-kratos": "ory", + "ory_hydra": "ory", + "ory_kratos": "ory", } // getEnv는 환경 변수를 읽거나 대체 값을 반환하는 헬퍼 함수입니다. func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback } // InitializeProvider는 환경 설정을 기반으로 IDP 공급자를 생성하고 반환합니다. // 이것은 IdentityProvider 인터페이스의 팩토리 역할을 합니다. func InitializeProvider() (domain.IdentityProvider, error) { - rawProviders := getEnv("IDP_PROVIDER", "ory") - providers := strings.Split(rawProviders, ",") - slog.Info("Initializing IDP chain", "providers", rawProviders) + rawProviders := getEnv("IDP_PROVIDER", "ory") + providers := strings.Split(rawProviders, ",") + slog.Info("Initializing IDP chain", "providers", rawProviders) - var initialized []domain.IdentityProvider - for _, p := range providers { - providerName := strings.TrimSpace(strings.ToLower(p)) - if canonical, ok := providerAliases[providerName]; ok { - providerName = canonical - } + var initialized []domain.IdentityProvider + for _, p := range providers { + providerName := strings.TrimSpace(strings.ToLower(p)) + if canonical, ok := providerAliases[providerName]; ok { + providerName = canonical + } - switch providerName { - case "ory": - // Kratos/Hydra 주 공급자 - oryProvider := service.NewOryProvider() - initialized = append(initialized, oryProvider) - - case "descope": - descopeProjectID := getEnv("DESCOPE_PROJECT_ID", "") - descopeManagementKey := getEnv("DESCOPE_MANAGEMENT_KEY", "") - // 선택된 공급자에 대한 키가 설정되었는지 확인하기 위한 기본 유효성 검사 - if descopeProjectID == "" || descopeManagementKey == "" { - slog.Warn("Skipping Descope provider due to missing credentials") - continue - } - initialized = append(initialized, service.NewDescopeProvider(descopeProjectID, descopeManagementKey)) - - default: - // 알 수 없는 공급자는 건너뛰고 다음 후보를 시도 - slog.Warn("Skipping unsupported IDP provider entry", "provider", providerName) - } - } + switch providerName { + case "ory": + // Kratos/Hydra 주 공급자 + oryProvider := service.NewOryProvider() + initialized = append(initialized, oryProvider) + default: + // 알 수 없는 공급자는 건너뛰고 다음 후보를 시도 + slog.Warn("Skipping unsupported IDP provider entry", "provider", providerName) + } + } if len(initialized) == 0 { return nil, fmt.Errorf("no valid IDP_PROVIDER entries configured from: %s", rawProviders) } diff --git a/backend/internal/service/descope_service.go b/backend/internal/service/descope_service.go deleted file mode 100644 index c5819fd8..00000000 --- a/backend/internal/service/descope_service.go +++ /dev/null @@ -1,356 +0,0 @@ -package service - -import ( - "baron-sso-backend/internal/domain" - "context" - "fmt" - "log/slog" - "net/http" - "os" - "strings" - "time" - - "github.com/descope/go-sdk/descope" - "github.com/descope/go-sdk/descope/client" -) - -type DescopeProvider struct { - Client *client.DescopeClient - FrontendURL string - fieldMapping map[string]string // Key: Broker Field Name, Value: Descope Attribute Key -} - -func NewDescopeProvider(projectID, managementKey string) *DescopeProvider { - var descopeClient *client.DescopeClient - var err error - if projectID != "" { - descopeClient, err = client.NewWithConfig(&client.Config{ - ProjectID: projectID, - ManagementKey: managementKey, - }) - if err != nil { - slog.Warn("Failed to initialize Descope Client in Provider", "error", err) - } - } - - // Define the mapping between BrokerUser fields and Descope attributes. - // In a real scenario, this could be loaded from a config file. - // For this implementation, we hardcode the support to demonstrate the validation. - // We map the Broker's required custom attributes to Descope's keys. - mapping := map[string]string{ - "grade": "customAttributes.userRank", // Broker 'grade' maps to Descope 'userRank' - "department": "customAttributes.dept", // Broker 'department' maps to Descope 'dept' - } - - return &DescopeProvider{ - Client: descopeClient, - FrontendURL: os.Getenv("USERFRONT_URL"), - fieldMapping: mapping, - } -} - -func (d *DescopeProvider) Name() string { - return "Descope" -} - -// GetMetadata returns the schema support information. -// Currently, it returns the standard fields Descope supports + the mapped custom attributes. -func (d *DescopeProvider) GetMetadata() (*domain.IDPMetadata, error) { - // 1. Standard Fields supported by Descope - supported := []string{"id", "email", "name", "phone_number"} - - // 2. Add mapped custom attributes - // The Validator checks if the Broker's required keys (e.g., "grade") are present in this list. - for brokerKey := range d.fieldMapping { - supported = append(supported, brokerKey) - } - - return &domain.IDPMetadata{ - SupportedFields: supported, - }, nil -} - -// CreateUser는 Descope Management API를 사용해 사용자를 생성합니다. -func (d *DescopeProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) { - if d.Client == nil { - return "", fmt.Errorf("descope provider: client is nil") - } - if user == nil { - return "", fmt.Errorf("descope provider: user payload is nil") - } - if user.Email == "" || password == "" { - return "", fmt.Errorf("descope provider: email and password are required") - } - - normalizedPhone := user.PhoneNumber - normalizedPhone = strings.ReplaceAll(normalizedPhone, "-", "") - normalizedPhone = strings.ReplaceAll(normalizedPhone, " ", "") - if strings.HasPrefix(normalizedPhone, "010") { - normalizedPhone = "+82" + normalizedPhone[1:] - } else if strings.HasPrefix(normalizedPhone, "82") { - normalizedPhone = "+" + normalizedPhone - } - - // 존재 여부 확인 - exists, _ := d.Client.Management.User().Load(context.Background(), user.Email) - if exists != nil { - return "", fmt.Errorf("descope provider: user already exists") - } - - descopeUser := &descope.UserRequest{ - User: descope.User{ - Email: user.Email, - Name: user.Name, - Phone: normalizedPhone, - }, - } - descopeUser.CustomAttributes = map[string]any{} - for k, v := range user.Attributes { - descopeUser.CustomAttributes[k] = v - } - descopeUser.CustomAttributes["createdAt"] = time.Now().Format(time.RFC3339) - - if _, err := d.Client.Management.User().Create(context.Background(), user.Email, descopeUser); err != nil { - return "", fmt.Errorf("descope provider: create user failed: %w", err) - } - if err := d.Client.Management.User().SetPassword(context.Background(), user.Email, password); err != nil { - _ = d.Client.Management.User().Delete(context.Background(), user.Email) - return "", fmt.Errorf("descope provider: set password failed: %w", err) - } - - slog.Info("Descope user created", "email", user.Email) - return user.Email, nil -} - -// SignIn은 Descope Password 로그인 후 세션 토큰을 반환합니다. -func (d *DescopeProvider) SignIn(loginID, password string) (*domain.AuthInfo, error) { - if d.Client == nil { - return nil, fmt.Errorf("descope provider: client is nil") - } - authInfo, err := d.Client.Auth.Password().SignIn(context.Background(), loginID, password, nil) - if err != nil { - return nil, err - } - - res := &domain.AuthInfo{ - SessionToken: &domain.Token{ - JWT: authInfo.SessionToken.JWT, - Expiration: time.Unix(authInfo.SessionToken.Expiration, 0), - SessionID: authInfo.SessionToken.ID, - }, - // 내부 식별자는 Kratos identity ID로 통일합니다. - Subject: "", - } - if authInfo.RefreshToken != nil { - res.RefreshToken = &domain.Token{ - JWT: authInfo.RefreshToken.JWT, - Expiration: time.Unix(authInfo.RefreshToken.Expiration, 0), - } - } - return res, nil -} - -// UserExists는 loginID(이메일/전화번호) 기준으로 사용자가 있는지 확인합니다. -func (d *DescopeProvider) UserExists(loginID string) (bool, error) { - if d.Client == nil { - return false, fmt.Errorf("descope provider: client is nil") - } - - ctx := context.Background() - if strings.Contains(loginID, "@") { - user, err := d.Client.Management.User().Load(ctx, loginID) - if err != nil { - if isDescopeNotFound(err) { - return false, nil - } - return false, err - } - return user != nil, nil - } - - phone := normalizePhone(loginID) - searchOptions := &descope.UserSearchOptions{ - Phones: []string{phone}, - Limit: 1, - } - users, _, err := d.Client.Management.User().SearchAll(ctx, searchOptions) - if err != nil { - return false, err - } - return len(users) > 0, nil -} - -// IssueSession은 비밀번호 없이 로그인 세션을 발급합니다. -func (d *DescopeProvider) IssueSession(loginID string) (*domain.AuthInfo, error) { - if d.Client == nil { - return nil, fmt.Errorf("descope provider: client is nil") - } - ctx := context.Background() - - targetLoginID, err := d.resolveLoginID(loginID) - if err != nil { - return nil, err - } - - embeddedToken, err := d.Client.Management.User().GenerateEmbeddedLink(ctx, targetLoginID, nil, 0) - if err != nil { - return nil, fmt.Errorf("descope provider: generate embedded link failed: %w", err) - } - - authInfo, err := d.Client.Auth.MagicLink().Verify(ctx, embeddedToken, nil) - if err != nil { - return nil, fmt.Errorf("descope provider: magic link verify failed: %w", err) - } - - res := &domain.AuthInfo{ - SessionToken: &domain.Token{ - JWT: authInfo.SessionToken.JWT, - Expiration: time.Unix(authInfo.SessionToken.Expiration, 0), - SessionID: authInfo.SessionToken.ID, - }, - // 내부 식별자는 Kratos identity ID로 통일합니다. - Subject: "", - } - if authInfo.RefreshToken != nil { - res.RefreshToken = &domain.Token{ - JWT: authInfo.RefreshToken.JWT, - Expiration: time.Unix(authInfo.RefreshToken.Expiration, 0), - } - } - return res, nil -} - -func (d *DescopeProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) { - return nil, domain.ErrNotSupported -} - -func (d *DescopeProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) { - return nil, domain.ErrNotSupported -} - -// GetPasswordPolicy는 Descope 비밀번호 정책을 반환합니다. -func (d *DescopeProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) { - if d.Client == nil { - return nil, fmt.Errorf("descope provider: client is nil") - } - policy, err := d.Client.Auth.Password().GetPasswordPolicy(context.Background()) - if err != nil { - return nil, err - } - return &domain.PasswordPolicy{ - MinLength: int(policy.MinLength), - Lowercase: policy.Lowercase, - Uppercase: policy.Uppercase, - Number: policy.Number, - NonAlphanumeric: policy.NonAlphanumeric, - MinCharacterTypes: 0, - }, nil -} - -func (d *DescopeProvider) InitiatePasswordReset(loginID, redirectUrl string) error { - ctx := context.Background() - err := d.Client.Auth.Password().SendPasswordReset(ctx, loginID, redirectUrl, nil) - if err != nil { - slog.Error("Descope SendPasswordReset failed (raw)", - "loginID", loginID, - "redirectUrl", redirectUrl, - "err", err, - "err_type", fmt.Sprintf("%T", err), - ) - - if de, ok := err.(*descope.Error); ok { - status := de.Info[descope.ErrorInfoKeys.HTTPResponseStatusCode] // "Status-Code" - slog.Error("Descope error details", - "code", de.Code, - "description", de.Description, - "message", de.Message, - "status_code", status, - "info", de.Info, - ) - } - } - return err -} - -func (d *DescopeProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) { - ctx := context.Background() - authInfo, err := d.Client.Auth.MagicLink().Verify(ctx, token, nil) - if err != nil { - return nil, err - } - - res := &domain.AuthInfo{ - SessionToken: &domain.Token{ - JWT: authInfo.SessionToken.JWT, - Expiration: time.Unix(authInfo.SessionToken.Expiration, 0), - SessionID: authInfo.SessionToken.ID, - }, - } - if authInfo.RefreshToken != nil { - res.RefreshToken = &domain.Token{ - JWT: authInfo.RefreshToken.JWT, - Expiration: time.Unix(authInfo.RefreshToken.Expiration, 0), - } - } - - return res, nil -} - -func (d *DescopeProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error { - ctx := context.Background() - return d.Client.Auth.Password().UpdateUserPassword(ctx, loginID, newPassword, r) -} - -func (d *DescopeProvider) resolveLoginID(loginID string) (string, error) { - if strings.Contains(loginID, "@") { - return loginID, nil - } - - phone := normalizePhone(loginID) - searchOptions := &descope.UserSearchOptions{ - Phones: []string{phone}, - Limit: 1, - } - users, _, err := d.Client.Management.User().SearchAll(context.Background(), searchOptions) - if err != nil { - return "", fmt.Errorf("descope provider: user search failed: %w", err) - } - if len(users) == 0 { - return "", fmt.Errorf("descope provider: user not found") - } - if len(users[0].LoginIDs) > 0 { - return users[0].LoginIDs[0], nil - } - if users[0].UserID != "" { - return users[0].UserID, nil - } - return "", fmt.Errorf("descope provider: user found but login id missing") -} - -func normalizePhone(phone string) string { - normalized := strings.ReplaceAll(phone, "-", "") - normalized = strings.ReplaceAll(normalized, " ", "") - if strings.HasPrefix(normalized, "010") { - return "+82" + normalized[1:] - } - if strings.HasPrefix(normalized, "82") { - return "+" + normalized - } - return normalized -} - -func isDescopeNotFound(err error) bool { - if de, ok := err.(*descope.Error); ok { - if rawStatus, ok := de.Info[descope.ErrorInfoKeys.HTTPResponseStatusCode]; ok { - switch v := rawStatus.(type) { - case int: - return v == http.StatusNotFound - case float64: - return int(v) == http.StatusNotFound - case string: - return v == fmt.Sprintf("%d", http.StatusNotFound) - } - } - } - return false -} diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index 5a3280c0..e68489f4 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -165,7 +165,6 @@ import_success = "Organization chart imported successfully." [msg.admin.overview] description = "Description" -idp_fallback = "Fallback: Descope" idp_primary = "IDP: Ory primary" [msg.admin.overview.playbook] diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index 0cc7f832..6837d1ae 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -165,7 +165,6 @@ import_success = "조직도가 성공적으로 임포트되었습니다." [msg.admin.overview] description = "모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다." -idp_fallback = "Fallback: Descope" idp_primary = "IDP: Ory primary" [msg.admin.overview.playbook] diff --git a/docker-compose.yaml b/docker-compose.yaml index b4920b0b..e9c823aa 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,8 +11,6 @@ services: - GO_ENV=${APP_ENV:-development} - COOKIE_SECRET=${COOKIE_SECRET} - JWT_SECRET=${JWT_SECRET} - - DESCOPE_PROJECT_ID=${DESCOPE_PROJECT_ID:-} - - DESCOPE_MANAGEMENT_KEY=${DESCOPE_MANAGEMENT_KEY:-} - NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY} - NAVER_CLOUD_SECRET_KEY=${NAVER_CLOUD_SECRET_KEY} - NAVER_CLOUD_SERVICE_ID=${NAVER_CLOUD_SERVICE_ID} diff --git a/docker/staging_pull_compose.template.yaml b/docker/staging_pull_compose.template.yaml index 5da9eb02..e65cb04d 100644 --- a/docker/staging_pull_compose.template.yaml +++ b/docker/staging_pull_compose.template.yaml @@ -335,8 +335,6 @@ services: - GO_ENV=${APP_ENV:-development} - COOKIE_SECRET=${COOKIE_SECRET} - JWT_SECRET=${JWT_SECRET} - - DESCOPE_PROJECT_ID=${DESCOPE_PROJECT_ID:-} - - DESCOPE_MANAGEMENT_KEY=${DESCOPE_MANAGEMENT_KEY:-} - NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY} - NAVER_CLOUD_SECRET_KEY=${NAVER_CLOUD_SECRET_KEY} - NAVER_CLOUD_SERVICE_ID=${NAVER_CLOUD_SERVICE_ID} diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index f28934f8..8d7f7d8e 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -105,7 +105,6 @@ const Map koStrings = { "msg.admin.org.import_error": "조직도 임포트 중 오류가 발생했습니다.", "msg.admin.org.import_success": "조직도가 성공적으로 임포트되었습니다.", "msg.admin.overview.description": "모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다.", - "msg.admin.overview.idp_fallback": "Fallback: Descope", "msg.admin.overview.idp_primary": "IDP: Ory primary", "msg.admin.overview.playbook.description": "운영 정책, 레이트리밋, 감사 로그의 기본 룰을 요약합니다.", @@ -1980,7 +1979,6 @@ const Map enStrings = { "msg.admin.org.import_success": "Organization chart imported successfully.", "msg.admin.overview.description": "Review shared metrics and policy status across all tenants in one place.", - "msg.admin.overview.idp_fallback": "Fallback: Descope", "msg.admin.overview.idp_primary": "IDP: Ory primary", "msg.admin.overview.playbook.description": "Operational guardrails and architecture decisions for the admin control plane.",