Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions authcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func (f FederationLeaf) GetAuthorizationURL(
}
q := url.Values{}
q.Set("request", string(requestObject))
q.Set("client_id", f.EntityID)
q.Set("client_id", f.EntityID())
q.Set("response_type", "code")
q.Set("redirect_uri", redirectURI)
q.Set("scope", scope)
Expand All @@ -193,7 +193,7 @@ func (f FederationLeaf) CodeExchange(
params.Set("grant_type", "authorization_code")
params.Set("code", code)
params.Set("redirect_uri", redirectURI)
params.Set("client_id", f.EntityID)
params.Set("client_id", f.EntityID())

clientAssertion, err := f.oidcROProducer.ClientAssertion(
opMetadata.TokenEndpoint,
Expand Down
3 changes: 1 addition & 2 deletions cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cache

import (
"encoding/base64"
"log"
"strings"
"time"

Expand Down Expand Up @@ -31,7 +30,7 @@ type cacheWrapper struct {
func newCacheWrapper(defaultExpiration time.Duration) cacheWrapper {
c := gocache.NewCache().WithDefaultTTL(defaultExpiration)
if err := c.StartJanitor(); err != nil {
log.Fatal(err) // skipcq: RVV-A0003
internal.WithError(err).Error("Cache: failed to start janitor; proceeding without background cleanup")
}
return cacheWrapper{
c,
Expand Down
9 changes: 6 additions & 3 deletions explicit.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,13 @@ func (f FederationLeaf) DoExplicitClientRegistration(op string) (
if opMetadata == nil || opMetadata.FederationRegistrationEndpoint == "" {
return nil, nil, errors.New("op does not have a federation registration endpoint")
}
entityConfigurationData := f.EntityConfigurationPayload()
entityConfigurationData, err := f.EntityConfigurationPayload()
if err != nil {
return nil, nil, errors.Wrap(err, "could not get entity configuration payload")
}
AdjustRPMetadataToOP(entityConfigurationData.Metadata.RelyingParty, opMetadata)
entityConfigurationData.Audience = op
entityConfiguration, err := f.EntityStatementSigner.JWT(entityConfigurationData)
entityConfiguration, err := f.SignEntityStatement(*entityConfigurationData)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -147,7 +150,7 @@ func (f FederationLeaf) DoExplicitClientRegistration(op string) (
if err != nil {
return nil, nil, errors.Wrap(err, "could not parse explicit registration response")
}
if res.Audience != f.EntityID {
if res.Audience != f.EntityID() {
return nil, nil,
errors.New("explicit client registration: OP returned unexpected audience")
}
Expand Down
281 changes: 226 additions & 55 deletions federation.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,16 @@ import (
"github.com/go-oidfed/lib/unixtime"
)

// FederationEntity is a type for an entity participating in federations.
// It holds all relevant information about the federation entity and can be used to create
// an EntityConfiguration about it
type FederationEntity struct {
EntityID string
Metadata *Metadata
MetadataUpdater func(*Metadata)
AuthorityHints []string
ConfigurationLifetime time.Duration
*jwx.EntityStatementSigner
TrustMarks []*EntityConfigurationTrustMarkConfig
TrustMarkIssuers AllowedTrustMarkIssuers
TrustMarkOwners TrustMarkOwners
Extra map[string]any
// FederationEntity defines the common behavior for federation entities,
// implemented by both StaticFederationEntity and DynamicFederationEntity.
type FederationEntity interface {
EntityID() string
// EntityConfigurationPayload returns the payload for the entity configuration
EntityConfigurationPayload() (*EntityStatementPayload, error)
// EntityConfigurationJWT returns the signed entity configuration as a JWT
EntityConfigurationJWT() ([]byte, error)
// SignEntityStatement signs the provided entity configuration payload
SignEntityStatement(payload EntityStatementPayload) ([]byte, error)
}

// FederationLeaf is a type for a leaf entity and holds all relevant information about it; it can also be used to
Expand All @@ -36,16 +32,176 @@ type FederationLeaf struct {
oidcROProducer *RequestObjectProducer
}

// NewFederationEntity creates a new FederationEntity with the passed properties
// DynamicFederationEntity mirrors FederationEntity but exposes all properties
// (except EntityID) as functions of time, enabling time-dependent values.
type DynamicFederationEntity struct {
ID string
Metadata func() (*Metadata, error)
AuthorityHints func() ([]string, error)
ConfigurationLifetime func() (time.Duration, error)
EntityStatementSigner func() (*jwx.EntityStatementSigner, error)
TrustMarks func() ([]*EntityConfigurationTrustMarkConfig, error)
TrustMarkIssuers func() (AllowedTrustMarkIssuers, error)
TrustMarkOwners func() (TrustMarkOwners, error)
Extra func() (map[string]any, []string, error)
}

// EntityID returns the entity ID of the DynamicFederationEntity
func (f DynamicFederationEntity) EntityID() string {
return f.ID
}

// EntityConfigurationPayload returns an EntityStatementPayload for this DynamicFederationEntity
// resolving all dynamic properties at time.Now().
func (f DynamicFederationEntity) EntityConfigurationPayload() (*EntityStatementPayload, error) {
now := time.Now()

var err error
// Resolve dynamic fields
metadata := (*Metadata)(nil)
if f.Metadata != nil {
metadata, err = f.Metadata()
if err != nil {
return nil, err
}
}

var authorityHints []string
if f.AuthorityHints != nil {
authorityHints, err = f.AuthorityHints()
if err != nil {
return nil, err
}
}

lifetime := time.Duration(0)
if f.ConfigurationLifetime != nil {
lifetime, err = f.ConfigurationLifetime()
if err != nil {
return nil, err
}
}
if lifetime <= 0 {
lifetime = defaultEntityConfigurationLifetime
}

signer := (*jwx.EntityStatementSigner)(nil)
if f.EntityStatementSigner != nil {
signer, err = f.EntityStatementSigner()
if err != nil {
return nil, err
}
}

var tms []TrustMarkInfo
if f.TrustMarks != nil {
trustMarkConfigs, err := f.TrustMarks()
if err != nil {
return nil, err
}
tms = make([]TrustMarkInfo, 0, len(trustMarkConfigs))
for _, tmc := range trustMarkConfigs {
tm, err := tmc.TrustMarkJWT()
if err != nil {
internal.Log(err.Error())
continue
}
tms = append(
tms, TrustMarkInfo{
TrustMarkType: tmc.TrustMarkType,
TrustMarkJWT: tm,
},
)
}
}

var trustMarkIssuers AllowedTrustMarkIssuers
if f.TrustMarkIssuers != nil {
trustMarkIssuers, err = f.TrustMarkIssuers()
if err != nil {
return nil, err
}
}

var trustMarkOwners TrustMarkOwners
if f.TrustMarkOwners != nil {
trustMarkOwners, err = f.TrustMarkOwners()
if err != nil {
return nil, err
}
}

var extra map[string]any
var crits []string
if f.Extra != nil {
extra, crits, err = f.Extra()
if err != nil {
return nil, err
}
}

if metadata != nil {
metadata.ApplyInformationalClaimsToFederationEntity()
}

var jwks jwx.JWKS
if signer != nil {
jwks, err = signer.JWKS()
if err != nil {
return nil, err
}
}

return &EntityStatementPayload{
Issuer: f.ID,
Subject: f.ID,
IssuedAt: unixtime.Unixtime{Time: now},
ExpiresAt: unixtime.Unixtime{Time: now.Add(lifetime)},
JWKS: jwks,
AuthorityHints: authorityHints,
Metadata: metadata,
TrustMarks: tms,
TrustMarkIssuers: trustMarkIssuers,
TrustMarkOwners: trustMarkOwners,
CriticalExtensions: crits,
Extra: extra,
}, nil
}

// EntityConfigurationJWT creates and returns the signed jwt for the dynamic entity configuration
func (f DynamicFederationEntity) EntityConfigurationJWT() ([]byte, error) {
payload, err := f.EntityConfigurationPayload()
if err != nil {
return nil, err
}
return f.SignEntityStatement(*payload)
}

// SignEntityStatement creates a signed JWT for the given EntityStatementPayload
func (f DynamicFederationEntity) SignEntityStatement(payload EntityStatementPayload) ([]byte, error) {
if f.EntityStatementSigner == nil {
return nil, errors.New("no signer function configured")
}
signer, err := f.EntityStatementSigner()
if signer == nil {
return nil, errors.New("no signer available at current time")
}
if err != nil {
return nil, err
}
return signer.JWT(payload)
}

// NewFederationEntity creates a new StaticFederationEntity with the passed properties
func NewFederationEntity(
entityID string, authorityHints []string, metadata *Metadata,
signer *jwx.EntityStatementSigner, configurationLifetime time.Duration, extra map[string]any,
) (*FederationEntity, error) {
) (*StaticFederationEntity, error) {
if configurationLifetime <= 0 {
configurationLifetime = defaultEntityConfigurationLifetime
}
return &FederationEntity{
EntityID: entityID,
return &StaticFederationEntity{
ID: entityID,
Metadata: metadata,
AuthorityHints: authorityHints,
EntityStatementSigner: signer,
Expand Down Expand Up @@ -73,51 +229,66 @@ func NewFederationLeaf(
}, nil
}

// EntityConfigurationPayload returns an EntityStatementPayload for this FederationEntity
func (f FederationEntity) EntityConfigurationPayload() *EntityStatementPayload {
now := time.Now()
var tms []TrustMarkInfo
for _, tmc := range f.TrustMarks {
tm, err := tmc.TrustMarkJWT()
if err != nil {
internal.Log(err.Error())
continue
}
tms = append(
tms, TrustMarkInfo{
TrustMarkType: tmc.TrustMarkType,
TrustMarkJWT: tm,
},
)
}
if f.MetadataUpdater != nil {
f.MetadataUpdater(f.Metadata)
}
f.Metadata.ApplyInformationalClaimsToFederationEntity()
return &EntityStatementPayload{
Issuer: f.EntityID,
Subject: f.EntityID,
IssuedAt: unixtime.Unixtime{Time: now},
ExpiresAt: unixtime.Unixtime{Time: now.Add(f.ConfigurationLifetime)},
JWKS: f.EntityStatementSigner.JWKS(),
AuthorityHints: f.AuthorityHints,
Metadata: f.Metadata,
TrustMarks: tms,
TrustMarkIssuers: f.TrustMarkIssuers,
TrustMarkOwners: f.TrustMarkOwners,
Extra: f.Extra,
}
// StaticFederationEntity is a type for an entity participating in federations.
// It holds all relevant information about the federation entity and can be used to create
// an EntityConfiguration about it
type StaticFederationEntity struct {
ID string
Metadata *Metadata
AuthorityHints []string
ConfigurationLifetime time.Duration
*jwx.EntityStatementSigner
TrustMarks []*EntityConfigurationTrustMarkConfig
TrustMarkIssuers AllowedTrustMarkIssuers
TrustMarkOwners TrustMarkOwners
Extra map[string]any
CriticalClaims []string
}

// EntityID returns the entity ID of the StaticFederationEntity
func (f StaticFederationEntity) EntityID() string {
return f.ID
}

// EntityConfigurationPayload returns an EntityStatementPayload for this
// StaticFederationEntity
func (f StaticFederationEntity) EntityConfigurationPayload() (*EntityStatementPayload, error) {
return DynamicFederationEntity{
ID: f.ID,
Metadata: func() (*Metadata, error) {
return f.Metadata, nil
},
AuthorityHints: func() ([]string, error) {
return f.AuthorityHints, nil
},
ConfigurationLifetime: func() (time.Duration, error) { return f.ConfigurationLifetime, nil },
EntityStatementSigner: func() (*jwx.EntityStatementSigner, error) {
return f.EntityStatementSigner, nil
},
TrustMarks: func() ([]*EntityConfigurationTrustMarkConfig, error) {
return f.TrustMarks, nil
},
TrustMarkIssuers: func() (AllowedTrustMarkIssuers, error) { return f.TrustMarkIssuers, nil },
TrustMarkOwners: func() (TrustMarkOwners, error) {
return f.TrustMarkOwners, nil
},
Extra: func() (map[string]any, []string, error) { return f.Extra, f.CriticalClaims, nil },
}.EntityConfigurationPayload()
}

// EntityConfigurationJWT creates and returns the signed jwt as a []byte for
// the entity's entity configuration
func (f FederationEntity) EntityConfigurationJWT() ([]byte, error) {
return f.EntityStatementSigner.JWT(f.EntityConfigurationPayload())
func (f StaticFederationEntity) EntityConfigurationJWT() ([]byte, error) {
payload, err := f.EntityConfigurationPayload()
if err != nil {
return nil, err
}
return f.SignEntityStatement(*payload)
}

// SignEntityStatement creates a signed JWT for the given EntityStatementPayload; this function is intended to be
// used on TA/IA
func (f FederationEntity) SignEntityStatement(payload EntityStatementPayload) ([]byte, error) {
func (f StaticFederationEntity) SignEntityStatement(payload EntityStatementPayload) ([]byte, error) {
return f.EntityStatementSigner.JWT(payload)
}

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/go-oidfed/lib
go 1.25.1

require (
github.com/ThalesGroup/crypto11 v1.2.6
github.com/TwiN/gocache/v2 v2.4.0
github.com/adam-hanna/arrayOperations v1.0.1
github.com/coreos/go-oidc/v3 v3.16.0
Expand Down Expand Up @@ -49,10 +50,12 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/thales-e-security/pool v0.0.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
Expand Down
Loading