Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make OAuth provider discoverable from within a Pod #10845

Merged
merged 1 commit into from
Oct 20, 2016
Merged
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
20 changes: 17 additions & 3 deletions api/swagger-spec/openshift-openapi-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -44079,9 +44096,6 @@
"/version/openshift/": {
"get": {
"description": "get the code version",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
Expand Down
1 change: 1 addition & 0 deletions pkg/authorization/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/*",
),
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/server/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 49 additions & 11 deletions pkg/cmd/server/origin/master.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 11 additions & 2 deletions pkg/oauth/api/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
}

Expand Down
58 changes: 58 additions & 0 deletions pkg/oauth/discovery/discovery.go
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the remaining metadata attributes? Why only those?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I focused on the REQUIRED and RECOMMENDED metadata. A lot of the OPTIONAL ones are not supported by OpenShift (for example, jwks_uri and token_endpoint_auth_signing_alg_values_supported). Since @liggitt just added PKCE support, I could add code_challenge_methods_supported (but that may be out of scope and could certainly be added later). token_endpoint_auth_methods_supported, service_documentation, ui_locales_supported, op_policy_uri, op_tos_uri, protected_resources, etc may all be valid now or in the future, but are probably out of scope as well.

// 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"`
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these scopes and not others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see an easy way for me to get any other scopes. I welcome suggestions for making this list more complete.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the remaining role scope is varying, since it contains actual role name and namespace. Well, we're complaint with the spec, since it allows not to advertise all. Thanks for the info

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,
}
}
40 changes: 40 additions & 0 deletions pkg/oauth/discovery/discovery_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 4 additions & 0 deletions test/testdata/bootstrappolicy/bootstrap_cluster_roles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,8 @@ items:
- apiGroups: null
attributeRestrictions: null
nonResourceURLs:
- /.well-known
- /.well-known/*
- /api
- /api/*
- /apis
Expand Down Expand Up @@ -2114,6 +2116,8 @@ items:
- apiGroups: null
attributeRestrictions: null
nonResourceURLs:
- /.well-known
- /.well-known/*
- /api
- /api/*
- /apis
Expand Down