forked from baron/baron-sso
devfront ID Token Claims 백엔드 반영
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user