https://trello.com/c/7uYQSTdR
Signed-off-by: Monis Khan <mkhan@redhat.com>
| ... | ... |
@@ -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 |
], |
| ... | ... |
@@ -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 |