1
0
forked from baron/baron-sso

OIDC back-channel logout 백엔드 전송 기능 추가

This commit is contained in:
2026-05-04 11:03:27 +09:00
parent a72df2e839
commit f9f0ed0f14
5 changed files with 539 additions and 109 deletions

View File

@@ -94,19 +94,21 @@ type devStatsResponse struct {
}
type clientSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
RedirectURIs []string `json:"redirectUris"`
Scopes []string `json:"scopes"`
ClientSecret string `json:"clientSecret,omitempty"`
TokenEndpointAuthMethod string `json:"tokenEndpointAuthMethod,omitempty"`
SkipConsent bool `json:"skipConsent"`
JwksUri string `json:"jwksUri,omitempty"`
Jwks interface{} `json:"jwks,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
RedirectURIs []string `json:"redirectUris"`
Scopes []string `json:"scopes"`
ClientSecret string `json:"clientSecret,omitempty"`
TokenEndpointAuthMethod string `json:"tokenEndpointAuthMethod,omitempty"`
SkipConsent bool `json:"skipConsent"`
JwksUri string `json:"jwksUri,omitempty"`
Jwks interface{} `json:"jwks,omitempty"`
BackchannelLogoutURI string `json:"backchannelLogoutUri,omitempty"`
BackchannelLogoutSessionRequired bool `json:"backchannelLogoutSessionRequired"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type clientListResponse struct {
@@ -179,19 +181,21 @@ type consentListResponse struct {
}
type clientUpsertRequest struct {
ID *string `json:"id"`
Name *string `json:"name"`
Type *string `json:"type"`
Status *string `json:"status"`
RedirectURIs *[]string `json:"redirectUris"`
Scopes *[]string `json:"scopes"`
GrantTypes *[]string `json:"grantTypes"`
ResponseTypes *[]string `json:"responseTypes"`
TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"`
SkipConsent *bool `json:"skipConsent"`
JwksUri *string `json:"jwksUri"`
Jwks interface{} `json:"jwks"`
Metadata *map[string]interface{} `json:"metadata"`
ID *string `json:"id"`
Name *string `json:"name"`
Type *string `json:"type"`
Status *string `json:"status"`
RedirectURIs *[]string `json:"redirectUris"`
Scopes *[]string `json:"scopes"`
GrantTypes *[]string `json:"grantTypes"`
ResponseTypes *[]string `json:"responseTypes"`
TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"`
SkipConsent *bool `json:"skipConsent"`
JwksUri *string `json:"jwksUri"`
Jwks interface{} `json:"jwks"`
BackchannelLogoutURI *string `json:"backchannelLogoutUri"`
BackchannelLogoutSessionRequired *bool `json:"backchannelLogoutSessionRequired"`
Metadata *map[string]interface{} `json:"metadata"`
}
type normalizedIDTokenClaim struct {
@@ -1679,9 +1683,15 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
if tenantID != "" {
metadata["tenant_id"] = tenantID
}
var err error
metadata["status"] = status
metadata["created_at"] = time.Now().Format(time.RFC3339)
var err error
backchannelLogoutURI := strings.TrimSpace(valueOr(req.BackchannelLogoutURI, ""))
backchannelLogoutSessionRequired := valueOrBool(req.BackchannelLogoutSessionRequired, false)
metadata, err = normalizeBackchannelLogoutMetadata(metadata, backchannelLogoutURI, backchannelLogoutSessionRequired)
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
metadata, err = normalizeClientTenantAccessMetadata(metadata)
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
@@ -1711,17 +1721,19 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
)
clientReq := domain.HydraClient{
ClientID: clientID,
ClientName: name,
RedirectURIs: redirectURIs,
GrantTypes: grantTypes,
ResponseTypes: responseTypes,
Scope: strings.Join(scopes, " "),
TokenEndpointAuthMethod: tokenAuthMethod,
SkipConsent: boolPtr(valueOrBool(req.SkipConsent, true)),
JWKSUri: jwksURI,
JWKS: jwks,
Metadata: metadata,
ClientID: clientID,
ClientName: name,
RedirectURIs: redirectURIs,
GrantTypes: grantTypes,
ResponseTypes: responseTypes,
Scope: strings.Join(scopes, " "),
TokenEndpointAuthMethod: tokenAuthMethod,
SkipConsent: boolPtr(valueOrBool(req.SkipConsent, true)),
JWKSUri: jwksURI,
JWKS: jwks,
BackChannelLogoutURI: backchannelLogoutURI,
BackChannelLogoutSessionRequired: boolPtr(backchannelLogoutSessionRequired),
Metadata: metadata,
}
h.setAuditDetailsExtra(c, map[string]any{
@@ -1866,6 +1878,16 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
}
metadata["status"] = status
}
resolvedBackchannelLogoutURI := valueOr(req.BackchannelLogoutURI, current.BackchannelLogoutURI())
resolvedBackchannelLogoutSessionRequired := valueOrBool(req.BackchannelLogoutSessionRequired, current.BackchannelLogoutSessionRequiredValue())
metadata, err = normalizeBackchannelLogoutMetadata(
metadata,
resolvedBackchannelLogoutURI,
resolvedBackchannelLogoutSessionRequired,
)
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
metadata, err = normalizeClientTenantAccessMetadata(metadata)
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, err.Error())
@@ -1901,17 +1923,19 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
resolvedSkipConsent := valueOrBool(req.SkipConsent, valueOrBool(current.SkipConsent, true))
updated := domain.HydraClient{
ClientID: current.ClientID,
ClientName: valueOr(req.Name, current.ClientName),
RedirectURIs: derefSlice(req.RedirectURIs, current.RedirectURIs),
GrantTypes: derefSlice(req.GrantTypes, current.GrantTypes),
ResponseTypes: derefSlice(req.ResponseTypes, current.ResponseTypes),
Scope: buildScope(valueOrSlice(req.Scopes, strings.Fields(current.Scope))),
TokenEndpointAuthMethod: resolvedTokenAuthMethod,
SkipConsent: boolPtr(resolvedSkipConsent),
JWKSUri: resolvedJWKSURI,
JWKS: resolvedJWKS,
Metadata: metadata,
ClientID: current.ClientID,
ClientName: valueOr(req.Name, current.ClientName),
RedirectURIs: derefSlice(req.RedirectURIs, current.RedirectURIs),
GrantTypes: derefSlice(req.GrantTypes, current.GrantTypes),
ResponseTypes: derefSlice(req.ResponseTypes, current.ResponseTypes),
Scope: buildScope(valueOrSlice(req.Scopes, strings.Fields(current.Scope))),
TokenEndpointAuthMethod: resolvedTokenAuthMethod,
SkipConsent: boolPtr(resolvedSkipConsent),
JWKSUri: resolvedJWKSURI,
JWKS: resolvedJWKS,
BackChannelLogoutURI: strings.TrimSpace(resolvedBackchannelLogoutURI),
BackChannelLogoutSessionRequired: boolPtr(resolvedBackchannelLogoutSessionRequired),
Metadata: metadata,
}
if err := validateReservedSystemClientName(updated.ClientID, updated.ClientName); err != nil {
return errorJSON(c, fiber.StatusForbidden, err.Error())
@@ -2651,19 +2675,21 @@ func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
}
return clientSummary{
ID: client.ClientID,
Name: name,
Type: clientType,
Status: status,
CreatedAt: createdAt,
RedirectURIs: client.RedirectURIs,
Scopes: scopes,
ClientSecret: clientSecret,
TokenEndpointAuthMethod: client.TokenEndpointAuthMethod,
SkipConsent: valueOrBool(client.SkipConsent, true),
JwksUri: client.JWKSUri,
Jwks: client.JWKS,
Metadata: client.Metadata,
ID: client.ClientID,
Name: name,
Type: clientType,
Status: status,
CreatedAt: createdAt,
RedirectURIs: client.RedirectURIs,
Scopes: scopes,
ClientSecret: clientSecret,
TokenEndpointAuthMethod: client.TokenEndpointAuthMethod,
SkipConsent: valueOrBool(client.SkipConsent, true),
JwksUri: client.JWKSUri,
Jwks: client.JWKS,
BackchannelLogoutURI: client.BackchannelLogoutURI(),
BackchannelLogoutSessionRequired: client.BackchannelLogoutSessionRequiredValue(),
Metadata: client.Metadata,
}
}
@@ -2683,6 +2709,58 @@ func readMetadataBoolValue(metadata map[string]interface{}, key string) bool {
return value
}
func normalizeBackchannelLogoutMetadata(metadata map[string]interface{}, logoutURI string, sessionRequired bool) (map[string]interface{}, error) {
if metadata == nil {
metadata = map[string]interface{}{}
}
trimmedURI := strings.TrimSpace(logoutURI)
if err := validateBackchannelLogoutURI(trimmedURI); err != nil {
return nil, err
}
if trimmedURI == "" {
delete(metadata, domain.MetadataBackChannelLogoutURI)
delete(metadata, domain.MetadataBackChannelLogoutSessionRequired)
return metadata, nil
}
metadata[domain.MetadataBackChannelLogoutURI] = trimmedURI
metadata[domain.MetadataBackChannelLogoutSessionRequired] = sessionRequired
return metadata, nil
}
func validateBackchannelLogoutURI(raw string) error {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return nil
}
parsed, err := url.Parse(trimmed)
if err != nil || parsed == nil {
return fmt.Errorf("backchannelLogoutUri must be a valid absolute URL")
}
if parsed.Scheme == "" || parsed.Host == "" {
return fmt.Errorf("backchannelLogoutUri must be a valid absolute URL")
}
if parsed.Fragment != "" {
return fmt.Errorf("backchannelLogoutUri must not include a fragment")
}
switch strings.ToLower(parsed.Scheme) {
case "https":
return nil
case "http":
host := strings.ToLower(parsed.Hostname())
if host == "localhost" || host == "127.0.0.1" {
return nil
}
return fmt.Errorf("backchannelLogoutUri must use https outside localhost development")
default:
return fmt.Errorf("backchannelLogoutUri must use http or https")
}
}
func normalizeClientAutoLoginMetadata(metadata map[string]interface{}) (map[string]interface{}, error) {
if metadata == nil {
return metadata, nil