Files
coder/coderd/oauth2provider/metadata.go
T
Bobby Ho 6348c83142 feat: register public clients without a secret (#28046)
Third in the stack, split out of #27873. This is where dynamic client
registration first produces a public client.

- A registration requesting token_endpoint_auth_method `none` issues no
secret and persists the client_type derived in #28043. The response
omits client_secret entirely, per RFC 7591 §3.2.1.
- Discovery still advertises only the two secret-based methods. `none`
is withheld until the token endpoint honors it, so a conforming client
is not told to attempt an exchange that would be rejected.
- The app and its secret are written in one transaction. They were two
independent inserts, so a failed second one left a committed app that
could never authenticate while still holding a registration access
token. Pre-existing, but a public client's missing secret row makes the
orphaned case unspottable by inspection.
- registration_client_uri uses url.JoinPath, fixing a trailing-slash
access URL producing //oauth2/clients/{id}.
- Docs state both limits above, so the page does not describe a flow
that returns 400 until #28047 lands.

A client registered here cannot obtain a token yet; #28047 adds that.
Dynamic client registration is off by default, so none of this is
user-visible until then.

Refs
https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client
2026-08-20 12:49:47 -07:00

64 lines
2.9 KiB
Go

package oauth2provider
import (
"net/http"
"net/url"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
)
// GetAuthorizationServerMetadata returns an http.HandlerFunc that handles GET /.well-known/oauth-authorization-server
func GetAuthorizationServerMetadata(db database.Store, accessURL *url.URL) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// This is queried on every request rather than cached, for the
// same reason as the registration endpoint: discovery is not
// expected to be a hot path, and a flood of requests should be
// mitigated with rate limiting or firewalling, not a cache.
//nolint:gocritic // Public discovery endpoint, no authenticated actor to authorize against.
dcrEnabled, err := db.GetOAuth2DCREnabled(dbauthz.AsSystemOAuth2(ctx))
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
metadata := codersdk.OAuth2AuthorizationServerMetadata{
Issuer: accessURL.String(),
AuthorizationEndpoint: accessURL.JoinPath("/oauth2/authorize").String(),
TokenEndpoint: accessURL.JoinPath("/oauth2/tokens").String(),
RevocationEndpoint: accessURL.JoinPath("/oauth2/revoke").String(), // RFC 7009
ResponseTypesSupported: []codersdk.OAuth2ProviderResponseType{codersdk.OAuth2ProviderResponseTypeCode},
GrantTypesSupported: []codersdk.OAuth2ProviderGrantType{codersdk.OAuth2ProviderGrantTypeAuthorizationCode, codersdk.OAuth2ProviderGrantTypeRefreshToken},
CodeChallengeMethodsSupported: []codersdk.OAuth2PKCECodeChallengeMethod{codersdk.OAuth2PKCECodeChallengeMethodS256},
ScopesSupported: rbac.ExternalScopeNames(),
// Not gated on dcrEnabled: existing clients still need to
// exchange tokens when new registrations are turned off.
TokenEndpointAuthMethodsSupported: codersdk.AdvertisedOAuth2TokenEndpointAuthMethods(),
}
if dcrEnabled {
metadata.RegistrationEndpoint = accessURL.JoinPath("/oauth2/register").String() // RFC 7591
}
httpapi.Write(ctx, rw, http.StatusOK, metadata)
}
}
// GetProtectedResourceMetadata returns an http.HandlerFunc that handles GET /.well-known/oauth-protected-resource
func GetProtectedResourceMetadata(accessURL *url.URL) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
metadata := codersdk.OAuth2ProtectedResourceMetadata{
Resource: accessURL.String(),
AuthorizationServers: []string{accessURL.String()},
ScopesSupported: rbac.ExternalScopeNames(),
// RFC 6750 Bearer Token methods supported as fallback methods in api key middleware
BearerMethodsSupported: []string{"header", "query"},
}
httpapi.Write(ctx, rw, http.StatusOK, metadata)
}
}