fix: ignore unsupported grant types in client ID metadata documents (#1682)

This commit is contained in:
Alessandro (Ale) Segala
2026-08-10 09:45:34 -07:00
committed by GitHub
parent 2f43ce41fe
commit 84a58cd757
2 changed files with 57 additions and 23 deletions
+32 -22
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"github.com/ory/fosite"
@@ -129,23 +130,9 @@ func (cimdPolicy) ValidateCIMDClient(_ context.Context, doc *fosite.ClientMetada
return fmt.Errorf("client metadata documents only support token_endpoint_auth_method %q, got %q", "none", doc.TokenEndpointAuthMethod)
}
// Restrict metadata clients to grant types implemented by Pocket ID and require a flow that can initiate authorization
grantTypes := doc.GrantTypes
if len(grantTypes) == 0 {
grantTypes = []string{string(fosite.GrantTypeAuthorizationCode)}
}
hasInitiatingGrant := false
for _, grantType := range grantTypes {
switch grantType {
case string(fosite.GrantTypeAuthorizationCode), string(fosite.GrantTypeDeviceCode):
hasInitiatingGrant = true
case string(fosite.GrantTypeRefreshToken):
default:
return fmt.Errorf("client metadata document contains unsupported grant_type %q", grantType)
}
}
if !hasInitiatingGrant {
return errors.New("client metadata document must enable authorization_code or device_code")
// Require a flow that can initiate authorization (ignoring the grant types Pocket ID does not implement)
if !hasInitiatingGrantType(supportedMetadataGrantTypes(doc.GrantTypes)) {
return errInitiatingGrantRequired
}
// Pocket ID only implements the code response type for metadata clients
@@ -162,6 +149,30 @@ func (cimdPolicy) ValidateCIMDClient(_ context.Context, doc *fosite.ClientMetada
return nil
}
var errInitiatingGrantRequired = errors.New("client metadata document must enable authorization_code or device_code")
// supportedMetadataGrantTypes keeps only the grant types Pocket ID implements for metadata clients and drops the rest
func supportedMetadataGrantTypes(grantTypes []string) []string {
if len(grantTypes) == 0 {
// Per RFC 7591 section 2: an omitted grant_types defaults to authorization_code
return []string{string(fosite.GrantTypeAuthorizationCode)}
}
supported := make([]string, 0, len(grantTypes))
for _, grantType := range grantTypes {
switch grantType {
case string(fosite.GrantTypeAuthorizationCode), string(fosite.GrantTypeDeviceCode), string(fosite.GrantTypeRefreshToken):
supported = append(supported, grantType)
}
}
return supported
}
func hasInitiatingGrantType(grantTypes []string) bool {
return slices.Contains(grantTypes, string(fosite.GrantTypeAuthorizationCode)) ||
slices.Contains(grantTypes, string(fosite.GrantTypeDeviceCode))
}
// validateMetadataRedirectURIs rejects self-asserted redirect URIs Pocket ID must not accept
func validateMetadataRedirectURIs(field string, uris []string) error {
for _, raw := range uris {
@@ -193,11 +204,10 @@ func buildClientFromMetadata(doc *fosite.ClientMetadataDocument, rawURL string)
return model.OidcClient{}, err
}
// Record what the document says the client restricts itself to, so it is not silently granted capabilities it never declared
// RFC 7591 section 2 defaults an omitted grant_types to authorization_code
grantTypes := doc.GrantTypes
if len(grantTypes) == 0 {
grantTypes = []string{"authorization_code"}
// Record the supported grant types the document declares, so the client is neither silently granted capabilities it never declared nor persisted with grants Pocket ID cannot honor
grantTypes := supportedMetadataGrantTypes(doc.GrantTypes)
if !hasInitiatingGrantType(grantTypes) {
return model.OidcClient{}, errInitiatingGrantRequired
}
client := model.OidcClient{
+25 -1
View File
@@ -52,6 +52,29 @@ func TestBuildClientFromMetadata(t *testing.T) {
}
})
t.Run("unsupported grant types are dropped", func(t *testing.T) {
doc := &fosite.ClientMetadataDocument{
ClientID: id,
RedirectURIs: []string{"https://app.example.com/callback"},
TokenEndpointAuthMethod: "none",
GrantTypes: []string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:jwt-bearer"},
}
c, err := buildClientFromMetadata(doc, id)
require.NoError(t, err)
assert.Equal(t, []string{"authorization_code", "refresh_token"}, []string(c.MetadataGrantTypes))
})
t.Run("documents without a supported initiating grant are rejected", func(t *testing.T) {
doc := &fosite.ClientMetadataDocument{
ClientID: id,
RedirectURIs: []string{"https://app.example.com/callback"},
TokenEndpointAuthMethod: "none",
GrantTypes: []string{"refresh_token", "urn:ietf:params:oauth:grant-type:jwt-bearer"},
}
_, err := buildClientFromMetadata(doc, id)
require.ErrorIs(t, err, errInitiatingGrantRequired)
})
t.Run("name falls back to the client ID host", func(t *testing.T) {
c, err := buildClientFromMetadata(&fosite.ClientMetadataDocument{ClientID: id, TokenEndpointAuthMethod: "none"}, id)
require.NoError(t, err)
@@ -71,7 +94,8 @@ func TestCIMDPolicyValidate(t *testing.T) {
{name: "defaults are supported"},
{name: "authorization code and refresh token are supported", grantTypes: []string{"authorization_code", "refresh_token"}},
{name: "device code is supported", grantTypes: []string{string(fosite.GrantTypeDeviceCode)}},
{name: "client credentials is rejected", grantTypes: []string{"client_credentials"}, wantError: "unsupported grant_type"},
{name: "unsupported grant types are ignored", grantTypes: []string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:jwt-bearer"}},
{name: "client credentials cannot initiate authorization", grantTypes: []string{"client_credentials"}, wantError: "must enable"},
{name: "refresh token cannot initiate authorization", grantTypes: []string{"refresh_token"}, wantError: "must enable"},
{name: "implicit response is rejected", grantTypes: []string{"authorization_code"}, responseTypes: []string{"token"}, wantError: "unsupported response_type"},
} {