forked from baron/baron-sso
Implement tenant import and RP auto login policies
This commit is contained in:
@@ -85,20 +85,21 @@ const (
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
SmsService domain.SmsService
|
||||
EmailService domain.EmailService
|
||||
RedisService domain.RedisRepository
|
||||
HeadlessJWKS *service.HeadlessJWKSCacheService
|
||||
KratosAdmin service.KratosAdminService
|
||||
IdpProvider domain.IdentityProvider
|
||||
AuditRepo domain.AuditRepository
|
||||
OathkeeperRepo domain.OathkeeperLogRepository
|
||||
Hydra *service.HydraAdminService
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
KetoOutboxRepo repository.KetoOutboxRepository
|
||||
UserRepo repository.UserRepository
|
||||
ConsentRepo repository.ClientConsentRepository
|
||||
SmsService domain.SmsService
|
||||
EmailService domain.EmailService
|
||||
RedisService domain.RedisRepository
|
||||
HeadlessJWKS *service.HeadlessJWKSCacheService
|
||||
KratosAdmin service.KratosAdminService
|
||||
IdpProvider domain.IdentityProvider
|
||||
AuditRepo domain.AuditRepository
|
||||
OathkeeperRepo domain.OathkeeperLogRepository
|
||||
Hydra *service.HydraAdminService
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
KetoOutboxRepo repository.KetoOutboxRepository
|
||||
UserRepo repository.UserRepository
|
||||
ConsentRepo repository.ClientConsentRepository
|
||||
RPUserMetadataRepo repository.RPUserMetadataRepository
|
||||
}
|
||||
|
||||
type signupState struct {
|
||||
@@ -1157,6 +1158,120 @@ func withOidcSessionMetadata(claims map[string]any, sessionID string) map[string
|
||||
return claims
|
||||
}
|
||||
|
||||
func (h *AuthHandler) withRPProfileClaims(ctx context.Context, claims map[string]any, client domain.HydraClient, subject string) map[string]any {
|
||||
if claims == nil {
|
||||
claims = map[string]any{}
|
||||
}
|
||||
if h == nil || h.RPUserMetadataRepo == nil {
|
||||
return claims
|
||||
}
|
||||
|
||||
clientID := strings.TrimSpace(client.ClientID)
|
||||
subject = strings.TrimSpace(subject)
|
||||
if clientID == "" || subject == "" {
|
||||
return claims
|
||||
}
|
||||
|
||||
claimKeys := extractClaimEnabledCustomUserSchemaKeys(client.Metadata)
|
||||
if len(claimKeys) == 0 {
|
||||
return claims
|
||||
}
|
||||
|
||||
row, err := h.RPUserMetadataRepo.Get(ctx, clientID, subject)
|
||||
if err != nil || row == nil || len(row.Metadata) == 0 {
|
||||
return claims
|
||||
}
|
||||
|
||||
fields := make(map[string]any)
|
||||
for _, key := range claimKeys {
|
||||
raw, ok := row.Metadata[key]
|
||||
if !ok || raw == nil {
|
||||
continue
|
||||
}
|
||||
if value, ok := raw.(string); ok {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
fields[key] = value
|
||||
continue
|
||||
}
|
||||
fields[key] = raw
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return claims
|
||||
}
|
||||
|
||||
profile := map[string]any{
|
||||
"client_id": clientID,
|
||||
"fields": fields,
|
||||
}
|
||||
if existing, ok := claims["rp_profiles"].([]any); ok {
|
||||
claims["rp_profiles"] = append(existing, profile)
|
||||
return claims
|
||||
}
|
||||
if existing, ok := claims["rp_profiles"].([]interface{}); ok {
|
||||
claims["rp_profiles"] = append(existing, profile)
|
||||
return claims
|
||||
}
|
||||
claims["rp_profiles"] = []any{profile}
|
||||
return claims
|
||||
}
|
||||
|
||||
func extractClaimEnabledCustomUserSchemaKeys(metadata map[string]interface{}) []string {
|
||||
if metadata == nil {
|
||||
return nil
|
||||
}
|
||||
rawSchema, ok := metadata["customUserSchema"]
|
||||
if !ok || rawSchema == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var items []interface{}
|
||||
switch schema := rawSchema.(type) {
|
||||
case []interface{}:
|
||||
items = schema
|
||||
case []map[string]interface{}:
|
||||
items = make([]interface{}, 0, len(schema))
|
||||
for _, item := range schema {
|
||||
items = append(items, item)
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(items))
|
||||
seen := make(map[string]struct{})
|
||||
for _, item := range items {
|
||||
field, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
if typed, typedOK := item.(map[string]any); typedOK {
|
||||
field = typed
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
enabled, _ := field["claimEnabled"].(bool)
|
||||
if !enabled {
|
||||
enabled, _ = field["claim_enabled"].(bool)
|
||||
}
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
key, _ := field["key"].(string)
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func collectEmailList(traits map[string]any, primaryEmail string) []string {
|
||||
emails := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
@@ -4792,6 +4907,8 @@ type linkedRpSummary struct {
|
||||
Logo string `json:"logo,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
InitURL string `json:"init_url,omitempty"`
|
||||
AutoLoginSupported bool `json:"auto_login_supported"`
|
||||
AutoLoginURL string `json:"auto_login_url,omitempty"`
|
||||
LastAuthenticatedAt string `json:"lastAuthenticatedAt,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
@@ -4872,19 +4989,23 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
if len(scopes) == 0 && strings.TrimSpace(client.Scope) != "" {
|
||||
scopes = strings.Fields(client.Scope)
|
||||
}
|
||||
initURL := resolveLinkedRPInitURL(client.ClientID, scopes, client.RedirectURIs)
|
||||
autoLoginSupported := resolveLinkedRPAutoLoginSupported(client.ClientID, client.Metadata)
|
||||
autoLoginURL := resolveLinkedRPAutoLoginURL(client.ClientID, client.Metadata)
|
||||
initURL := resolveLinkedRPInitURL(client.ClientID, client.Metadata)
|
||||
|
||||
existing := records[clientID]
|
||||
if existing == nil {
|
||||
records[clientID] = &linkedRpRecord{
|
||||
linkedRpSummary: linkedRpSummary{
|
||||
ID: clientID,
|
||||
Name: name,
|
||||
Logo: extractHydraClientLogo(client.Metadata),
|
||||
URL: clientURL,
|
||||
InitURL: initURL,
|
||||
Status: "active", // Hydra 세션이 있으면 활성
|
||||
Scopes: scopes,
|
||||
ID: clientID,
|
||||
Name: name,
|
||||
Logo: extractHydraClientLogo(client.Metadata),
|
||||
URL: clientURL,
|
||||
InitURL: initURL,
|
||||
AutoLoginSupported: autoLoginSupported,
|
||||
AutoLoginURL: autoLoginURL,
|
||||
Status: "active", // Hydra 세션이 있으면 활성
|
||||
Scopes: scopes,
|
||||
},
|
||||
lastAuth: lastAuth,
|
||||
}
|
||||
@@ -4903,6 +5024,12 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
if existing.InitURL == "" {
|
||||
existing.InitURL = initURL
|
||||
}
|
||||
if !existing.AutoLoginSupported {
|
||||
existing.AutoLoginSupported = autoLoginSupported
|
||||
}
|
||||
if existing.AutoLoginURL == "" {
|
||||
existing.AutoLoginURL = autoLoginURL
|
||||
}
|
||||
existing.Scopes = mergeScopes(existing.Scopes, scopes)
|
||||
if lastAuth.After(existing.lastAuth) {
|
||||
existing.lastAuth = lastAuth
|
||||
@@ -4943,11 +5070,13 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
)
|
||||
}
|
||||
if record.InitURL == "" {
|
||||
record.InitURL = resolveLinkedRPInitURL(
|
||||
client.ClientID,
|
||||
record.Scopes,
|
||||
client.RedirectURIs,
|
||||
)
|
||||
record.InitURL = resolveLinkedRPInitURL(client.ClientID, client.Metadata)
|
||||
}
|
||||
if !record.AutoLoginSupported {
|
||||
record.AutoLoginSupported = resolveLinkedRPAutoLoginSupported(client.ClientID, client.Metadata)
|
||||
}
|
||||
if record.AutoLoginURL == "" {
|
||||
record.AutoLoginURL = resolveLinkedRPAutoLoginURL(client.ClientID, client.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4999,21 +5128,21 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
client.ClientURI,
|
||||
client.RedirectURIs,
|
||||
)
|
||||
initURL := resolveLinkedRPInitURL(
|
||||
client.ClientID,
|
||||
dc.GrantedScopes,
|
||||
client.RedirectURIs,
|
||||
)
|
||||
autoLoginSupported := resolveLinkedRPAutoLoginSupported(client.ClientID, client.Metadata)
|
||||
autoLoginURL := resolveLinkedRPAutoLoginURL(client.ClientID, client.Metadata)
|
||||
initURL := resolveLinkedRPInitURL(client.ClientID, client.Metadata)
|
||||
|
||||
records[dc.ClientID] = &linkedRpRecord{
|
||||
linkedRpSummary: linkedRpSummary{
|
||||
ID: dc.ClientID,
|
||||
Name: name,
|
||||
Logo: extractHydraClientLogo(client.Metadata),
|
||||
URL: clientURL,
|
||||
InitURL: initURL,
|
||||
Status: status,
|
||||
Scopes: dc.GrantedScopes,
|
||||
ID: dc.ClientID,
|
||||
Name: name,
|
||||
Logo: extractHydraClientLogo(client.Metadata),
|
||||
URL: clientURL,
|
||||
InitURL: initURL,
|
||||
AutoLoginSupported: autoLoginSupported,
|
||||
AutoLoginURL: autoLoginURL,
|
||||
Status: status,
|
||||
Scopes: dc.GrantedScopes,
|
||||
},
|
||||
lastAuth: dc.UpdatedAt,
|
||||
}
|
||||
@@ -5087,11 +5216,9 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
record.URL = clientURL
|
||||
record.InitURL = resolveLinkedRPInitURL(
|
||||
client.ClientID,
|
||||
scopes,
|
||||
client.RedirectURIs,
|
||||
)
|
||||
record.InitURL = resolveLinkedRPInitURL(client.ClientID, client.Metadata)
|
||||
record.AutoLoginSupported = resolveLinkedRPAutoLoginSupported(client.ClientID, client.Metadata)
|
||||
record.AutoLoginURL = resolveLinkedRPAutoLoginURL(client.ClientID, client.Metadata)
|
||||
} else {
|
||||
// Hydra 정보 없음 (삭제됨 등) -> Audit 정보나 ID로 대체
|
||||
if record.Name == "" {
|
||||
@@ -5239,6 +5366,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
|
||||
buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope, tenantID),
|
||||
currentSessionID,
|
||||
)
|
||||
sessionClaims = h.withRPProfileClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject)
|
||||
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), challenge, consentRequest, sessionClaims)
|
||||
if err == nil {
|
||||
return c.JSON(acceptResp)
|
||||
@@ -5268,6 +5396,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
|
||||
buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope, tenantID),
|
||||
currentSessionID,
|
||||
)
|
||||
sessionClaims = h.withRPProfileClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject)
|
||||
|
||||
// [Debug] 실제 생성된 클레임 출력 (요청사항 확인용 - 자동 승인 시)
|
||||
appEnv := strings.ToLower(os.Getenv("APP_ENV"))
|
||||
@@ -5450,6 +5579,7 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
||||
buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope, tenantID),
|
||||
currentSessionID,
|
||||
)
|
||||
sessionClaims = h.withRPProfileClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject)
|
||||
|
||||
// [Debug] 실제 생성된 클레임 출력 (요청사항 확인용)
|
||||
appEnv := strings.ToLower(os.Getenv("APP_ENV"))
|
||||
@@ -7255,6 +7385,10 @@ func resolveLinkedRPURL(clientID string, clientURI string, redirectURIs []string
|
||||
if value := strings.TrimSpace(os.Getenv("DEVFRONT_URL")); value != "" {
|
||||
return value
|
||||
}
|
||||
case "orgfront":
|
||||
if value := strings.TrimSpace(os.Getenv("ORGFRONT_URL")); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
clientURL := strings.TrimSpace(clientURI)
|
||||
@@ -7271,10 +7405,22 @@ func resolveLinkedRPURL(clientID string, clientURI string, redirectURIs []string
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveLinkedRPInitURL(clientID string, scopes []string, redirectURIs []string) string {
|
||||
func resolveLinkedRPAutoLoginSupported(clientID string, metadata map[string]interface{}) bool {
|
||||
if readMetadataBoolValue(metadata, domain.MetadataAutoLoginSupported) {
|
||||
return true
|
||||
}
|
||||
switch strings.TrimSpace(clientID) {
|
||||
case "adminfront", "devfront", "orgfront":
|
||||
return resolveLinkedRPAutoLoginURL(clientID, nil) != ""
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func resolveLinkedRPAutoLoginURL(clientID string, metadata map[string]interface{}) string {
|
||||
clientID = strings.TrimSpace(clientID)
|
||||
if clientID == "" {
|
||||
return ""
|
||||
if metadataURL := readMetadataStringValue(metadata, domain.MetadataAutoLoginURL); metadataURL != "" {
|
||||
return metadataURL
|
||||
}
|
||||
|
||||
switch clientID {
|
||||
@@ -7286,8 +7432,23 @@ func resolveLinkedRPInitURL(clientID string, scopes []string, redirectURIs []str
|
||||
if value := strings.TrimRight(strings.TrimSpace(os.Getenv("DEVFRONT_URL")), "/"); value != "" {
|
||||
return value + "/login?auto=1&returnTo=%2Fclients"
|
||||
}
|
||||
case "orgfront":
|
||||
if value := strings.TrimRight(strings.TrimSpace(os.Getenv("ORGFRONT_URL")), "/"); value != "" {
|
||||
return value + "/login?auto=1"
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveLinkedRPInitURL(clientID string, metadata map[string]interface{}) string {
|
||||
if !resolveLinkedRPAutoLoginSupported(clientID, metadata) {
|
||||
return ""
|
||||
}
|
||||
return resolveLinkedRPAutoLoginURL(clientID, metadata)
|
||||
}
|
||||
|
||||
func buildHydraAuthorizationURL(clientID string, scopes []string, redirectURIs []string) string {
|
||||
hydraPublicURL := strings.TrimRight(os.Getenv("HYDRA_PUBLIC_URL"), "/")
|
||||
if hydraPublicURL == "" {
|
||||
userfrontURL := strings.TrimRight(os.Getenv("USERFRONT_URL"), "/")
|
||||
|
||||
Reference in New Issue
Block a user