Skip to content

Commit

Permalink
Make OAuth provider discoverable from within a Pod
Browse files Browse the repository at this point in the history
  • Loading branch information
enj committed Oct 6, 2016
1 parent 43d24b2 commit ef95e46
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 17 deletions.
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 {
// 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}, // TODO use config.AllowedAccessTypes once our implementation handles other grant types
CodeChallengeMethodsSupported: validation.CodeChallengeMethodsSupported,
}
}
39 changes: 39 additions & 0 deletions pkg/oauth/discovery/discovery_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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",
},
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

0 comments on commit ef95e46

Please sign in to comment.