Skip to content

Commit 735a92f

Browse files
committed
Add option to configure an external OAuth server
Signed-off-by: Simo Sorce <[email protected]>
1 parent bb8bbbe commit 735a92f

File tree

11 files changed

+156
-36
lines changed

11 files changed

+156
-36
lines changed

Diff for: pkg/authorization/authorizer/scope/converter.go

+13
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,19 @@ func DefaultSupportedScopesMap() map[string]string {
150150
return defaultSupportedScopesMap
151151
}
152152

153+
func DescribeScopes(scopes []string) map[string]string {
154+
ret := map[string]string{}
155+
for _, s := range scopes {
156+
val, ok := defaultSupportedScopesMap[s]
157+
if ok {
158+
ret[s] = val
159+
} else {
160+
ret[s] = ""
161+
}
162+
}
163+
return ret
164+
}
165+
153166
// user:<scope name>
154167
type userEvaluator struct{}
155168

Diff for: pkg/cmd/server/apis/config/types.go

+5
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ type MasterConfig struct {
381381
EtcdConfig *EtcdConfig
382382
// OAuthConfig, if present start the /oauth endpoint in this process
383383
OAuthConfig *OAuthConfig
384+
384385
// DNSConfig, if present start the DNS server in this process
385386
DNSConfig *DNSConfig
386387

@@ -430,6 +431,10 @@ type MasterAuthConfig struct {
430431
RequestHeader *RequestHeaderAuthenticationOptions
431432
// WebhookTokenAuthnConfig, if present configures remote token reviewers
432433
WebhookTokenAuthenticators []WebhookTokenAuthenticator
434+
// OAuthMetadataFile is a path to a file containing the discovery endpoint for OAuth 2.0 Authorization
435+
// Server Metadata for an external OAuth server.
436+
// See IETF Draft: // https://tools.ietf.org/html/draft-ietf-oauth-discovery-04#section-2
437+
OAuthMetadataFile string
433438
}
434439

435440
// RequestHeaderAuthenticationOptions provides options for setting up a front proxy against the entire

Diff for: pkg/cmd/server/apis/config/v1/testdata/master-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ auditConfig:
2727
webHookKubeConfig: ""
2828
webHookMode: ""
2929
authConfig:
30+
oauthMetadataFile: ""
3031
requestHeader: null
3132
webhookTokenAuthenticators: null
3233
controllerConfig:

Diff for: pkg/cmd/server/apis/config/v1/types.go

+5
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ type MasterConfig struct {
244244
EtcdConfig *EtcdConfig `json:"etcdConfig"`
245245
// OAuthConfig, if present start the /oauth endpoint in this process
246246
OAuthConfig *OAuthConfig `json:"oauthConfig"`
247+
247248
// DNSConfig, if present start the DNS server in this process
248249
DNSConfig *DNSConfig `json:"dnsConfig"`
249250

@@ -293,6 +294,10 @@ type MasterAuthConfig struct {
293294
RequestHeader *RequestHeaderAuthenticationOptions `json:"requestHeader"`
294295
// WebhookTokenAuthnConfig, if present configures remote token reviewers
295296
WebhookTokenAuthenticators []WebhookTokenAuthenticator `json:"webhookTokenAuthenticators"`
297+
// OAuthMetadataFile is a path to a file containing the discovery endpoint for OAuth 2.0 Authorization
298+
// Server Metadata for an external OAuth server.
299+
// See IETF Draft: // https://tools.ietf.org/html/draft-ietf-oauth-discovery-04#section-2
300+
OAuthMetadataFile string `json:"oauthMetadataFile"`
296301
}
297302

298303
// RequestHeaderAuthenticationOptions provides options for setting up a front proxy against the entire

Diff for: pkg/cmd/server/apis/config/validation/master.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
configapi "github.com/openshift/origin/pkg/cmd/server/apis/config"
2626
"github.com/openshift/origin/pkg/cmd/server/bootstrappolicy"
2727
"github.com/openshift/origin/pkg/cmd/server/cm"
28+
oauthutil "github.com/openshift/origin/pkg/oauth/util"
2829
"github.com/openshift/origin/pkg/security/mcs"
2930
"github.com/openshift/origin/pkg/security/uid"
3031
"github.com/openshift/origin/pkg/util/labelselector"
@@ -141,8 +142,10 @@ func ValidateMasterConfig(config *configapi.MasterConfig, fldPath *field.Path) V
141142
validationResults.AddErrors(ValidatePolicyConfig(config.PolicyConfig, fldPath.Child("policyConfig"))...)
142143
if config.OAuthConfig != nil {
143144
validationResults.Append(ValidateOAuthConfig(config.OAuthConfig, fldPath.Child("oauthConfig")))
145+
if len(config.AuthConfig.OAuthMetadataFile) > 0 {
146+
validationResults.AddErrors(field.Invalid(fldPath.Child("authConfig", "oauthMetadataFile"), config.AuthConfig.OAuthMetadataFile, "Cannot specify external OAuth Metadata when the internal Oauth Server is configured"))
147+
}
144148
}
145-
146149
validationResults.Append(ValidateServiceAccountConfig(config.ServiceAccountConfig, builtInKubernetes, fldPath.Child("serviceAccountConfig")))
147150

148151
validationResults.Append(ValidateHTTPServingInfo(config.ServingInfo, fldPath.Child("servingInfo")))
@@ -171,6 +174,12 @@ func ValidateMasterConfig(config *configapi.MasterConfig, fldPath *field.Path) V
171174
func ValidateMasterAuthConfig(config configapi.MasterAuthConfig, fldPath *field.Path) ValidationResults {
172175
validationResults := ValidationResults{}
173176

177+
if len(config.OAuthMetadataFile) > 0 {
178+
if _, _, err := oauthutil.LoadOAuthMetadataFile(config.OAuthMetadataFile); err != nil {
179+
validationResults.AddErrors(field.Invalid(fldPath.Child("oauthMetadataFile"), config.OAuthMetadataFile, fmt.Sprintf("Metadata validation failed: %v", err)))
180+
}
181+
}
182+
174183
for _, wta := range config.WebhookTokenAuthenticators {
175184
configFile := fldPath.Child("webhookTokenAuthenticators", "ConfigFile")
176185
if len(wta.ConfigFile) == 0 {

Diff for: pkg/cmd/server/apis/config/validation/master_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -535,10 +535,24 @@ func TestValidateMasterAuthConfig(t *testing.T) {
535535
}
536536
defer os.Remove(testConfigFile.Name())
537537

538+
metadataFile, err := ioutil.TempFile("", "oauth.metadata")
539+
if err != nil {
540+
t.Fatalf("unexpected error: %v", err)
541+
}
542+
defer os.Remove(metadataFile.Name())
543+
ioutil.WriteFile(metadataFile.Name(), testMetadataContent, os.FileMode(0644))
544+
badMetadataFile, err := ioutil.TempFile("", "badoauth.metadata")
545+
if err != nil {
546+
t.Fatalf("unexpected error: %v", err)
547+
}
548+
defer os.Remove(badMetadataFile.Name())
549+
ioutil.WriteFile(badMetadataFile.Name(), []byte("bad file"), os.FileMode(0644))
550+
538551
testCases := []struct {
539552
testName string
540553
RequestHeader *configapi.RequestHeaderAuthenticationOptions
541554
WebhookTokenAuthenticators []configapi.WebhookTokenAuthenticator
555+
OAuthMetadataFile string
542556
expectedErrors []string
543557
}{
544558
{
@@ -593,11 +607,22 @@ func TestValidateMasterAuthConfig(t *testing.T) {
593607
"webhookTokenAuthenticators.cacheTTL: Required value",
594608
},
595609
},
610+
{
611+
testName: "No OAuth Metadata file",
612+
OAuthMetadataFile: "NoFile",
613+
expectedErrors: []string{"Metadata validation failed: Unable to read External OAuth Metadata file: open NoFile: no such file or directory"},
614+
},
615+
{
616+
name: "Bad Metadata file",
617+
OAuthMetadataFile: badMetadataFile.Name(),
618+
expectedErrors: []string{"Metadata validation failed: Unable to decode External OAuth Metadata file: invalid character 'b' looking for beginning of value"},
619+
},
596620
}
597621
for _, test := range testCases {
598622
config := configapi.MasterAuthConfig{
599623
RequestHeader: test.RequestHeader,
600624
WebhookTokenAuthenticators: test.WebhookTokenAuthenticators,
625+
OAuthMetadataFile: test.OAuthMetadataFile,
601626
}
602627
errors := ValidateMasterAuthConfig(config, nil)
603628
if len(test.expectedErrors) != len(errors.Errors) {
@@ -611,3 +636,12 @@ func TestValidateMasterAuthConfig(t *testing.T) {
611636
}
612637
}
613638
}
639+
640+
var testMetadataContent = []byte(`{
641+
"issuer": "https://127.0.0.1/",
642+
"authorization_endpoint": "https://127.0.0.1/",
643+
"token_endpoint": "https://127.0.0.1/",
644+
"scopes_supported": ["openid", "profile", "email", "address", "phone", "offline_access"],
645+
"response_types_supported": ["code", "code token"],
646+
"grant_types_supported": ["authorization_code", "implicit"],
647+
"code_challenge_methods_supported": ["plain", "S256"]}`)

Diff for: pkg/cmd/server/kubernetes/master/master_config.go

+6-7
Original file line numberDiff line numberDiff line change
@@ -598,23 +598,22 @@ func defaultOpenAPIConfig(config configapi.MasterConfig) *openapicommon.Config {
598598
},
599599
}
600600
}
601-
if config.OAuthConfig != nil {
602-
baseUrl := config.OAuthConfig.MasterPublicURL
601+
if _, oauthMetadata, _ := oauthutil.PrepOauthMetadata(config); oauthMetadata != nil {
603602
securityDefinitions["Oauth2Implicit"] = &spec.SecurityScheme{
604603
SecuritySchemeProps: spec.SecuritySchemeProps{
605604
Type: "oauth2",
606605
Flow: "implicit",
607-
AuthorizationURL: oauthutil.OpenShiftOAuthAuthorizeURL(baseUrl),
608-
Scopes: scope.DefaultSupportedScopesMap(),
606+
AuthorizationURL: oauthMetadata.AuthorizationEndpoint,
607+
Scopes: scope.DescribeScopes(oauthMetadata.ScopesSupported),
609608
},
610609
}
611610
securityDefinitions["Oauth2AccessToken"] = &spec.SecurityScheme{
612611
SecuritySchemeProps: spec.SecuritySchemeProps{
613612
Type: "oauth2",
614613
Flow: "accessCode",
615-
AuthorizationURL: oauthutil.OpenShiftOAuthAuthorizeURL(baseUrl),
616-
TokenURL: oauthutil.OpenShiftOAuthTokenURL(baseUrl),
617-
Scopes: scope.DefaultSupportedScopesMap(),
614+
AuthorizationURL: oauthMetadata.AuthorizationEndpoint,
615+
TokenURL: oauthMetadata.TokenEndpoint,
616+
Scopes: scope.DescribeScopes(oauthMetadata.ScopesSupported),
618617
},
619618
}
620619
}

Diff for: pkg/cmd/server/origin/master.go

+10-8
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,20 @@ func (c *MasterConfig) newOpenshiftAPIConfig(kubeAPIServerConfig apiserver.Confi
7575
return ret, ret.ExtraConfig.Validate()
7676
}
7777

78-
func (c *MasterConfig) newOpenshiftNonAPIConfig(kubeAPIServerConfig apiserver.Config) *OpenshiftNonAPIConfig {
78+
func (c *MasterConfig) newOpenshiftNonAPIConfig(kubeAPIServerConfig apiserver.Config) (*OpenshiftNonAPIConfig, error) {
79+
var err error
7980
ret := &OpenshiftNonAPIConfig{
8081
GenericConfig: &apiserver.RecommendedConfig{
8182
Config: kubeAPIServerConfig,
8283
SharedInformerFactory: c.ClientGoKubeInformers,
8384
},
84-
ExtraConfig: NonAPIExtraConfig{
85-
EnableOAuth: c.Options.OAuthConfig != nil,
86-
},
8785
}
88-
if c.Options.OAuthConfig != nil {
89-
ret.ExtraConfig.MasterPublicURL = c.Options.OAuthConfig.MasterPublicURL
86+
ret.ExtraConfig.OAuthMetadata, _, err = oauthutil.PrepOauthMetadata(c.Options)
87+
if err != nil {
88+
return nil, err
9089
}
9190

92-
return ret
91+
return ret, nil
9392
}
9493

9594
func (c *MasterConfig) withAPIExtensions(delegateAPIServer apiserver.DelegationTarget, kubeAPIServerConfig apiserver.Config) (apiserver.DelegationTarget, apiextensionsinformers.SharedInformerFactory, error) {
@@ -110,7 +109,10 @@ func (c *MasterConfig) withAPIExtensions(delegateAPIServer apiserver.DelegationT
110109
}
111110

112111
func (c *MasterConfig) withNonAPIRoutes(delegateAPIServer apiserver.DelegationTarget, kubeAPIServerConfig apiserver.Config) (apiserver.DelegationTarget, error) {
113-
openshiftNonAPIConfig := c.newOpenshiftNonAPIConfig(kubeAPIServerConfig)
112+
openshiftNonAPIConfig, err := c.newOpenshiftNonAPIConfig(kubeAPIServerConfig)
113+
if err != nil {
114+
return nil, err
115+
}
114116
openshiftNonAPIServer, err := openshiftNonAPIConfig.Complete().New(delegateAPIServer)
115117
if err != nil {
116118
return nil, err

Diff for: pkg/cmd/server/origin/nonapiserver.go

+6-18
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
package origin
22

33
import (
4-
"encoding/json"
54
"net/http"
65

7-
"github.com/golang/glog"
8-
96
genericmux "k8s.io/apiserver/pkg/server/mux"
107

11-
oauthutil "github.com/openshift/origin/pkg/oauth/util"
128
genericapiserver "k8s.io/apiserver/pkg/server"
139
)
1410

1511
type NonAPIExtraConfig struct {
16-
MasterPublicURL string
17-
EnableOAuth bool
12+
OAuthMetadata []byte
1813
}
1914

2015
type OpenshiftNonAPIConfig struct {
@@ -59,8 +54,8 @@ func (c completedOpenshiftNonAPIConfig) New(delegationTarget genericapiserver.De
5954

6055
// TODO move this up to the spot where we wire the oauth endpoint
6156
// Set up OAuth metadata only if we are configured to use OAuth
62-
if c.ExtraConfig.EnableOAuth {
63-
initOAuthAuthorizationServerMetadataRoute(s.GenericAPIServer.Handler.NonGoRestfulMux, oauthMetadataEndpoint, c.ExtraConfig.MasterPublicURL)
57+
if len(c.ExtraConfig.OAuthMetadata) > 0 {
58+
initOAuthAuthorizationServerMetadataRoute(s.GenericAPIServer.Handler.NonGoRestfulMux, c.ExtraConfig)
6459
}
6560

6661
return s, nil
@@ -76,17 +71,10 @@ const (
7671
// initOAuthAuthorizationServerMetadataRoute initializes an HTTP endpoint for OAuth 2.0 Authorization Server Metadata discovery
7772
// https://tools.ietf.org/id/draft-ietf-oauth-discovery-04.html#rfc.section.2
7873
// masterPublicURL should be internally and externally routable to allow all users to discover this information
79-
func initOAuthAuthorizationServerMetadataRoute(mux *genericmux.PathRecorderMux, path, masterPublicURL string) {
80-
// Build OAuth metadata once
81-
metadata, err := json.MarshalIndent(oauthutil.GetOauthMetadata(masterPublicURL), "", " ")
82-
if err != nil {
83-
glog.Errorf("Unable to initialize OAuth authorization server metadata route: %v", err)
84-
return
85-
}
86-
87-
mux.UnlistedHandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
74+
func initOAuthAuthorizationServerMetadataRoute(mux *genericmux.PathRecorderMux, ExtraConfig *NonAPIExtraConfig) {
75+
mux.UnlistedHandleFunc(oauthMetadataEndpoint, func(w http.ResponseWriter, req *http.Request) {
8876
w.Header().Set("Content-Type", "application/json")
8977
w.WriteHeader(http.StatusOK)
90-
w.Write(metadata)
78+
w.Write(ExtraConfig.OAuthMetadata)
9179
})
9280
}

Diff for: pkg/oauth/util/discovery.go

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
package util
22

33
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"net/url"
8+
9+
"github.com/golang/glog"
10+
411
"github.com/RangelReale/osin"
512
"github.com/openshift/origin/pkg/authorization/authorizer/scope"
13+
configapi "github.com/openshift/origin/pkg/cmd/server/apis/config"
614
"github.com/openshift/origin/pkg/oauth/apis/oauth/validation"
715
"github.com/openshift/origin/pkg/oauthserver/osinserver"
816
)
@@ -38,7 +46,7 @@ type OauthAuthorizationServerMetadata struct {
3846
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
3947
}
4048

41-
func GetOauthMetadata(masterPublicURL string) OauthAuthorizationServerMetadata {
49+
func getOauthMetadata(masterPublicURL string) OauthAuthorizationServerMetadata {
4250
config := osinserver.NewDefaultServerConfig()
4351
return OauthAuthorizationServerMetadata{
4452
Issuer: masterPublicURL,
@@ -51,3 +59,59 @@ func GetOauthMetadata(masterPublicURL string) OauthAuthorizationServerMetadata {
5159
CodeChallengeMethodsSupported: validation.CodeChallengeMethodsSupported,
5260
}
5361
}
62+
63+
func validateURL(urlString string) error {
64+
urlObj, err := url.Parse(urlString)
65+
if err != nil {
66+
return fmt.Errorf("%q is an invalid URL: %v", urlString, err)
67+
}
68+
if len(urlObj.Scheme) == 0 {
69+
return fmt.Errorf("must contain a valid scheme")
70+
}
71+
if len(urlObj.Host) == 0 {
72+
return fmt.Errorf("must contain a valid host")
73+
}
74+
return nil
75+
}
76+
77+
func LoadOAuthMetadataFile(metadataFile string) ([]byte, *OauthAuthorizationServerMetadata, error) {
78+
data, err := ioutil.ReadFile(metadataFile)
79+
if err != nil {
80+
return nil, nil, fmt.Errorf("Unable to read External OAuth Metadata file: %v", err)
81+
}
82+
83+
oauthMetadata := &OauthAuthorizationServerMetadata{}
84+
if err := json.Unmarshal(data, oauthMetadata); err != nil {
85+
return nil, nil, fmt.Errorf("Unable to decode External OAuth Metadata file: %v", err)
86+
}
87+
88+
if err := validateURL(oauthMetadata.Issuer); err != nil {
89+
return nil, nil, fmt.Errorf("Error validating External OAuth Metadata Issuer field: %v", err)
90+
}
91+
92+
if err := validateURL(oauthMetadata.AuthorizationEndpoint); err != nil {
93+
return nil, nil, fmt.Errorf("Error validating External OAuth Metadata AuthorizationEndpoint field: %v", err)
94+
}
95+
96+
if err := validateURL(oauthMetadata.TokenEndpoint); err != nil {
97+
return nil, nil, fmt.Errorf("Error validating External OAuth Metadata TokenEndpoint field: %v", err)
98+
}
99+
100+
return data, oauthMetadata, nil
101+
}
102+
103+
func PrepOauthMetadata(config configapi.MasterConfig) ([]byte, *OauthAuthorizationServerMetadata, error) {
104+
if config.OAuthConfig != nil {
105+
metadataStruct := getOauthMetadata(config.OAuthConfig.MasterPublicURL)
106+
metadata, err := json.MarshalIndent(metadataStruct, "", " ")
107+
if err != nil {
108+
glog.Errorf("Unable to initialize OAuth authorization server metadata route: %v", err)
109+
return nil, nil, err
110+
}
111+
return metadata, &metadataStruct, nil
112+
}
113+
if len(config.AuthConfig.OAuthMetadataFile) > 0 {
114+
return LoadOAuthMetadataFile(config.AuthConfig.OAuthMetadataFile)
115+
}
116+
return nil, nil, nil
117+
}

Diff for: pkg/oauth/util/discovery_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
func TestGetOauthMetadata(t *testing.T) {
11-
actual := GetOauthMetadata("https://localhost:8443")
11+
actual := getOauthMetadata("https://localhost:8443")
1212
expected := OauthAuthorizationServerMetadata{
1313
Issuer: "https://localhost:8443",
1414
AuthorizationEndpoint: "https://localhost:8443/oauth/authorize",

0 commit comments

Comments
 (0)