Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Commit

Permalink
Add support for dynamic OIDC configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
nacx committed Feb 16, 2024
1 parent 76e9f95 commit 06741bc
Show file tree
Hide file tree
Showing 11 changed files with 451 additions and 121 deletions.
196 changes: 104 additions & 92 deletions config/gen/go/v1/oidc/config.pb.go

Large diffs are not rendered by default.

24 changes: 4 additions & 20 deletions config/gen/go/v1/oidc/config.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions config/v1/oidc/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,17 @@ message LogoutConfig {
// The configuration of an OpenID Connect filter that can be used to retrieve identity and access tokens
// via the standard authorization code grant flow from an OIDC Provider.
message OIDCConfig {
// The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig).
// If this is set, the endpoints will be dynamically retrieved from the OIDC Provider's configuration endpoint.
string configuration_uri = 19;

// The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint).
// Required.
string authorization_uri = 1 [(validate.rules).string.min_len = 1];
// Required if `configuration_uri` is not set.
string authorization_uri = 1;

// The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint).
// Required.
string token_uri = 2 [(validate.rules).string.min_len = 1];
// Required if `configuration_uri` is not set.
string token_uri = 2;

// This value will be used as the `redirect_uri` param of the authorization code grant
// [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
Expand All @@ -93,7 +96,7 @@ message OIDCConfig {
// JWT validation at regular intervals.
message JwksFetcherConfig {
// Request URI that has the JWKs.
// Required.
// Required if `configuration_uri` is not set.
string jwks_uri = 1;

// Request interval to check whether new JWKs are available. If not specified,
Expand Down
27 changes: 27 additions & 0 deletions internal/authz/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ func NewOIDCHandler(cfg *oidcv1.OIDCConfig, jwks oidc.JWKSProvider,
client := &http.Client{Transport: transport}
// TODO (sergicastro) use proxy uri

if err := loadWellKnownConfig(client, cfg); err != nil {
return nil, err
}

return &oidcHandler{
log: internal.Logger(internal.Authz).With("type", "oidc"),
config: cfg,
Expand Down Expand Up @@ -583,3 +587,26 @@ func getCookieName(config *oidcv1.OIDCConfig) string {
}
return defaultCookieName
}

// loadWellKnownConfig loads the OIDC well-known configuration into the given OIDCConfig.
func loadWellKnownConfig(client *http.Client, cfg *oidcv1.OIDCConfig) error {
if cfg.GetConfigurationUri() == "" {
return nil
}

wellKnownConfig, err := oidc.GetWellKnownConfig(client, cfg.GetConfigurationUri())
if err != nil {
return err
}

cfg.AuthorizationUri = wellKnownConfig.AuthorizationEndpoint
cfg.TokenUri = wellKnownConfig.TokenEndpoint
if cfg.GetJwksFetcher() == nil {
cfg.JwksConfig = &oidcv1.OIDCConfig_JwksFetcher{
JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{},
}
}
cfg.GetJwksFetcher().JwksUri = wellKnownConfig.JWKSURL

return nil
}
47 changes: 47 additions & 0 deletions internal/authz/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,30 @@ var (
ClientSecret: "test-client-secret",
Scopes: []string{"openid", "email"},
}

dynamicOIDCConfig = &oidcv1.OIDCConfig{
IdToken: &oidcv1.TokenConfig{
Header: "Authorization",
Preamble: "Bearer",
},
AccessToken: &oidcv1.TokenConfig{
Header: "X-Access-Token",
Preamble: "Bearer",
},
ConfigurationUri: "http://idp-test-server/.well-known/openid-configuration",
CallbackUri: "https://localhost:443/callback",
ClientId: "test-client-id",
ClientSecret: "test-client-secret",
Scopes: []string{"openid", "email"},
}

wellKnownURIs = `
{
"issuer": "http://idp-test-server",
"authorization_endpoint": "http://idp-test-server/authorize",
"token_endpoint": "http://idp-test-server/token",
"jwks_uri": "http://idp-test-server/jwks"
}`
)

func TestOIDCProcess(t *testing.T) {
Expand Down Expand Up @@ -755,6 +779,24 @@ func TestAreTokensExpired(t *testing.T) {
}
}

func TestLoadWellKnownConfig(t *testing.T) {
idpServer := newServer()
idpServer.Start()
t.Cleanup(idpServer.Stop)

require.NoError(t, loadWellKnownConfig(idpServer.newHTTPClient(), dynamicOIDCConfig))
require.Equal(t, dynamicOIDCConfig.AuthorizationUri, "http://idp-test-server/authorize")
require.Equal(t, dynamicOIDCConfig.TokenUri, "http://idp-test-server/token")
require.Equal(t, dynamicOIDCConfig.GetJwksFetcher().GetJwksUri(), "http://idp-test-server/jwks")
}

func TestLoadWellKnownConfigError(t *testing.T) {
clock := oidc.Clock{}
sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)}
_, err := NewOIDCHandler(dynamicOIDCConfig, oidc.NewJWKSProvider(), sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState))
require.Error(t, err) // Fail to retrieve the dynamic config since the test server is not running
}

func modifyCallbackRequestPath(path string) *envoy.CheckRequest {
return &envoy.CheckRequest{
Attributes: &envoy.AttributeContext{
Expand Down Expand Up @@ -901,6 +943,11 @@ func newServer() *idpServer {
}
}
})
handler.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(wellKnownURIs))
})
s.Handler = handler
return idpServer
}
Expand Down
26 changes: 22 additions & 4 deletions internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ var (
ErrDuplicateOIDCConfig = errors.New("duplicate OIDC configuration")
ErrMultipleOIDCConfig = errors.New("multiple OIDC configurations")
ErrInvalidRedisURL = errors.New("invalid Redis URL")
ErrRequiredURL = errors.New("required URL")
)

