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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
Generated
+37
@@ -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
|
||||
|
||||
Generated
+26
@@ -4287,6 +4287,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
|
||||
|
||||
Generated
+8
@@ -1482,6 +1482,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;
|
||||
|
||||
Reference in New Issue
Block a user