diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 610388e16f4f4..2d5481ff1f4db 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -65,6 +65,26 @@ const docTemplate = `{ } } }, + "/.well-known/oauth-protected-resource": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "OAuth2 protected resource metadata.", + "operationId": "oauth2-protected-resource-metadata", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OAuth2ProtectedResourceMetadata" + } + } + } + } + }, "/appearance": { "get": { "security": [ @@ -13381,6 +13401,32 @@ const docTemplate = `{ } } }, + "codersdk.OAuth2ProtectedResourceMetadata": { + "type": "object", + "properties": { + "authorization_servers": { + "type": "array", + "items": { + "type": "string" + } + }, + "bearer_methods_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "resource": { + "type": "string" + }, + "scopes_supported": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.OAuth2ProviderApp": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index cc7ea271ab2bd..6329cb1901c44 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -49,6 +49,22 @@ } } }, + "/.well-known/oauth-protected-resource": { + "get": { + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "OAuth2 protected resource metadata.", + "operationId": "oauth2-protected-resource-metadata", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.OAuth2ProtectedResourceMetadata" + } + } + } + } + }, "/appearance": { "get": { "security": [ @@ -12057,6 +12073,32 @@ } } }, + "codersdk.OAuth2ProtectedResourceMetadata": { + "type": "object", + "properties": { + "authorization_servers": { + "type": "array", + "items": { + "type": "string" + } + }, + "bearer_methods_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "resource": { + "type": "string" + }, + "scopes_supported": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "codersdk.OAuth2ProviderApp": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index e173bdfac4747..194671ed93fae 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -911,6 +911,8 @@ func New(options *Options) *API { // OAuth2 metadata endpoint for RFC 8414 discovery r.Get("/.well-known/oauth-authorization-server", api.oauth2AuthorizationServerMetadata) + // OAuth2 protected resource metadata endpoint for RFC 9728 discovery + r.Get("/.well-known/oauth-protected-resource", api.oauth2ProtectedResourceMetadata) // OAuth2 linking routes do not make sense under the /api/v2 path. These are // for an external application to use Coder as an OAuth2 provider, not for diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 71ee6ea10d08c..5c06abd0fb147 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -552,6 +552,8 @@ func APITokenFromRequest(r *http.Request) string { return headerValue } + // TODO(ThomasK33): Implement RFC 6750 + return "" } diff --git a/coderd/oauth2.go b/coderd/oauth2.go index 6ddfb7f5efbf9..cc0b84501de21 100644 --- a/coderd/oauth2.go +++ b/coderd/oauth2.go @@ -417,3 +417,23 @@ func (api *API) oauth2AuthorizationServerMetadata(rw http.ResponseWriter, r *htt } httpapi.Write(ctx, rw, http.StatusOK, metadata) } + +// @Summary OAuth2 protected resource metadata. +// @ID oauth2-protected-resource-metadata +// @Produce json +// @Tags Enterprise +// @Success 200 {object} codersdk.OAuth2ProtectedResourceMetadata +// @Router /.well-known/oauth-protected-resource [get] +func (api *API) oauth2ProtectedResourceMetadata(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + metadata := codersdk.OAuth2ProtectedResourceMetadata{ + Resource: api.AccessURL.String(), + AuthorizationServers: []string{api.AccessURL.String()}, + // TODO: Implement scope system based on RBAC permissions + ScopesSupported: []string{}, + // Note: Coder uses custom authentication methods, not RFC 6750 bearer tokens + // TODO(ThomasK33): Implement RFC 6750 + // BearerMethodsSupported: []string{}, // Omitted - no standard bearer token support + } + httpapi.Write(ctx, rw, http.StatusOK, metadata) +} diff --git a/coderd/oauth2_metadata_test.go b/coderd/oauth2_metadata_test.go index b07208d4c9d58..163430bc30ef9 100644 --- a/coderd/oauth2_metadata_test.go +++ b/coderd/oauth2_metadata_test.go @@ -41,3 +41,35 @@ func TestOAuth2AuthorizationServerMetadata(t *testing.T) { require.Contains(t, metadata.GrantTypesSupported, "refresh_token") require.Contains(t, metadata.CodeChallengeMethodsSupported, "S256") } + +func TestOAuth2ProtectedResourceMetadata(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Get the protected resource metadata + resp, err := client.Request(ctx, http.MethodGet, "/.well-known/oauth-protected-resource", nil) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + + var metadata codersdk.OAuth2ProtectedResourceMetadata + err = json.NewDecoder(resp.Body).Decode(&metadata) + require.NoError(t, err) + + // Verify the metadata + require.NotEmpty(t, metadata.Resource) + require.NotEmpty(t, metadata.AuthorizationServers) + require.Len(t, metadata.AuthorizationServers, 1) + require.Equal(t, metadata.Resource, metadata.AuthorizationServers[0]) + // BearerMethodsSupported is omitted since Coder uses custom authentication methods + // Standard RFC 6750 bearer tokens are not supported + require.True(t, len(metadata.BearerMethodsSupported) == 0) + // ScopesSupported can be empty until scope system is implemented + // Empty slice is marshaled as empty array, but can be nil when unmarshaled + require.True(t, len(metadata.ScopesSupported) == 0) +} diff --git a/codersdk/oauth2.go b/codersdk/oauth2.go index 84af80b211467..4c4407cbeaca1 100644 --- a/codersdk/oauth2.go +++ b/codersdk/oauth2.go @@ -244,3 +244,11 @@ type OAuth2AuthorizationServerMetadata struct { ScopesSupported []string `json:"scopes_supported,omitempty"` TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` } + +// OAuth2ProtectedResourceMetadata represents RFC 9728 OAuth 2.0 Protected Resource Metadata +type OAuth2ProtectedResourceMetadata struct { + Resource string `json:"resource"` + AuthorizationServers []string `json:"authorization_servers"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"` +} diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index cacdddfe37832..c885383a0fd35 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -46,6 +46,43 @@ curl -X GET http://coder-server:8080/api/v2/.well-known/oauth-authorization-serv |--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------------------------| | 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OAuth2AuthorizationServerMetadata](schemas.md#codersdkoauth2authorizationservermetadata) | +## OAuth2 protected resource metadata + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/.well-known/oauth-protected-resource \ + -H 'Accept: application/json' +``` + +`GET /.well-known/oauth-protected-resource` + +### Example responses + +> 200 Response + +```json +{ + "authorization_servers": [ + "string" + ], + "bearer_methods_supported": [ + "string" + ], + "resource": "string", + "scopes_supported": [ + "string" + ] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.OAuth2ProtectedResourceMetadata](schemas.md#codersdkoauth2protectedresourcemetadata) | + ## Get appearance ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 1e565e53a710c..081cfb8571af3 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4284,6 +4284,32 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `device_flow` | boolean | false | | | | `enterprise_base_url` | string | false | | | +## codersdk.OAuth2ProtectedResourceMetadata + +```json +{ + "authorization_servers": [ + "string" + ], + "bearer_methods_supported": [ + "string" + ], + "resource": "string", + "scopes_supported": [ + "string" + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|----------------------------|-----------------|----------|--------------|-------------| +| `authorization_servers` | array of string | false | | | +| `bearer_methods_supported` | array of string | false | | | +| `resource` | string | false | | | +| `scopes_supported` | array of string | false | | | + ## codersdk.OAuth2ProviderApp ```json diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 1cd1e80253651..4ff007a175a4d 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1481,6 +1481,14 @@ export interface OAuth2GithubConfig { readonly enterprise_base_url: string; } +// From codersdk/oauth2.go +export interface OAuth2ProtectedResourceMetadata { + readonly resource: string; + readonly authorization_servers: readonly string[]; + readonly scopes_supported?: readonly string[]; + readonly bearer_methods_supported?: readonly string[]; +} + // From codersdk/oauth2.go export interface OAuth2ProviderApp { readonly id: string;