From fda7222b1e6893ff97200e67156bc3f304fc9fa5 Mon Sep 17 00:00:00 2001 From: enj Date: Wed, 7 Sep 2016 17:44:54 -0400 Subject: [PATCH] Make OAuth provider discoverable from within a Pod https://trello.com/c/7uYQSTdR Signed-off-by: Monis Khan --- api/swagger-spec/openshift-openapi-spec.json | 20 ++++++- pkg/authorization/api/types.go | 1 + pkg/cmd/server/api/types.go | 2 +- pkg/cmd/server/origin/master.go | 60 +++++++++++++++---- pkg/oauth/api/validation/validation.go | 13 +++- pkg/oauth/discovery/discovery.go | 58 ++++++++++++++++++ pkg/oauth/discovery/discovery_test.go | 40 +++++++++++++ .../bootstrap_cluster_roles.yaml | 4 ++ 8 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 pkg/oauth/discovery/discovery.go create mode 100644 pkg/oauth/discovery/discovery_test.go diff --git a/api/swagger-spec/openshift-openapi-spec.json b/api/swagger-spec/openshift-openapi-spec.json index 412c20affb4f..514f16ef11cf 100644 --- a/api/swagger-spec/openshift-openapi-spec.json +++ b/api/swagger-spec/openshift-openapi-spec.json @@ -10,6 +10,23 @@ "version": "latest" }, "paths": { + "/.well-known/oauth-authorization-server/": { + "get": { + "description": "get the server's OAuth 2.0 Authorization Server Metadata", + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "operationId": "getOAuthAuthorizationServerMetadata", + "responses": { + "default": { + "description": "Default Response." + } + } + } + }, "/api/": { "get": { "description": "get available API versions", @@ -44079,9 +44096,6 @@ "/version/openshift/": { "get": { "description": "get the code version", - "consumes": [ - "application/json" - ], "produces": [ "application/json" ], diff --git a/pkg/authorization/api/types.go b/pkg/authorization/api/types.go index c8e19a1f66c8..fc325d0404e0 100644 --- a/pkg/authorization/api/types.go +++ b/pkg/authorization/api/types.go @@ -50,6 +50,7 @@ var DiscoveryRule = PolicyRule{ "/apis", "/apis/*", "/oapi", "/oapi/*", "/osapi", "/osapi/", // these cannot be removed until we can drop support for pre 3.1 clients + "/.well-known", "/.well-known/*", ), } diff --git a/pkg/cmd/server/api/types.go b/pkg/cmd/server/api/types.go index 70c5a9304265..34336d90de0d 100644 --- a/pkg/cmd/server/api/types.go +++ b/pkg/cmd/server/api/types.go @@ -635,7 +635,7 @@ type OAuthConfig struct { // MasterURL is used for making server-to-server calls to exchange authorization codes for access tokens MasterURL string - // MasterPublicURL is used for building valid client redirect URLs for external access + // MasterPublicURL is used for building valid client redirect URLs for internal and external access MasterPublicURL string // AssetPublicURL is used for building valid client redirect URLs for external access diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index 5892e182cbaf..903e5714fab0 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -69,6 +69,7 @@ import ( "github.com/openshift/origin/pkg/image/registry/imagestreammapping" "github.com/openshift/origin/pkg/image/registry/imagestreamtag" oauthapi "github.com/openshift/origin/pkg/oauth/api" + "github.com/openshift/origin/pkg/oauth/discovery" accesstokenetcd "github.com/openshift/origin/pkg/oauth/registry/oauthaccesstoken/etcd" authorizetokenetcd "github.com/openshift/origin/pkg/oauth/registry/oauthauthorizetoken/etcd" clientregistry "github.com/openshift/origin/pkg/oauth/registry/oauthclient" @@ -134,6 +135,10 @@ const ( OpenShiftAPIV1 = "v1" OpenShiftAPIPrefixV1 = OpenShiftAPIPrefix + "/" + OpenShiftAPIV1 swaggerAPIPrefix = "/swaggerapi/" + // Discovery endpoint for OAuth 2.0 Authorization Server Metadata + // See IETF Draft: + // https://tools.ietf.org/html/draft-ietf-oauth-discovery-04#section-2 + oauthMetadataEndpoint = "/.well-known/oauth-authorization-server" ) var ( @@ -446,35 +451,68 @@ func (c *MasterConfig) InstallProtectedAPI(container *restful.Container) ([]stri initReadinessCheckRoute(root, "/healthz/ready", c.ProjectAuthorizationCache.ReadyForAccess) initVersionRoute(container, "/version/openshift") + // Set up OAuth metadata only if we are configured to use OAuth + if c.Options.OAuthConfig != nil { + initOAuthAuthorizationServerMetadataRoute(container, oauthMetadataEndpoint, c.Options.OAuthConfig.MasterPublicURL) + } + return messages, nil } -// initReadinessCheckRoute initializes an HTTP endpoint for readiness checking +// initVersionRoute initializes an HTTP endpoint for the server's version information. func initVersionRoute(container *restful.Container, path string) { + // Build version info once + versionInfo, err := json.MarshalIndent(version.Get(), "", " ") + if err != nil { + glog.Errorf("Unable to initialize version route: %v", err) + return + } + // Set up a service to return the git code version. versionWS := new(restful.WebService) versionWS.Path(path) versionWS.Doc("git code version from which this is built") versionWS.Route( - versionWS.GET("/").To(handleVersion). + versionWS.GET("/").To(func(_ *restful.Request, resp *restful.Response) { + writeJSON(resp, versionInfo) + }). Doc("get the code version"). Operation("getCodeVersion"). - Produces(restful.MIME_JSON). - Consumes(restful.MIME_JSON)) + Produces(restful.MIME_JSON)) container.Add(versionWS) } -// handleVersion writes the server's version information. -func handleVersion(req *restful.Request, resp *restful.Response) { - output, err := json.MarshalIndent(version.Get(), "", " ") +func writeJSON(resp *restful.Response, json []byte) { + resp.ResponseWriter.Header().Set("Content-Type", "application/json") + resp.ResponseWriter.WriteHeader(http.StatusOK) + resp.ResponseWriter.Write(json) +} + +// initOAuthAuthorizationServerMetadataRoute initializes an HTTP endpoint for OAuth 2.0 Authorization Server Metadata discovery +// https://tools.ietf.org/id/draft-ietf-oauth-discovery-04.html#rfc.section.2 +// masterPublicURL should be internally and externally routable to allow all users to discover this information +func initOAuthAuthorizationServerMetadataRoute(container *restful.Container, path, masterPublicURL string) { + // Build OAuth metadata once + metadata, err := json.MarshalIndent(discovery.Get(masterPublicURL, OpenShiftOAuthAuthorizeURL(masterPublicURL), OpenShiftOAuthTokenURL(masterPublicURL)), "", " ") if err != nil { - http.Error(resp.ResponseWriter, err.Error(), http.StatusInternalServerError) + glog.Errorf("Unable to initialize OAuth authorization server metadata route: %v", err) return } - resp.ResponseWriter.Header().Set("Content-Type", "application/json") - resp.ResponseWriter.WriteHeader(http.StatusOK) - resp.ResponseWriter.Write(output) + + // Set up a service to return the OAuth metadata. + oauthWS := new(restful.WebService) + oauthWS.Path(path) + oauthWS.Doc("OAuth 2.0 Authorization Server Metadata") + oauthWS.Route( + oauthWS.GET("/").To(func(_ *restful.Request, resp *restful.Response) { + writeJSON(resp, metadata) + }). + Doc("get the server's OAuth 2.0 Authorization Server Metadata"). + Operation("getOAuthAuthorizationServerMetadata"). + Produces(restful.MIME_JSON)) + + container.Add(oauthWS) } func (c *MasterConfig) GetRestStorage() map[string]rest.Storage { diff --git a/pkg/oauth/api/validation/validation.go b/pkg/oauth/api/validation/validation.go index 52938e59e98f..846360f4625a 100644 --- a/pkg/oauth/api/validation/validation.go +++ b/pkg/oauth/api/validation/validation.go @@ -18,6 +18,15 @@ import ( const MinTokenLength = 32 +// PKCE [RFC7636] code challenge methods supported +// https://tools.ietf.org/html/rfc7636#section-4.3 +const ( + codeChallengeMethodPlain = "plain" + codeChallengeMethodSHA256 = "S256" +) + +var CodeChallengeMethodsSupported = []string{codeChallengeMethodPlain, codeChallengeMethodSHA256} + func ValidateTokenName(name string, prefix bool) []string { if reasons := oapi.MinimalNameRequirements(name, prefix); len(reasons) != 0 { return reasons @@ -101,10 +110,10 @@ func ValidateAuthorizeToken(authorizeToken *api.OAuthAuthorizeToken) field.Error switch authorizeToken.CodeChallengeMethod { case "": allErrs = append(allErrs, field.Required(field.NewPath("codeChallengeMethod"), "required if codeChallenge is specified")) - case "plain", "S256": + case codeChallengeMethodPlain, codeChallengeMethodSHA256: // no-op, good default: - allErrs = append(allErrs, field.NotSupported(field.NewPath("codeChallengeMethod"), authorizeToken.CodeChallengeMethod, []string{"plain", "S256"})) + allErrs = append(allErrs, field.NotSupported(field.NewPath("codeChallengeMethod"), authorizeToken.CodeChallengeMethod, CodeChallengeMethodsSupported)) } } diff --git a/pkg/oauth/discovery/discovery.go b/pkg/oauth/discovery/discovery.go new file mode 100644 index 000000000000..27b5b572ba73 --- /dev/null +++ b/pkg/oauth/discovery/discovery.go @@ -0,0 +1,58 @@ +package discovery + +import ( + "github.com/RangelReale/osin" + "github.com/openshift/origin/pkg/authorization/authorizer/scope" + "github.com/openshift/origin/pkg/oauth/api/validation" + "github.com/openshift/origin/pkg/oauth/server/osinserver" +) + +// OauthAuthorizationServerMetadata holds OAuth 2.0 Authorization Server Metadata used for discovery +// https://tools.ietf.org/html/draft-ietf-oauth-discovery-04#section-2 +type OauthAuthorizationServerMetadata struct { + // The authorization server's issuer identifier, which is a URL that uses the https scheme and has no query or fragment components. + // This is the location where .well-known RFC 5785 [RFC5785] resources containing information about the authorization server are published. + Issuer string `json:"issuer"` + + // URL of the authorization server's authorization endpoint [RFC6749]. + AuthorizationEndpoint string `json:"authorization_endpoint"` + + // URL of the authorization server's token endpoint [RFC6749]. + TokenEndpoint string `json:"token_endpoint"` + + // JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this authorization server supports. + // Servers MAY choose not to advertise some supported scope values even when this parameter is used. + ScopesSupported []string `json:"scopes_supported"` + + // JSON array containing a list of the OAuth 2.0 response_type values that this authorization server supports. + // The array values used are the same as those used with the response_types parameter defined by "OAuth 2.0 Dynamic Client Registration Protocol" [RFC7591]. + ResponseTypesSupported osin.AllowedAuthorizeType `json:"response_types_supported"` + + // JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports. + // The array values used are the same as those used with the grant_types parameter defined by "OAuth 2.0 Dynamic Client Registration Protocol" [RFC7591]. + GrantTypesSupported osin.AllowedAccessType `json:"grant_types_supported"` + + // JSON array containing a list of PKCE [RFC7636] code challenge methods supported by this authorization server. + // Code challenge method values are used in the "code_challenge_method" parameter defined in Section 4.3 of [RFC7636]. + // The valid code challenge method values are those registered in the IANA "PKCE Code Challenge Methods" registry [IANA.OAuth.Parameters]. + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` +} + +func Get(masterPublicURL, authorizeURL, tokenURL string) OauthAuthorizationServerMetadata { + config := osinserver.NewDefaultServerConfig() + return OauthAuthorizationServerMetadata{ + Issuer: masterPublicURL, + AuthorizationEndpoint: authorizeURL, + TokenEndpoint: tokenURL, + ScopesSupported: []string{ // Note: this list is incomplete, which is allowed per the draft spec + scope.UserFull, + scope.UserInfo, + scope.UserAccessCheck, + scope.UserListScopedProjects, + scope.UserListAllProjects, + }, + ResponseTypesSupported: config.AllowedAuthorizeTypes, + GrantTypesSupported: osin.AllowedAccessType{osin.AUTHORIZATION_CODE, osin.AccessRequestType("implicit")}, // TODO use config.AllowedAccessTypes once our implementation handles other grant types + CodeChallengeMethodsSupported: validation.CodeChallengeMethodsSupported, + } +} diff --git a/pkg/oauth/discovery/discovery_test.go b/pkg/oauth/discovery/discovery_test.go new file mode 100644 index 000000000000..1c0f59298a47 --- /dev/null +++ b/pkg/oauth/discovery/discovery_test.go @@ -0,0 +1,40 @@ +package discovery + +import ( + "reflect" + "testing" + + "github.com/RangelReale/osin" +) + +func TestGet(t *testing.T) { + actual := Get("https://localhost:8443", "https://localhost:8443/oauth/authorize", "https://localhost:8443/oauth/token") + expected := OauthAuthorizationServerMetadata{ + Issuer: "https://localhost:8443", + AuthorizationEndpoint: "https://localhost:8443/oauth/authorize", + TokenEndpoint: "https://localhost:8443/oauth/token", + ScopesSupported: []string{ + "user:full", + "user:info", + "user:check-access", + "user:list-scoped-projects", + "user:list-projects", + }, + ResponseTypesSupported: osin.AllowedAuthorizeType{ + "code", + "token", + }, + GrantTypesSupported: osin.AllowedAccessType{ + "authorization_code", + "implicit", + }, + CodeChallengeMethodsSupported: []string{ + "plain", + "S256", + }, + } + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Expected %#v, got %#v", expected, actual) + } +} diff --git a/test/testdata/bootstrappolicy/bootstrap_cluster_roles.yaml b/test/testdata/bootstrappolicy/bootstrap_cluster_roles.yaml index e35f624af357..7026b9bef653 100644 --- a/test/testdata/bootstrappolicy/bootstrap_cluster_roles.yaml +++ b/test/testdata/bootstrappolicy/bootstrap_cluster_roles.yaml @@ -1497,6 +1497,8 @@ items: - apiGroups: null attributeRestrictions: null nonResourceURLs: + - /.well-known + - /.well-known/* - /api - /api/* - /apis @@ -2114,6 +2116,8 @@ items: - apiGroups: null attributeRestrictions: null nonResourceURLs: + - /.well-known + - /.well-known/* - /api - /api/* - /apis