Skip to content

Commit 33bbf18

Browse files
authored
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.
1 parent 1b73b1a commit 33bbf18

File tree

10 files changed

+236
-2
lines changed

10 files changed

+236
-2
lines changed

coderd/apidoc/docs.go

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,8 @@ func New(options *Options) *API {
914914

915915
// OAuth2 metadata endpoint for RFC 8414 discovery
916916
r.Get("/.well-known/oauth-authorization-server", api.oauth2AuthorizationServerMetadata)
917+
// OAuth2 protected resource metadata endpoint for RFC 9728 discovery
918+
r.Get("/.well-known/oauth-protected-resource", api.oauth2ProtectedResourceMetadata)
917919

918920
// OAuth2 linking routes do not make sense under the /api/v2 path. These are
919921
// for an external application to use Coder as an OAuth2 provider, not for

coderd/httpmw/apikey.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,8 @@ func APITokenFromRequest(r *http.Request) string {
671671
return headerValue
672672
}
673673

674+
// TODO(ThomasK33): Implement RFC 6750
675+
674676
return ""
675677
}
676678

coderd/oauth2.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,3 +417,23 @@ func (api *API) oauth2AuthorizationServerMetadata(rw http.ResponseWriter, r *htt
417417
}
418418
httpapi.Write(ctx, rw, http.StatusOK, metadata)
419419
}
420+
421+
// @Summary OAuth2 protected resource metadata.
422+
// @ID oauth2-protected-resource-metadata
423+
// @Produce json
424+
// @Tags Enterprise
425+
// @Success 200 {object} codersdk.OAuth2ProtectedResourceMetadata
426+
// @Router /.well-known/oauth-protected-resource [get]
427+
func (api *API) oauth2ProtectedResourceMetadata(rw http.ResponseWriter, r *http.Request) {
428+
ctx := r.Context()
429+
metadata := codersdk.OAuth2ProtectedResourceMetadata{
430+
Resource: api.AccessURL.String(),
431+
AuthorizationServers: []string{api.AccessURL.String()},
432+
// TODO: Implement scope system based on RBAC permissions
433+
ScopesSupported: []string{},
434+
// Note: Coder uses custom authentication methods, not RFC 6750 bearer tokens
435+
// TODO(ThomasK33): Implement RFC 6750
436+
// BearerMethodsSupported: []string{}, // Omitted - no standard bearer token support
437+
}
438+
httpapi.Write(ctx, rw, http.StatusOK, metadata)
439+
}

coderd/oauth2_metadata_test.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"net/http"
7+
"net/url"
78
"testing"
89

910
"github.com/stretchr/testify/require"
@@ -17,12 +18,17 @@ func TestOAuth2AuthorizationServerMetadata(t *testing.T) {
1718
t.Parallel()
1819

1920
client := coderdtest.New(t, nil)
21+
serverURL := client.URL
2022

2123
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
2224
defer cancel()
2325

24-
// Get the metadata
25-
resp, err := client.Request(ctx, http.MethodGet, "/.well-known/oauth-authorization-server", nil)
26+
// Use a plain HTTP client since this endpoint doesn't require authentication
27+
endpoint := serverURL.ResolveReference(&url.URL{Path: "/.well-known/oauth-authorization-server"}).String()
28+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
29+
require.NoError(t, err)
30+
31+
resp, err := http.DefaultClient.Do(req)
2632
require.NoError(t, err)
2733
defer resp.Body.Close()
2834

@@ -41,3 +47,40 @@ func TestOAuth2AuthorizationServerMetadata(t *testing.T) {
4147
require.Contains(t, metadata.GrantTypesSupported, "refresh_token")
4248
require.Contains(t, metadata.CodeChallengeMethodsSupported, "S256")
4349
}
50+
51+
func TestOAuth2ProtectedResourceMetadata(t *testing.T) {
52+
t.Parallel()
53+
54+
client := coderdtest.New(t, nil)
55+
serverURL := client.URL
56+
57+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
58+
defer cancel()
59+
60+
// Use a plain HTTP client since this endpoint doesn't require authentication
61+
endpoint := serverURL.ResolveReference(&url.URL{Path: "/.well-known/oauth-protected-resource"}).String()
62+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
63+
require.NoError(t, err)
64+
65+
resp, err := http.DefaultClient.Do(req)
66+
require.NoError(t, err)
67+
defer resp.Body.Close()
68+
69+
require.Equal(t, http.StatusOK, resp.StatusCode)
70+
71+
var metadata codersdk.OAuth2ProtectedResourceMetadata
72+
err = json.NewDecoder(resp.Body).Decode(&metadata)
73+
require.NoError(t, err)
74+
75+
// Verify the metadata
76+
require.NotEmpty(t, metadata.Resource)
77+
require.NotEmpty(t, metadata.AuthorizationServers)
78+
require.Len(t, metadata.AuthorizationServers, 1)
79+
require.Equal(t, metadata.Resource, metadata.AuthorizationServers[0])
80+
// BearerMethodsSupported is omitted since Coder uses custom authentication methods
81+
// Standard RFC 6750 bearer tokens are not supported
82+
require.True(t, len(metadata.BearerMethodsSupported) == 0)
83+
// ScopesSupported can be empty until scope system is implemented
84+
// Empty slice is marshaled as empty array, but can be nil when unmarshaled
85+
require.True(t, len(metadata.ScopesSupported) == 0)
86+
}

codersdk/oauth2.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,11 @@ type OAuth2AuthorizationServerMetadata struct {
244244
ScopesSupported []string `json:"scopes_supported,omitempty"`
245245
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
246246
}
247+
248+
// OAuth2ProtectedResourceMetadata represents RFC 9728 OAuth 2.0 Protected Resource Metadata
249+
type OAuth2ProtectedResourceMetadata struct {
250+
Resource string `json:"resource"`
251+
AuthorizationServers []string `json:"authorization_servers"`
252+
ScopesSupported []string `json:"scopes_supported,omitempty"`
253+
BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"`
254+
}

docs/reference/api/enterprise.md

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/api/schemas.md

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/api/typesGenerated.ts

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)