From 45d353ffdbd1dbfed0ed92671c3d3471443c4a30 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Fri, 27 Mar 2026 23:56:17 +0200 Subject: [PATCH] fixed OAuth2 client secret reset when marshalizing a cached collection model --- CHANGELOG.md | 5 +++++ core/collection_model.go | 21 ++++++++++++------ core/collection_model_test.go | 40 +++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 621996b1..2c220ed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.36.8 + +- Fixed OAuth2 client secret reset when marshalizing a cached collection model. + + ## v0.36.7 - Fixed high memory usage with large file uploads ([#7572](https://github.com/pocketbase/pocketbase/discussions/7572)). diff --git a/core/collection_model.go b/core/collection_model.go index bf068243..aeb0551e 100644 --- a/core/collection_model.go +++ b/core/collection_model.go @@ -559,19 +559,26 @@ func (m Collection) MarshalJSON() ([]byte, error) { collectionAuthOptions }{m.baseCollection, m.collectionAuthOptions} - // ensure that it is always returned as array - if alias.OAuth2.Providers == nil { - alias.OAuth2.Providers = []OAuth2ProviderConfig{} - } - + // @todo to avoid the below changes consider omitting the field values from the individual structs json tags + // // hide secret keys from the serialization alias.AuthToken.Secret = "" alias.FileToken.Secret = "" alias.PasswordResetToken.Secret = "" alias.EmailChangeToken.Secret = "" alias.VerificationToken.Secret = "" - for i := range alias.OAuth2.Providers { - alias.OAuth2.Providers[i].ClientSecret = "" + + if alias.OAuth2.Providers == nil { + // ensure that it is always returned as array + alias.OAuth2.Providers = []OAuth2ProviderConfig{} + } else { + // create a deep copy of the slice to avoid modifying the cached model state + redactedProviders := make([]OAuth2ProviderConfig, len(alias.OAuth2.Providers)) + copy(redactedProviders, alias.OAuth2.Providers) + for i := range redactedProviders { + redactedProviders[i].ClientSecret = "" + } + alias.OAuth2.Providers = redactedProviders } return json.Marshal(alias) diff --git a/core/collection_model_test.go b/core/collection_model_test.go index ba0abf88..f8fca426 100644 --- a/core/collection_model_test.go +++ b/core/collection_model_test.go @@ -760,6 +760,46 @@ func TestCollectionSerialize(t *testing.T) { } } +func TestCollectionSerializeNotModifyingCache(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + c, err := app.FindCachedCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + _, err = json.Marshal(c) + if err != nil { + t.Fatal(err) + } + + redactedFields := map[string]string{ + "AuthToken.Secret": c.AuthToken.Secret, + "FileToken.Secret": c.FileToken.Secret, + "PasswordResetToken.Secret": c.PasswordResetToken.Secret, + "EmailChangeToken.Secret": c.EmailChangeToken.Secret, + "VerificationToken.Secret": c.VerificationToken.Secret, + } + + if len(c.OAuth2.Providers) == 0 { + t.Fatal("Expected at least one users OAuth2 provider, got 0") + } + for _, p := range c.OAuth2.Providers { + redactedFields[p.Name+".ClientSecret"] = p.ClientSecret + } + + for k, v := range redactedFields { + t.Run(k, func(t *testing.T) { + if v == "" { + t.Fatalf("Expected the redacted field %q to remain unmodified after serialization, got empty value", k) + } + }) + } +} + func TestCollectionDBExport(t *testing.T) { t.Parallel()