fix: ignore trailing slash in resources

This commit is contained in:
Elias Schneider
2026-08-17 08:34:28 +02:00
parent 07580d9f9f
commit 9421d43f8e
4 changed files with 34 additions and 1 deletions

View File

@@ -88,6 +88,9 @@ func (s *Service) Get(ctx context.Context, tx *gorm.DB, id string) (api API, err
}
func (s *Service) Create(ctx context.Context, input apiCreateDto) (api API, err error) {
// Store one canonical resource identifier so trailing-slash variants cannot create separate audiences
input.Resource = strings.TrimRight(input.Resource, "/")
// Reject the issuer as an audience so a custom API cannot impersonate Pocket ID's own identity tokens
if isIssuerAudience(input.Resource, s.issuer) {
return API{}, apperror.InvalidField("resource", "reserved", "is reserved by Pocket ID and cannot be used for a custom API")
@@ -443,6 +446,9 @@ func (s *Service) DescribePermissions(ctx context.Context, audience string, keys
return nil, nil
}
// Match scope information against the same canonical audience used during resource resolution
audience = strings.TrimRight(audience, "/")
var permissions []Permission
err := s.db.WithContext(ctx).
Model(&Permission{}).

View File

@@ -250,6 +250,19 @@ func TestCreateAcceptsAbsoluteResourceURIs(t *testing.T) {
}
}
func TestCreateTrimsResourceTrailingSlashes(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
svc := New(Dependencies{DB: db}).service
created, err := svc.Create(t.Context(), apiCreateDto{Name: "Orders", Resource: "https://api.orders.example.com///"})
require.NoError(t, err)
assert.Equal(t, "https://api.orders.example.com", created.Audience)
// A trailing-slash variant must conflict with the canonical resource instead of creating a second audience
_, err = svc.Create(t.Context(), apiCreateDto{Name: "Duplicate", Resource: "https://api.orders.example.com/"})
require.True(t, apperror.IsCode(err, apperror.CodeAlreadyInUse))
}
func TestDescribePermissions(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
svc := New(Dependencies{DB: db}).service
@@ -263,7 +276,7 @@ func TestDescribePermissions(t *testing.T) {
}})
require.NoError(t, err)
infos, err := svc.DescribePermissions(t.Context(), "https://api.orders.example.com", []string{"read:orders", "unknown"})
infos, err := svc.DescribePermissions(t.Context(), "https://api.orders.example.com/", []string{"read:orders", "unknown"})
require.NoError(t, err)
require.Len(t, infos, 1)
assert.Equal(t, "read:orders", infos[0].Key)

View File

@@ -3,6 +3,7 @@ package oidc
import (
"context"
"slices"
"strings"
"github.com/ory/fosite"
"github.com/pocket-id/pocket-id/backend/internal/dto"
@@ -53,6 +54,8 @@ func resolveResource(ctx context.Context, tx *gorm.DB, provider APIAccessProvide
if !fosite.IsValidResourceIndicatorURI(resource) || provider == nil {
return "", nil, fosite.ErrInvalidTarget.WithHintf("The requested resource '%s' is invalid, missing, unknown, or malformed.", resource)
}
// Resolve every trailing-slash variant against the same canonical resource and stamp that value into the token audience
resource = strings.TrimRight(resource, "/")
allowedScopes, apiExists, err := provider.AllowedScopesForAudience(ctx, tx, clientID, resource, subjectType)
if err != nil {

View File

@@ -95,6 +95,17 @@ func TestResolveResourceCustomAPIGrantsValidScopes(t *testing.T) {
assert.ElementsMatch(t, []string{"openid", "read:orders"}, granted)
}
func TestResolveResourceTrimsTrailingSlashes(t *testing.T) {
provider := userAccess(map[string][]string{
"https://api.orders.example.com": {"read:orders"},
})
audience, granted, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com///", []string{"read:orders"}, SubjectTypeUser)
require.NoError(t, err)
assert.Equal(t, "https://api.orders.example.com", audience)
assert.Equal(t, []string{"read:orders"}, granted)
}
func TestResolveResourceRejectsScopeFromAnotherAPI(t *testing.T) {
provider := userAccess(map[string][]string{
"https://api.orders.example.com": {"read:orders", "write:orders"},