// LocalConfigFile is a run.Config that loads the configuration file.
type LocalConfigFile struct {
path string
// Config is the loaded configuration.
path string
Config configv1.Config
}

Expand Down Expand Up @@ -108,8 +108,14 @@ func (l *LocalConfigFile) Validate() error {
// mergeAndValidateOIDCConfigs merges the OIDC overrides with the default OIDC configuration so that
// all filters have only one location where the OIDC configuration is defined.
func mergeAndValidateOIDCConfigs(cfg *configv1.Config) error {
var errs []error

for _, fc := range cfg.Chains {
for _, f := range fc.Filters {
if _, ok := f.Type.(*configv1.Filter_Mock); ok {
continue
}

// Merge the OIDC overrides and populate the normal OIDC field instead so that
// consumers of the config always have an up-to-date object
if f.GetOidcOverride() != nil {
Expand All @@ -120,7 +126,19 @@ func mergeAndValidateOIDCConfigs(cfg *configv1.Config) error {

if redisURL := f.GetOidc().GetRedisSessionStoreConfig().GetServerUri(); redisURL != "" {
if _, err := redis.ParseURL(redisURL); err != nil {
return fmt.Errorf("%w: invalid redis URL in chain %q", ErrInvalidRedisURL, fc.Name)
errs = append(errs, fmt.Errorf("%w: invalid redis URL in chain %q", ErrInvalidRedisURL, fc.Name))
}
}

if f.GetOidc().GetConfigurationUri() == "" {
if f.GetOidc().GetAuthorizationUri() == "" {
errs = append(errs, fmt.Errorf("%w: missing authorization URI in chain %q", ErrRequiredURL, fc.Name))
}
if f.GetOidc().GetTokenUri() == "" {
errs = append(errs, fmt.Errorf("%w: missing token URI in chain %q", ErrRequiredURL, fc.Name))
}
if f.GetOidc().GetJwks() == "" && f.GetOidc().GetJwksFetcher().GetJwksUri() == "" {
errs = append(errs, fmt.Errorf("%w: missing JWKS URI in chain %q", ErrRequiredURL, fc.Name))
}
}
}
Expand All @@ -129,7 +147,7 @@ func mergeAndValidateOIDCConfigs(cfg *configv1.Config) error {
// location for the OIDC settings.
cfg.DefaultOidcConfig = nil

return nil
return errors.Join(errs...)
}

func ConfigToJSONString(c *configv1.Config) string {
Expand Down
2 changes: 2 additions & 0 deletions internal/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func TestValidateConfig(t *testing.T) {
{"invalid-oidc-override", "testdata/invalid-oidc-override.json", errCheck{is: ErrInvalidOIDCOverride}},
{"multiple-oidc", "testdata/multiple-oidc.json", errCheck{is: ErrMultipleOIDCConfig}},
{"invalid-redis", "testdata/invalid-redis.json", errCheck{is: ErrInvalidRedisURL}},
{"invalid-oidc-uris", "testdata/invalid-oidc-uris.json", errCheck{is: ErrRequiredURL}},
{"oidc-dynamic", "testdata/oidc-dynamic.json", errCheck{is: nil}},
{"valid", "testdata/mock.json", errCheck{is: nil}},
}

Expand Down
64 changes: 64 additions & 0 deletions internal/oidc/discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2024 Tetrate
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package oidc

import (
"encoding/json"
"fmt"
"net/http"
)

// WellKnownConfig represents the OIDC well-known configuration
type WellKnownConfig struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
JWKSURL string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgorithms []string `json:"id_token_signing_alg_values_supported"`
TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
EndSessionEndpoint string `json:"end_session_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
IntrospectionEndpoint string `json:"introspection_endpoint"`
ScopesSupported []string `json:"scopes_supported"`
ClaimsSupported []string `json:"claims_supported"`
CodeChallengeMethods []string `json:"code_challenge_methods_supported"`
TokenRevocationEndpoint string `json:"token_revocation_endpoint"`
}

// GetWellKnownConfig retrieves the OIDC well-known configuration from the given issuer URL.
func GetWellKnownConfig(client *http.Client, url string) (WellKnownConfig, error) {
// Make a GET request to the well-known configuration endpoint
response, err := client.Get(url)
if err != nil {
return WellKnownConfig{}, err
}
defer func() { _ = response.Body.Close() }()

// Check if the response status code is successful
if response.StatusCode != http.StatusOK {
return WellKnownConfig{}, fmt.Errorf("failed to retrieve OIDC config: %s", response.Status)
}

// Decode the JSON response into the OIDCConfig struct
var cfg WellKnownConfig
if err = json.NewDecoder(response.Body).Decode(&cfg); err != nil {
return WellKnownConfig{}, err
}

return cfg, nil
}
Loading

0 comments on commit 06741bc

Please sign in to comment.