Browse code

Make OAuth provider discoverable from within a Pod

https://trello.com/c/7uYQSTdR

Signed-off-by: Monis Khan <mkhan@redhat.com>

enj authored on 2016/09/08 06:44:54
Showing 8 changed files
... ...
@@ -10,6 +10,23 @@
10 10
    "version": "latest"
11 11
   },
12 12
   "paths": {
13
+   "/.well-known/oauth-authorization-server/": {
14
+    "get": {
15
+     "description": "get the server's OAuth 2.0 Authorization Server Metadata",
16
+     "produces": [
17
+      "application/json"
18
+     ],
19
+     "schemes": [
20
+      "https"
21
+     ],
22
+     "operationId": "getOAuthAuthorizationServerMetadata",
23
+     "responses": {
24
+      "default": {
25
+       "description": "Default Response."
26
+      }
27
+     }
28
+    }
29
+   },
13 30
    "/api/": {
14 31
     "get": {
15 32
      "description": "get available API versions",
... ...
@@ -44079,9 +44096,6 @@
44079 44079
    "/version/openshift/": {
44080 44080
     "get": {
44081 44081
      "description": "get the code version",
44082
-     "consumes": [
44083
-      "application/json"
44084
-     ],
44085 44082
      "produces": [
44086 44083
       "application/json"
44087 44084
      ],
... ...
@@ -50,6 +50,7 @@ var DiscoveryRule = PolicyRule{
50 50
 		"/apis", "/apis/*",
51 51
 		"/oapi", "/oapi/*",
52 52
 		"/osapi", "/osapi/", // these cannot be removed until we can drop support for pre 3.1 clients
53
+		"/.well-known", "/.well-known/*",
53 54
 	),
54 55
 }
55 56
 
... ...
@@ -635,7 +635,7 @@ type OAuthConfig struct {
635 635
 	// MasterURL is used for making server-to-server calls to exchange authorization codes for access tokens
636 636
 	MasterURL string
637 637
 
638
-	// MasterPublicURL is used for building valid client redirect URLs for external access
638
+	// MasterPublicURL is used for building valid client redirect URLs for internal and external access
639 639
 	MasterPublicURL string
640 640
 
641 641
 	// AssetPublicURL is used for building valid client redirect URLs for external access
... ...
@@ -69,6 +69,7 @@ import (
69 69
 	"github.com/openshift/origin/pkg/image/registry/imagestreammapping"
70 70
 	"github.com/openshift/origin/pkg/image/registry/imagestreamtag"
71 71
 	oauthapi "github.com/openshift/origin/pkg/oauth/api"
72
+	"github.com/openshift/origin/pkg/oauth/discovery"
72 73
 	accesstokenetcd "github.com/openshift/origin/pkg/oauth/registry/oauthaccesstoken/etcd"
73 74
 	authorizetokenetcd "github.com/openshift/origin/pkg/oauth/registry/oauthauthorizetoken/etcd"
74 75
 	clientregistry "github.com/openshift/origin/pkg/oauth/registry/oauthclient"
... ...
@@ -134,6 +135,10 @@ const (
134 134
 	OpenShiftAPIV1           = "v1"
135 135
 	OpenShiftAPIPrefixV1     = OpenShiftAPIPrefix + "/" + OpenShiftAPIV1
136 136
 	swaggerAPIPrefix         = "/swaggerapi/"
137
+	// Discovery endpoint for OAuth 2.0 Authorization Server Metadata
138
+	// See IETF Draft:
139
+	// https://tools.ietf.org/html/draft-ietf-oauth-discovery-04#section-2
140
+	oauthMetadataEndpoint = "/.well-known/oauth-authorization-server"
137 141
 )
138 142
 
139 143
 var (
... ...
@@ -446,35 +451,68 @@ func (c *MasterConfig) InstallProtectedAPI(container *restful.Container) ([]stri
446 446
 	initReadinessCheckRoute(root, "/healthz/ready", c.ProjectAuthorizationCache.ReadyForAccess)
447 447
 	initVersionRoute(container, "/version/openshift")
448 448
 
449
+	// Set up OAuth metadata only if we are configured to use OAuth
450
+	if c.Options.OAuthConfig != nil {
451
+		initOAuthAuthorizationServerMetadataRoute(container, oauthMetadataEndpoint, c.Options.OAuthConfig.MasterPublicURL)
452
+	}
453
+
449 454
 	return messages, nil
450 455
 }
451 456
 
452
-// initReadinessCheckRoute initializes an HTTP endpoint for readiness checking
457
+// initVersionRoute initializes an HTTP endpoint for the server's version information.
453 458
 func initVersionRoute(container *restful.Container, path string) {
459
+	// Build version info once
460
+	versionInfo, err := json.MarshalIndent(version.Get(), "", "  ")
461
+	if err != nil {
462
+		glog.Errorf("Unable to initialize version route: %v", err)
463
+		return
464
+	}
465
+
454 466
 	// Set up a service to return the git code version.
455 467
 	versionWS := new(restful.WebService)
456 468
 	versionWS.Path(path)
457 469
 	versionWS.Doc("git code version from which this is built")
458 470
 	versionWS.Route(
459
-		versionWS.GET("/").To(handleVersion).
471
+		versionWS.GET("/").To(func(_ *restful.Request, resp *restful.Response) {
472
+			writeJSON(resp, versionInfo)
473
+		}).
460 474
 			Doc("get the code version").
461 475
 			Operation("getCodeVersion").
462
-			Produces(restful.MIME_JSON).
463
-			Consumes(restful.MIME_JSON))
476
+			Produces(restful.MIME_JSON))
464 477
 
465 478
 	container.Add(versionWS)
466 479
 }
467 480
 
468
-// handleVersion writes the server's version information.
469
-func handleVersion(req *restful.Request, resp *restful.Response) {
470
-	output, err := json.MarshalIndent(version.Get(), "", "  ")
481
+func writeJSON(resp *restful.Response, json []byte) {
482
+	resp.ResponseWriter.Header().Set("Content-Type", "application/json")
483
+	resp.ResponseWriter.WriteHeader(http.StatusOK)
484
+	resp.ResponseWriter.Write(json)
485
+}
486
+
487
+// initOAuthAuthorizationServerMetadataRoute initializes an HTTP endpoint for OAuth 2.0 Authorization Server Metadata discovery
488
+// https://tools.ietf.org/id/draft-ietf-oauth-discovery-04.html#rfc.section.2
489
+// masterPublicURL should be internally and externally routable to allow all users to discover this information
490
+func initOAuthAuthorizationServerMetadataRoute(container *restful.Container, path, masterPublicURL string) {
491
+	// Build OAuth metadata once
492
+	metadata, err := json.MarshalIndent(discovery.Get(masterPublicURL, OpenShiftOAuthAuthorizeURL(masterPublicURL), OpenShiftOAuthTokenURL(masterPublicURL)), "", "  ")
471 493
 	if err != nil {
472
-		http.Error(resp.ResponseWriter, err.Error(), http.StatusInternalServerError)
494
+		glog.Errorf("Unable to initialize OAuth authorization server metadata route: %v", err)
473 495
 		return
474 496
 	}
475
-	resp.ResponseWriter.Header().Set("Content-Type", "application/json")
476
-	resp.ResponseWriter.WriteHeader(http.StatusOK)
477
-	resp.ResponseWriter.Write(output)
497
+
498
+	// Set up a service to return the OAuth metadata.
499
+	oauthWS := new(restful.WebService)
500
+	oauthWS.Path(path)
501
+	oauthWS.Doc("OAuth 2.0 Authorization Server Metadata")
502
+	oauthWS.Route(
503
+		oauthWS.GET("/").To(func(_ *restful.Request, resp *restful.Response) {
504
+			writeJSON(resp, metadata)
505
+		}).
506
+			Doc("get the server's OAuth 2.0 Authorization Server Metadata").
507
+			Operation("getOAuthAuthorizationServerMetadata").
508
+			Produces(restful.MIME_JSON))
509
+
510
+	container.Add(oauthWS)
478 511
 }
479 512
 
480 513
 func (c *MasterConfig) GetRestStorage() map[string]rest.Storage {
... ...
@@ -18,6 +18,15 @@ import (
18 18
 
19 19
 const MinTokenLength = 32
20 20
 
21
+// PKCE [RFC7636] code challenge methods supported
22
+// https://tools.ietf.org/html/rfc7636#section-4.3
23
+const (
24
+	codeChallengeMethodPlain  = "plain"
25
+	codeChallengeMethodSHA256 = "S256"
26
+)
27
+
28
+var CodeChallengeMethodsSupported = []string{codeChallengeMethodPlain, codeChallengeMethodSHA256}
29
+
21 30
 func ValidateTokenName(name string, prefix bool) []string {
22 31
 	if reasons := oapi.MinimalNameRequirements(name, prefix); len(reasons) != 0 {
23 32
 		return reasons
... ...
@@ -101,10 +110,10 @@ func ValidateAuthorizeToken(authorizeToken *api.OAuthAuthorizeToken) field.Error
101 101
 		switch authorizeToken.CodeChallengeMethod {
102 102
 		case "":
103 103
 			allErrs = append(allErrs, field.Required(field.NewPath("codeChallengeMethod"), "required if codeChallenge is specified"))
104
-		case "plain", "S256":
104
+		case codeChallengeMethodPlain, codeChallengeMethodSHA256:
105 105
 			// no-op, good
106 106
 		default:
107
-			allErrs = append(allErrs, field.NotSupported(field.NewPath("codeChallengeMethod"), authorizeToken.CodeChallengeMethod, []string{"plain", "S256"}))
107
+			allErrs = append(allErrs, field.NotSupported(field.NewPath("codeChallengeMethod"), authorizeToken.CodeChallengeMethod, CodeChallengeMethodsSupported))
108 108
 		}
109 109
 	}
110 110
 
111 111
new file mode 100644
... ...
@@ -0,0 +1,58 @@
0
+package discovery
1
+
2
+import (
3
+	"github.com/RangelReale/osin"
4
+	"github.com/openshift/origin/pkg/authorization/authorizer/scope"
5
+	"github.com/openshift/origin/pkg/oauth/api/validation"
6
+	"github.com/openshift/origin/pkg/oauth/server/osinserver"
7
+)
8
+
9
+// OauthAuthorizationServerMetadata holds OAuth 2.0 Authorization Server Metadata used for discovery
10
+// https://tools.ietf.org/html/draft-ietf-oauth-discovery-04#section-2
11
+type OauthAuthorizationServerMetadata struct {
12
+	// The authorization server's issuer identifier, which is a URL that uses the https scheme and has no query or fragment components.
13
+	// This is the location where .well-known RFC 5785 [RFC5785] resources containing information about the authorization server are published.
14
+	Issuer string `json:"issuer"`
15
+
16
+	// URL of the authorization server's authorization endpoint [RFC6749].
17
+	AuthorizationEndpoint string `json:"authorization_endpoint"`
18
+
19
+	// URL of the authorization server's token endpoint [RFC6749].
20
+	TokenEndpoint string `json:"token_endpoint"`
21
+
22
+	// JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this authorization server supports.
23
+	// Servers MAY choose not to advertise some supported scope values even when this parameter is used.
24
+	ScopesSupported []string `json:"scopes_supported"`
25
+
26
+	// JSON array containing a list of the OAuth 2.0 response_type values that this authorization server supports.
27
+	// 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].
28
+	ResponseTypesSupported osin.AllowedAuthorizeType `json:"response_types_supported"`
29
+
30
+	// JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports.
31
+	// 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].
32
+	GrantTypesSupported osin.AllowedAccessType `json:"grant_types_supported"`
33
+
34
+	// JSON array containing a list of PKCE [RFC7636] code challenge methods supported by this authorization server.
35
+	// Code challenge method values are used in the "code_challenge_method" parameter defined in Section 4.3 of [RFC7636].
36
+	// The valid code challenge method values are those registered in the IANA "PKCE Code Challenge Methods" registry [IANA.OAuth.Parameters].
37
+	CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
38
+}
39
+
40
+func Get(masterPublicURL, authorizeURL, tokenURL string) OauthAuthorizationServerMetadata {
41
+	config := osinserver.NewDefaultServerConfig()
42
+	return OauthAuthorizationServerMetadata{
43
+		Issuer:                masterPublicURL,
44
+		AuthorizationEndpoint: authorizeURL,
45
+		TokenEndpoint:         tokenURL,
46
+		ScopesSupported: []string{ // Note: this list is incomplete, which is allowed per the draft spec
47
+			scope.UserFull,
48
+			scope.UserInfo,
49
+			scope.UserAccessCheck,
50
+			scope.UserListScopedProjects,
51
+			scope.UserListAllProjects,
52
+		},
53
+		ResponseTypesSupported:        config.AllowedAuthorizeTypes,
54
+		GrantTypesSupported:           osin.AllowedAccessType{osin.AUTHORIZATION_CODE, osin.AccessRequestType("implicit")}, // TODO use config.AllowedAccessTypes once our implementation handles other grant types
55
+		CodeChallengeMethodsSupported: validation.CodeChallengeMethodsSupported,
56
+	}
57
+}
0 58
new file mode 100644
... ...
@@ -0,0 +1,40 @@
0
+package discovery
1
+
2
+import (
3
+	"reflect"
4
+	"testing"
5
+
6
+	"github.com/RangelReale/osin"
7
+)
8
+
9
+func TestGet(t *testing.T) {
10
+	actual := Get("https://localhost:8443", "https://localhost:8443/oauth/authorize", "https://localhost:8443/oauth/token")
11
+	expected := OauthAuthorizationServerMetadata{
12
+		Issuer:                "https://localhost:8443",
13
+		AuthorizationEndpoint: "https://localhost:8443/oauth/authorize",
14
+		TokenEndpoint:         "https://localhost:8443/oauth/token",
15
+		ScopesSupported: []string{
16
+			"user:full",
17
+			"user:info",
18
+			"user:check-access",
19
+			"user:list-scoped-projects",
20
+			"user:list-projects",
21
+		},
22
+		ResponseTypesSupported: osin.AllowedAuthorizeType{
23
+			"code",
24
+			"token",
25
+		},
26
+		GrantTypesSupported: osin.AllowedAccessType{
27
+			"authorization_code",
28
+			"implicit",
29
+		},
30
+		CodeChallengeMethodsSupported: []string{
31
+			"plain",
32
+			"S256",
33
+		},
34
+	}
35
+
36
+	if !reflect.DeepEqual(actual, expected) {
37
+		t.Errorf("Expected %#v, got %#v", expected, actual)
38
+	}
39
+}
... ...
@@ -1497,6 +1497,8 @@ items:
1497 1497
   - apiGroups: null
1498 1498
     attributeRestrictions: null
1499 1499
     nonResourceURLs:
1500
+    - /.well-known
1501
+    - /.well-known/*
1500 1502
     - /api
1501 1503
     - /api/*
1502 1504
     - /apis
... ...
@@ -2114,6 +2116,8 @@ items:
2114 2114
   - apiGroups: null
2115 2115
     attributeRestrictions: null
2116 2116
     nonResourceURLs:
2117
+    - /.well-known
2118
+    - /.well-known/*
2117 2119
     - /api
2118 2120
     - /api/*
2119 2121
     - /apis