1
0
forked from baron/baron-sso

feat(headless-login): add jwks cache visibility and refresh flow

- replace inline headless jwks support with jwksUri-only validation
- add cached jwks refresh worker, manual refresh/revoke endpoints, and parsed key summaries
- expose allowed algorithms and key previews in DevFront with regression coverage
This commit is contained in:
Lectom C Han
2026-04-01 18:33:22 +09:00
parent f51cdba51a
commit 9facd24a00
20 changed files with 2393 additions and 499 deletions

View File

@@ -0,0 +1,29 @@
package domain
import "time"
type HeadlessJWKSParsedKey struct {
Kid string `json:"kid,omitempty"`
Kty string `json:"kty,omitempty"`
Use string `json:"use,omitempty"`
Alg string `json:"alg,omitempty"`
NPreview string `json:"nPreview,omitempty"`
}
// HeadlessJWKSCacheState는 headless login용 JWKS 캐시 상태와 최근 동기화 결과를 나타냅니다.
type HeadlessJWKSCacheState struct {
ClientID string `json:"clientId"`
JWKSURI string `json:"jwksUri"`
CachedAt *time.Time `json:"cachedAt,omitempty"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
LastCheckedAt *time.Time `json:"lastCheckedAt,omitempty"`
LastSuccessfulVerificationAt *time.Time `json:"lastSuccessfulVerificationAt,omitempty"`
LastRefreshStatus string `json:"lastRefreshStatus,omitempty"`
LastError string `json:"lastError,omitempty"`
ConsecutiveFailures int `json:"consecutiveFailures,omitempty"`
CachedKids []string `json:"cachedKids,omitempty"`
ParsedKeys []HeadlessJWKSParsedKey `json:"parsedKeys,omitempty"`
ETag string `json:"etag,omitempty"`
LastModified string `json:"lastModified,omitempty"`
RawJWKS string `json:"-"`
}

View File

@@ -28,9 +28,8 @@ type HydraClient struct {
}
func (c *HydraClient) SupportsHeadlessLogin() bool {
// A headless login client must have a public key registered (URI or Inline)
// and use private_key_jwt for token endpoint authentication.
hasPublicKey := c.HeadlessJWKSURI() != "" || c.HeadlessJWKS() != nil
// Headless login now supports jwksUri only.
hasPublicKey := c.HeadlessJWKSURI() != ""
isPrivateKeyJwt := c.HeadlessTokenEndpointAuthMethod() == "private_key_jwt"
return hasPublicKey && isPrivateKeyJwt
}

View File

@@ -9,11 +9,7 @@ func TestHydraClient_HeadlessLoginFlags(t *testing.T) {
Metadata: map[string]any{
"headless_login_enabled": true,
"headless_token_endpoint_auth_method": "private_key_jwt",
"headless_jwks": map[string]any{
"keys": []map[string]any{{
"kty": "RSA",
}},
},
"headless_jwks_uri": "https://rp.example.com/.well-known/jwks.json",
},
}
@@ -25,7 +21,7 @@ func TestHydraClient_HeadlessLoginFlags(t *testing.T) {
}
})
t.Run("inline jwks with private_key_jwt and headless enabled", func(t *testing.T) {
t.Run("inline jwks without jwks uri does not support headless login", func(t *testing.T) {
client := HydraClient{
TokenEndpointAuthMethod: "private_key_jwt",
JWKS: map[string]any{
@@ -38,11 +34,11 @@ func TestHydraClient_HeadlessLoginFlags(t *testing.T) {
},
}
if !client.SupportsHeadlessLogin() {
t.Fatalf("expected headless login client")
if client.SupportsHeadlessLogin() {
t.Fatalf("expected headless login prerequisites to be missing")
}
if !client.IsHeadlessLoginEnabled() {
t.Fatalf("expected headless login enabled")
if client.IsHeadlessLoginEnabled() {
t.Fatalf("expected headless login disabled without jwks uri")
}
})