From 05537c189494e0fdeb86317e4a07c992e276a8df Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 26 Sep 2025 12:15:36 +0200 Subject: [PATCH] feat: publish RBAC scopes in OAuth2 metadata endpoints (#19942) Publish supported OAuth2 scopes from RBAC external scope names This PR updates the OAuth2 metadata endpoints to publish the supported scopes from the RBAC external scope names. Previously, the `ScopesSupported` field was empty with a TODO to implement a scope system. Now, both the authorization server metadata and protected resource metadata endpoints return the list of scopes from `rbac.ExternalScopeNames()`. The tests have been updated to verify that the correct scopes are being returned in the metadata responses. --- coderd/oauth2_metadata_test.go | 8 +++++--- coderd/oauth2provider/metadata.go | 21 ++++++++++----------- coderd/oauth2provider/metadata_test.go | 8 +++++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/coderd/oauth2_metadata_test.go b/coderd/oauth2_metadata_test.go index 62eb63d1e1..a3e8ec1f50 100644 --- a/coderd/oauth2_metadata_test.go +++ b/coderd/oauth2_metadata_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -46,6 +47,8 @@ func TestOAuth2AuthorizationServerMetadata(t *testing.T) { require.Contains(t, metadata.GrantTypesSupported, "authorization_code") require.Contains(t, metadata.GrantTypesSupported, "refresh_token") require.Contains(t, metadata.CodeChallengeMethodsSupported, "S256") + // Supported scopes are published from the curated catalog + require.Equal(t, rbac.ExternalScopeNames(), metadata.ScopesSupported) } func TestOAuth2ProtectedResourceMetadata(t *testing.T) { @@ -80,7 +83,6 @@ func TestOAuth2ProtectedResourceMetadata(t *testing.T) { // RFC 6750 bearer tokens are now supported as fallback methods require.Contains(t, metadata.BearerMethodsSupported, "header") require.Contains(t, metadata.BearerMethodsSupported, "query") - // 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) + // Supported scopes are published from the curated catalog + require.Equal(t, rbac.ExternalScopeNames(), metadata.ScopesSupported) } diff --git a/coderd/oauth2provider/metadata.go b/coderd/oauth2provider/metadata.go index 9ce10f8993..a6edc4006b 100644 --- a/coderd/oauth2provider/metadata.go +++ b/coderd/oauth2provider/metadata.go @@ -5,6 +5,7 @@ import ( "net/url" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" ) @@ -13,15 +14,14 @@ func GetAuthorizationServerMetadata(accessURL *url.URL) http.HandlerFunc { return func(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() metadata := codersdk.OAuth2AuthorizationServerMetadata{ - Issuer: accessURL.String(), - AuthorizationEndpoint: accessURL.JoinPath("/oauth2/authorize").String(), - TokenEndpoint: accessURL.JoinPath("/oauth2/tokens").String(), - RegistrationEndpoint: accessURL.JoinPath("/oauth2/register").String(), // RFC 7591 - ResponseTypesSupported: []string{"code"}, - GrantTypesSupported: []string{"authorization_code", "refresh_token"}, - CodeChallengeMethodsSupported: []string{"S256"}, - // TODO: Implement scope system - ScopesSupported: []string{}, + Issuer: accessURL.String(), + AuthorizationEndpoint: accessURL.JoinPath("/oauth2/authorize").String(), + TokenEndpoint: accessURL.JoinPath("/oauth2/tokens").String(), + RegistrationEndpoint: accessURL.JoinPath("/oauth2/register").String(), // RFC 7591 + ResponseTypesSupported: []string{"code"}, + GrantTypesSupported: []string{"authorization_code", "refresh_token"}, + CodeChallengeMethodsSupported: []string{"S256"}, + ScopesSupported: rbac.ExternalScopeNames(), TokenEndpointAuthMethodsSupported: []string{"client_secret_post"}, } httpapi.Write(ctx, rw, http.StatusOK, metadata) @@ -35,8 +35,7 @@ func GetProtectedResourceMetadata(accessURL *url.URL) http.HandlerFunc { metadata := codersdk.OAuth2ProtectedResourceMetadata{ Resource: accessURL.String(), AuthorizationServers: []string{accessURL.String()}, - // TODO: Implement scope system based on RBAC permissions - ScopesSupported: []string{}, + ScopesSupported: rbac.ExternalScopeNames(), // RFC 6750 Bearer Token methods supported as fallback methods in api key middleware BearerMethodsSupported: []string{"header", "query"}, } diff --git a/coderd/oauth2provider/metadata_test.go b/coderd/oauth2provider/metadata_test.go index c51bb80984..006c341f75 100644 --- a/coderd/oauth2provider/metadata_test.go +++ b/coderd/oauth2provider/metadata_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -35,6 +36,8 @@ func TestOAuth2AuthorizationServerMetadata(t *testing.T) { require.Contains(t, metadata.GrantTypesSupported, "authorization_code") require.Contains(t, metadata.GrantTypesSupported, "refresh_token") require.Contains(t, metadata.CodeChallengeMethodsSupported, "S256") + // Supported scopes are published from the curated catalog + require.Equal(t, rbac.ExternalScopeNames(), metadata.ScopesSupported) } func TestOAuth2ProtectedResourceMetadata(t *testing.T) { @@ -60,7 +63,6 @@ func TestOAuth2ProtectedResourceMetadata(t *testing.T) { // RFC 6750 bearer tokens are now supported as fallback methods require.Contains(t, metadata.BearerMethodsSupported, "header") require.Contains(t, metadata.BearerMethodsSupported, "query") - // 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) + // Supported scopes are published from the curated catalog + require.Equal(t, rbac.ExternalScopeNames(), metadata.ScopesSupported) }