1
0
forked from baron/baron-sso

devfront ID Token Claims 백엔드 반영

This commit is contained in:
2026-04-29 13:45:23 +09:00
parent e484d8c100
commit 0844befb35
5 changed files with 492 additions and 8 deletions

View File

@@ -194,6 +194,13 @@ type clientUpsertRequest struct {
Metadata *map[string]interface{} `json:"metadata"`
}
type normalizedIDTokenClaim struct {
Namespace string `json:"namespace"`
Key string `json:"key"`
Value string `json:"value"`
ValueType string `json:"valueType"`
}
var protectedSystemClientIDs = map[string]struct{}{
"oathkeeper-introspect": {},
}
@@ -1656,7 +1663,7 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
metadata, err = normalizeClientAutoLoginMetadata(metadata)
metadata, err = normalizeIDTokenClaimsMetadata(metadata)
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
@@ -1852,7 +1859,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
metadata, err = normalizeClientAutoLoginMetadata(metadata)
metadata, err = normalizeIDTokenClaimsMetadata(metadata)
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
@@ -2752,6 +2759,174 @@ func validateHeadlessClientInput(clientType string, jwksURI string, jwks interfa
return nil
}
func normalizeIDTokenClaimsMetadata(metadata map[string]interface{}) (map[string]interface{}, error) {
if metadata == nil {
return nil, nil
}
rawClaims, exists := metadata[domain.MetadataIDTokenClaims]
if !exists || rawClaims == nil {
return metadata, nil
}
normalized, err := normalizeIDTokenClaims(rawClaims)
if err != nil {
return nil, err
}
metadata[domain.MetadataIDTokenClaims] = normalized
return metadata, nil
}
func normalizeIDTokenClaims(rawClaims interface{}) ([]normalizedIDTokenClaim, error) {
rawList, ok := rawClaims.([]interface{})
if !ok {
if typedList, ok := rawClaims.([]map[string]interface{}); ok {
rawList = make([]interface{}, 0, len(typedList))
for _, item := range typedList {
rawList = append(rawList, item)
}
} else if typedList, ok := rawClaims.([]map[string]any); ok {
rawList = make([]interface{}, 0, len(typedList))
for _, item := range typedList {
rawList = append(rawList, item)
}
} else {
return nil, errors.New("metadata.id_token_claims must be an array")
}
}
normalized := make([]normalizedIDTokenClaim, 0, len(rawList))
seen := make(map[string]struct{}, len(rawList))
for _, item := range rawList {
record, ok := item.(map[string]interface{})
if !ok {
if typedRecord, ok := item.(map[string]any); ok {
record = make(map[string]interface{}, len(typedRecord))
for key, value := range typedRecord {
record[key] = value
}
} else {
return nil, errors.New("metadata.id_token_claims items must be objects")
}
}
namespace := strings.TrimSpace(readInterfaceString(record["namespace"], "top_level"))
if namespace == "" {
namespace = "top_level"
}
if namespace != "top_level" && namespace != "rp_claims" {
return nil, fmt.Errorf("metadata.id_token_claims namespace must be top_level or rp_claims: %s", namespace)
}
key := strings.TrimSpace(readInterfaceString(record["key"], ""))
if key == "" {
return nil, errors.New("metadata.id_token_claims key is required")
}
if namespace == "top_level" && key == "rp_claims" {
return nil, errors.New("metadata.id_token_claims top-level key rp_claims is reserved")
}
valueType := strings.TrimSpace(readInterfaceString(record["valueType"], "text"))
if valueType == "" {
valueType = "text"
}
switch valueType {
case "text", "number", "boolean", "array", "object":
default:
return nil, fmt.Errorf("metadata.id_token_claims valueType is invalid: %s", valueType)
}
value := strings.TrimSpace(readInterfaceString(record["value"], ""))
if _, err := parseConfiguredClaimValue(value, valueType); err != nil {
return nil, fmt.Errorf("metadata.id_token_claims %s.%s is invalid: %w", namespace, key, err)
}
signature := namespace + ":" + key
if _, exists := seen[signature]; exists {
return nil, fmt.Errorf("metadata.id_token_claims contains duplicate key: %s.%s", namespace, key)
}
seen[signature] = struct{}{}
normalized = append(normalized, normalizedIDTokenClaim{
Namespace: namespace,
Key: key,
Value: value,
ValueType: valueType,
})
}
return normalized, nil
}
func readInterfaceString(value interface{}, fallback string) string {
if value == nil {
return fallback
}
if text, ok := value.(string); ok {
return text
}
return fallback
}
func parseConfiguredClaimValue(rawValue string, valueType string) (any, error) {
trimmed := strings.TrimSpace(rawValue)
switch valueType {
case "text":
return trimmed, nil
case "number":
if trimmed == "" {
return nil, errors.New("number value is required")
}
parsed, err := strconv.ParseFloat(trimmed, 64)
if err != nil {
return nil, errors.New("number value must be a finite number")
}
return parsed, nil
case "boolean":
switch strings.ToLower(trimmed) {
case "true", "1", "yes", "on":
return true, nil
case "false", "0", "no", "off":
return false, nil
default:
return nil, errors.New("boolean value must be true/false")
}
case "array":
if trimmed == "" {
return []string{}, nil
}
if strings.HasPrefix(trimmed, "[") {
var parsed []any
if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil {
return nil, errors.New("array value must be valid JSON array")
}
return parsed, nil
}
parts := strings.Split(trimmed, ",")
values := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
values = append(values, part)
}
}
return values, nil
case "object":
if trimmed == "" {
return map[string]any{}, nil
}
var parsed map[string]any
if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil {
return nil, errors.New("object value must be valid JSON object")
}
return parsed, nil
default:
return nil, fmt.Errorf("unsupported claim value type: %s", valueType)
}
}
func requestIncludesInlineHeadlessJWKS(req clientUpsertRequest) bool {
if req.Jwks != nil {
return true