mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add OAuth2 protected resource metadata endpoint for RFC 9728 (#18643)
# Add OAuth2 Protected Resource Metadata Endpoint This PR implements the OAuth2 Protected Resource Metadata endpoint according to RFC 9728. The endpoint is available at `/.well-known/oauth-protected-resource` and provides information about Coder as an OAuth2 protected resource. Key changes: - Added a new endpoint at `/.well-known/oauth-protected-resource` that returns metadata about Coder as an OAuth2 protected resource - Created a new `OAuth2ProtectedResourceMetadata` struct in the SDK - Added tests to verify the endpoint functionality - Updated API documentation to include the new endpoint The implementation currently returns basic metadata including the resource identifier and authorization server URL. The `scopes_supported` field is empty until a scope system based on RBAC permissions is implemented. The `bearer_methods_supported` field is omitted as Coder uses custom authentication methods rather than standard RFC 6750 bearer tokens. A TODO has been added to implement RFC 6750 bearer token support in the future.
This commit is contained in:
Generated
+46
@@ -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": [
|
||||
@@ -13450,6 +13470,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": {
|
||||
|
||||
Generated
+42
@@ -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": [
|
||||
@@ -12116,6 +12132,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": {
|
||||
|
||||
@@ -914,6 +914,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
|
||||
|
||||
@@ -671,6 +671,8 @@ func APITokenFromRequest(r *http.Request) string {
|
||||
return headerValue
|
||||
}
|
||||
|
||||
// TODO(ThomasK33): Implement RFC 6750
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -17,12 +18,17 @@ func TestOAuth2AuthorizationServerMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
serverURL := client.URL
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Get the metadata
|
||||
resp, err := client.Request(ctx, http.MethodGet, "/.well-known/oauth-authorization-server", nil)
|
||||
// Use a plain HTTP client since this endpoint doesn't require authentication
|
||||
endpoint := serverURL.ResolveReference(&url.URL{Path: "/.well-known/oauth-authorization-server"}).String()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -41,3 +47,40 @@ 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)
|
||||
serverURL := client.URL
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Use a plain HTTP client since this endpoint doesn't require authentication
|
||||
endpoint := serverURL.ResolveReference(&url.URL{Path: "/.well-known/oauth-protected-resource"}).String()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user