oidc: Remove "aud" claim validation of logout tokens (#6156)
The "aud" claim of the logout token is supposed to contain the client-id of the client for which the token was issued. Our current implementation of validating that claim is somewhat broken. We only allow to configure a single value for the allowed client id. But we have different client-ids accessing oCIS. This completely removes the current validation of the `aud` claim until we come up with a working solution. As we currently require a session id to be present in the logout token the risk not validating the `aud` claim is pretty low. Related: #6149
This commit is contained in:
@@ -55,8 +55,6 @@ type oidcClient struct {
|
|||||||
// Logger to use for logging, must be set
|
// Logger to use for logging, must be set
|
||||||
Logger log.Logger
|
Logger log.Logger
|
||||||
|
|
||||||
clientID string
|
|
||||||
skipClientIDCheck bool
|
|
||||||
issuer string
|
issuer string
|
||||||
provider *ProviderMetadata
|
provider *ProviderMetadata
|
||||||
providerLock *sync.Mutex
|
providerLock *sync.Mutex
|
||||||
@@ -99,8 +97,6 @@ func NewOIDCClient(opts ...Option) OIDCClient {
|
|||||||
JWKSOptions: options.JWKSOptions, // TODO I don't like that we pass down config options ...
|
JWKSOptions: options.JWKSOptions, // TODO I don't like that we pass down config options ...
|
||||||
providerLock: &sync.Mutex{},
|
providerLock: &sync.Mutex{},
|
||||||
jwksLock: &sync.Mutex{},
|
jwksLock: &sync.Mutex{},
|
||||||
clientID: options.ClientID,
|
|
||||||
skipClientIDCheck: options.SkipClientIDCheck,
|
|
||||||
remoteKeySet: options.KeySet,
|
remoteKeySet: options.KeySet,
|
||||||
provider: options.ProviderMetadata,
|
provider: options.ProviderMetadata,
|
||||||
}
|
}
|
||||||
@@ -363,19 +359,6 @@ func (c *oidcClient) VerifyLogoutToken(ctx context.Context, rawToken string) (*L
|
|||||||
return nil, fmt.Errorf("oidc: logout token issued by a different provider, expected %q got %q", c.issuer, token.Issuer)
|
return nil, fmt.Errorf("oidc: logout token issued by a different provider, expected %q got %q", c.issuer, token.Issuer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty.
|
|
||||||
//
|
|
||||||
// This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party).
|
|
||||||
if !c.skipClientIDCheck {
|
|
||||||
if c.clientID != "" {
|
|
||||||
if !contains(token.Audience, c.clientID) {
|
|
||||||
return nil, fmt.Errorf("oidc: expected audience %q got %q", c.clientID, token.Audience)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(jws.Signatures) {
|
switch len(jws.Signatures) {
|
||||||
case 0:
|
case 0:
|
||||||
return nil, fmt.Errorf("oidc: logout token not signed")
|
return nil, fmt.Errorf("oidc: logout token not signed")
|
||||||
|
|||||||
@@ -60,9 +60,6 @@ func TestLogoutVerify(t *testing.T) {
|
|||||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
"http://schemas.openid.net/event/backchannel-logout": {}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
config: goidc.Config{
|
|
||||||
SkipClientIDCheck: true,
|
|
||||||
},
|
|
||||||
signKey: newRSAKey(t),
|
signKey: newRSAKey(t),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,8 +67,7 @@ func TestLogoutVerify(t *testing.T) {
|
|||||||
issuer: "https://bar",
|
issuer: "https://bar",
|
||||||
logoutToken: `{"iss":"https://foo"}`,
|
logoutToken: `{"iss":"https://foo"}`,
|
||||||
config: goidc.Config{
|
config: goidc.Config{
|
||||||
SkipClientIDCheck: true,
|
SkipExpiryCheck: true,
|
||||||
SkipExpiryCheck: true,
|
|
||||||
},
|
},
|
||||||
signKey: newRSAKey(t),
|
signKey: newRSAKey(t),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
@@ -90,8 +86,7 @@ func TestLogoutVerify(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
config: goidc.Config{
|
config: goidc.Config{
|
||||||
SkipClientIDCheck: true,
|
SkipExpiryCheck: true,
|
||||||
SkipExpiryCheck: true,
|
|
||||||
},
|
},
|
||||||
signKey: newRSAKey(t),
|
signKey: newRSAKey(t),
|
||||||
verificationKey: newRSAKey(t),
|
verificationKey: newRSAKey(t),
|
||||||
@@ -108,9 +103,6 @@ func TestLogoutVerify(t *testing.T) {
|
|||||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
"http://schemas.openid.net/event/backchannel-logout": {}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
config: goidc.Config{
|
|
||||||
SkipClientIDCheck: true,
|
|
||||||
},
|
|
||||||
signKey: newRSAKey(t),
|
signKey: newRSAKey(t),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
@@ -127,9 +119,6 @@ func TestLogoutVerify(t *testing.T) {
|
|||||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
"http://schemas.openid.net/event/backchannel-logout": {}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
config: goidc.Config{
|
|
||||||
SkipClientIDCheck: true,
|
|
||||||
},
|
|
||||||
signKey: newRSAKey(t),
|
signKey: newRSAKey(t),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
@@ -146,9 +135,6 @@ func TestLogoutVerify(t *testing.T) {
|
|||||||
"not a logout event": {}
|
"not a logout event": {}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
config: goidc.Config{
|
|
||||||
SkipClientIDCheck: true,
|
|
||||||
},
|
|
||||||
signKey: newRSAKey(t),
|
signKey: newRSAKey(t),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
@@ -162,9 +148,6 @@ func TestLogoutVerify(t *testing.T) {
|
|||||||
"jti": "bWJq",
|
"jti": "bWJq",
|
||||||
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
|
||||||
}`,
|
}`,
|
||||||
config: goidc.Config{
|
|
||||||
SkipClientIDCheck: true,
|
|
||||||
},
|
|
||||||
signKey: newRSAKey(t),
|
signKey: newRSAKey(t),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
@@ -174,49 +157,6 @@ func TestLogoutVerify(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVerifyAudienceLogout(t *testing.T) {
|
|
||||||
tests := []logoutVerificationTest{
|
|
||||||
{
|
|
||||||
name: "good audience",
|
|
||||||
logoutToken: `{"iss":"https://foo","aud":"client1","sub":"subject","events": {
|
|
||||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
config: goidc.Config{
|
|
||||||
ClientID: "client1",
|
|
||||||
SkipExpiryCheck: true,
|
|
||||||
},
|
|
||||||
signKey: newRSAKey(t),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mismatched audience",
|
|
||||||
logoutToken: `{"iss":"https://foo","aud":"client2","sub":"subject","events": {
|
|
||||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
|
||||||
}}`,
|
|
||||||
config: goidc.Config{
|
|
||||||
ClientID: "client1",
|
|
||||||
SkipExpiryCheck: true,
|
|
||||||
},
|
|
||||||
signKey: newRSAKey(t),
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple audiences, one matches",
|
|
||||||
logoutToken: `{"iss":"https://foo","aud":["client1","client2"],"sub":"subject","events": {
|
|
||||||
"http://schemas.openid.net/event/backchannel-logout": {}
|
|
||||||
}}`,
|
|
||||||
config: goidc.Config{
|
|
||||||
ClientID: "client2",
|
|
||||||
SkipExpiryCheck: true,
|
|
||||||
},
|
|
||||||
signKey: newRSAKey(t),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, test.run)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type logoutVerificationTest struct {
|
type logoutVerificationTest struct {
|
||||||
// Name of the subtest.
|
// Name of the subtest.
|
||||||
name string
|
name string
|
||||||
@@ -263,19 +203,11 @@ func (v logoutVerificationTest) runGetToken(t *testing.T) (*oidc.LogoutToken, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
pm := oidc.ProviderMetadata{}
|
pm := oidc.ProviderMetadata{}
|
||||||
var clientID string
|
|
||||||
switch t.Name() {
|
|
||||||
case "TestLogoutVerify/good_token":
|
|
||||||
clientID = "s6BhdRkqt3"
|
|
||||||
default:
|
|
||||||
clientID = "client1"
|
|
||||||
}
|
|
||||||
verifier := oidc.NewOIDCClient(
|
verifier := oidc.NewOIDCClient(
|
||||||
oidc.WithOidcIssuer(issuer),
|
oidc.WithOidcIssuer(issuer),
|
||||||
oidc.WithKeySet(ks),
|
oidc.WithKeySet(ks),
|
||||||
oidc.WithConfig(&v.config),
|
oidc.WithConfig(&v.config),
|
||||||
oidc.WithProviderMetadata(&pm),
|
oidc.WithProviderMetadata(&pm),
|
||||||
oidc.WithClientID(clientID),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return verifier.VerifyLogoutToken(ctx, token)
|
return verifier.VerifyLogoutToken(ctx, token)
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ type Options struct {
|
|||||||
// AccessTokenVerifyMethod to use when verifying access tokens
|
// AccessTokenVerifyMethod to use when verifying access tokens
|
||||||
// TODO pass a function or interface to verify? an AccessTokenVerifier?
|
// TODO pass a function or interface to verify? an AccessTokenVerifier?
|
||||||
AccessTokenVerifyMethod string
|
AccessTokenVerifyMethod string
|
||||||
// ClientID the client id to expect in tokens. If not set SkipClientIDCheck must be true
|
|
||||||
// TODO also check in access token
|
|
||||||
ClientID string
|
|
||||||
// SkipClientIDCheck must be true if ClientID is empty
|
|
||||||
SkipClientIDCheck bool
|
|
||||||
// Config to use
|
// Config to use
|
||||||
Config *goidc.Config
|
Config *goidc.Config
|
||||||
|
|
||||||
@@ -92,20 +87,6 @@ func WithKeySet(val KeySet) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithClientID provides a function to set the clientID option.
|
|
||||||
func WithClientID(val string) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.ClientID = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSkipClientIDCheck provides a function to set the skipClientIDCheck option.
|
|
||||||
func WithSkipClientIDCheck(val bool) Option {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.SkipClientIDCheck = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithConfig provides a function to set the Config option.
|
// WithConfig provides a function to set the Config option.
|
||||||
func WithConfig(val *goidc.Config) Option {
|
func WithConfig(val *goidc.Config) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
|
|||||||
@@ -87,8 +87,6 @@ func Server(cfg *config.Config) *cli.Command {
|
|||||||
oidc.WithHTTPClient(oidcHTTPClient),
|
oidc.WithHTTPClient(oidcHTTPClient),
|
||||||
oidc.WithOidcIssuer(cfg.OIDC.Issuer),
|
oidc.WithOidcIssuer(cfg.OIDC.Issuer),
|
||||||
oidc.WithJWKSOptions(cfg.OIDC.JWKS),
|
oidc.WithJWKSOptions(cfg.OIDC.JWKS),
|
||||||
oidc.WithClientID(cfg.OIDC.ClientID),
|
|
||||||
oidc.WithSkipClientIDCheck(cfg.OIDC.SkipClientIDCheck),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -108,8 +108,6 @@ type OIDC struct {
|
|||||||
UserinfoCache *Cache `yaml:"user_info_cache"`
|
UserinfoCache *Cache `yaml:"user_info_cache"`
|
||||||
JWKS JWKS `yaml:"jwks"`
|
JWKS JWKS `yaml:"jwks"`
|
||||||
RewriteWellKnown bool `yaml:"rewrite_well_known" env:"PROXY_OIDC_REWRITE_WELLKNOWN" desc:"Enables rewriting the /.well-known/openid-configuration to the configured OIDC issuer. Needed by the Desktop Client, Android Client and iOS Client to discover the OIDC provider."`
|
RewriteWellKnown bool `yaml:"rewrite_well_known" env:"PROXY_OIDC_REWRITE_WELLKNOWN" desc:"Enables rewriting the /.well-known/openid-configuration to the configured OIDC issuer. Needed by the Desktop Client, Android Client and iOS Client to discover the OIDC provider."`
|
||||||
ClientID string `yaml:"client_id" env:"OCIS_OIDC_CLIENT_ID;PROXY_OIDC_CLIENT_ID" desc:"OIDC client ID, which ownCloud Web uses. This client needs to be set up in your external IDP (has no effect when using the builtin IDP)."`
|
|
||||||
SkipClientIDCheck bool `yaml:"skip_client_id_check" env:"PROXY_OIDC_SKIP_CLIENT_ID_CHECK" desc:"If true will skip checking the configured client ID is present in audience claims. See following chapter for more details: https://openid.net/specs/openid-connect-core-1_0.html#IDToken"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWKS struct {
|
type JWKS struct {
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ func DefaultConfig() *config.Config {
|
|||||||
RefreshTimeout: 10, // seconds
|
RefreshTimeout: 10, // seconds
|
||||||
RefreshUnknownKID: true,
|
RefreshUnknownKID: true,
|
||||||
},
|
},
|
||||||
ClientID: "web",
|
|
||||||
},
|
},
|
||||||
PolicySelector: nil,
|
PolicySelector: nil,
|
||||||
RoleAssignment: config.RoleAssignment{
|
RoleAssignment: config.RoleAssignment{
|
||||||
|
|||||||
Reference in New Issue
Block a user