forked from baron/baron-sso
refactor: 미사용 Descope 연동 코드 및 환경 변수 제거 (resolves #519)
This commit is contained in:
@@ -7,7 +7,6 @@ APP_ENV=stage # 애플리케이션 실행 환경 (dev, stage, production)
|
||||
TZ=Asia/Seoul
|
||||
|
||||
|
||||
# IDP_PROVIDER는 우선순위 순으로 콤마 구분 (예: Kratos/Hydra 우선, Descope 백업)
|
||||
IDP_PROVIDER=ory
|
||||
|
||||
# --- Infrastructure Ports ---
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -165,7 +165,6 @@ import_success = "조직도가 성공적으로 임포트되었습니다."
|
||||
|
||||
[msg.admin.overview]
|
||||
description = "모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다."
|
||||
idp_fallback = "Fallback: Descope"
|
||||
idp_primary = "IDP: Ory primary"
|
||||
|
||||
[msg.admin.overview.playbook]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -165,7 +165,6 @@ import_success = "조직도가 성공적으로 임포트되었습니다."
|
||||
|
||||
[msg.admin.overview]
|
||||
description = "모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다."
|
||||
idp_fallback = "Fallback: Descope"
|
||||
idp_primary = "IDP: Ory primary"
|
||||
|
||||
[msg.admin.overview.playbook]
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -105,7 +105,6 @@ const Map<String, String> 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<String, String> 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.",
|
||||
|
||||
Reference in New Issue
Block a user