diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ca649e0..60b62b80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,9 @@ _The PKCE value is currently configurable from the UI only for the OIDC providers._ _This was added to accommodate OIDC providers that may throw an error if unsupported PKCE params are submitted with the auth request (eg. LinkedIn; see [#3799](https://github.com/pocketbase/pocketbase/discussions/3799#discussioncomment-7640312))._ +- Allow a single OAuth2 user to be used for authentication in multiple auth collection. + - ⚠️ Because now you can have more than one external provider with `collectionId-provider-providerId` pair, `Dao.FindExternalAuthByProvider(provider, providerId)` method was removed in favour of the more generic `Dao.FindFirstExternalAuthByExpr(expr)`. + ## v0.20.0-rc3 diff --git a/README.md b/README.md index 308230c3..bc41ad8f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ your own custom app specific business logic and still have a single portable exe ### Installation ```sh -# go 1.19+ +# go 1.21+ go get github.com/pocketbase/pocketbase ``` @@ -93,7 +93,7 @@ Enable CGO only if you really need to squeeze the read/write query performance a To build the minimal standalone executable, like the prebuilt ones in the releases page, you can simply run `go build` inside the `examples/base` directory: -0. [Install Go 1.19+](https://go.dev/doc/install) (_if you haven't already_) +0. [Install Go 1.21+](https://go.dev/doc/install) (_if you haven't already_) 1. Clone/download the repo 2. Navigate to `examples/base` 3. Run `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build` diff --git a/apis/serve.go b/apis/serve.go index 81fac6be..d0063d2c 100644 --- a/apis/serve.go +++ b/apis/serve.go @@ -52,11 +52,11 @@ type ServeConfig struct { // // Example: // -// app.Bootstrap() -// apis.Serve(app, apis.ServeConfig{ -// HttpAddr: "127.0.0.1:8080", -// ShowStartBanner: false, -// }) +// app.Bootstrap() +// apis.Serve(app, apis.ServeConfig{ +// HttpAddr: "127.0.0.1:8080", +// ShowStartBanner: false, +// }) func Serve(app core.App, config ServeConfig) (*http.Server, error) { if len(config.AllowedOrigins) == 0 { config.AllowedOrigins = []string{"*"} diff --git a/daos/external_auth.go b/daos/external_auth.go index 7e3708ba..a5cfa79e 100644 --- a/daos/external_auth.go +++ b/daos/external_auth.go @@ -32,27 +32,6 @@ func (dao *Dao) FindAllExternalAuthsByRecord(authRecord *models.Record) ([]*mode return auths, nil } -// FindExternalAuthByProvider returns the first available -// ExternalAuth model for the specified provider and providerId. -func (dao *Dao) FindExternalAuthByProvider(provider, providerId string) (*models.ExternalAuth, error) { - model := &models.ExternalAuth{} - - err := dao.ExternalAuthQuery(). - AndWhere(dbx.Not(dbx.HashExp{"providerId": ""})). // exclude empty providerIds - AndWhere(dbx.HashExp{ - "provider": provider, - "providerId": providerId, - }). - Limit(1). - One(model) - - if err != nil { - return nil, err - } - - return model, nil -} - // FindExternalAuthByRecordAndProvider returns the first available // ExternalAuth model for the specified record data and provider. func (dao *Dao) FindExternalAuthByRecordAndProvider(authRecord *models.Record, provider string) (*models.ExternalAuth, error) { @@ -74,6 +53,24 @@ func (dao *Dao) FindExternalAuthByRecordAndProvider(authRecord *models.Record, p return model, nil } +// FindFirstExternalAuthByExpr returns the first available +// ExternalAuth model that satisfies the non-nil expression. +func (dao *Dao) FindFirstExternalAuthByExpr(expr dbx.Expression) (*models.ExternalAuth, error) { + model := &models.ExternalAuth{} + + err := dao.ExternalAuthQuery(). + AndWhere(dbx.Not(dbx.HashExp{"providerId": ""})). // exclude empty providerIds + AndWhere(expr). + Limit(1). + One(model) + + if err != nil { + return nil, err + } + + return model, nil +} + // SaveExternalAuth upserts the provided ExternalAuth model. func (dao *Dao) SaveExternalAuth(model *models.ExternalAuth) error { // extra check the model data in case the provider's API response diff --git a/daos/external_auth_test.go b/daos/external_auth_test.go index f4d05c08..78701b98 100644 --- a/daos/external_auth_test.go +++ b/daos/external_auth_test.go @@ -3,6 +3,7 @@ package daos_test import ( "testing" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tests" ) @@ -56,25 +57,23 @@ func TestFindAllExternalAuthsByRecord(t *testing.T) { } } -func TestFindExternalAuthByProvider(t *testing.T) { +func TestFindFirstExternalAuthByExpr(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() scenarios := []struct { - provider string - providerId string + expr dbx.Expression expectedId string }{ - {"", "", ""}, - {"github", "", ""}, - {"github", "id1", ""}, - {"github", "id2", ""}, - {"google", "test123", "clmflokuq1xl341"}, - {"gitlab", "test123", "dlmflokuq1xl342"}, + {dbx.HashExp{"provider": "github", "providerId": ""}, ""}, + {dbx.HashExp{"provider": "github", "providerId": "id1"}, ""}, + {dbx.HashExp{"provider": "github", "providerId": "id2"}, ""}, + {dbx.HashExp{"provider": "google", "providerId": "test123"}, "clmflokuq1xl341"}, + {dbx.HashExp{"provider": "gitlab", "providerId": "test123"}, "dlmflokuq1xl342"}, } for i, s := range scenarios { - auth, err := app.Dao().FindExternalAuthByProvider(s.provider, s.providerId) + auth, err := app.Dao().FindFirstExternalAuthByExpr(s.expr) hasErr := err != nil expectErr := s.expectedId == "" @@ -147,7 +146,11 @@ func TestSaveExternalAuth(t *testing.T) { } // check if it was really saved - foundAuth, err := app.Dao().FindExternalAuthByProvider("test", "test_id") + foundAuth, err := app.Dao().FindFirstExternalAuthByExpr(dbx.HashExp{ + "collectionId": "v851q4r790rhknl", + "provider": "test", + "providerId": "test_id", + }) if err != nil { t.Fatal(err) } diff --git a/forms/record_oauth2_login.go b/forms/record_oauth2_login.go index 5fe8ad74..c85bcf6c 100644 --- a/forms/record_oauth2_login.go +++ b/forms/record_oauth2_login.go @@ -7,6 +7,7 @@ import ( "time" validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" @@ -162,7 +163,11 @@ func (form *RecordOAuth2Login) Submit( var authRecord *models.Record // check for existing relation with the auth record - rel, _ := form.dao.FindExternalAuthByProvider(form.Provider, authUser.Id) + rel, _ := form.dao.FindFirstExternalAuthByExpr(dbx.HashExp{ + "collectionId": form.collection.Id, + "provider": form.Provider, + "providerId": authUser.Id, + }) switch { case rel != nil: authRecord, err = form.dao.FindRecordById(form.collection.Id, rel.RecordId) diff --git a/migrations/1640988000_init.go b/migrations/1640988000_init.go index d7413b21..48d71a77 100644 --- a/migrations/1640988000_init.go +++ b/migrations/1640988000_init.go @@ -85,7 +85,7 @@ func init() { ); CREATE UNIQUE INDEX _externalAuths_record_provider_idx on {{_externalAuths}} ([[collectionId]], [[recordId]], [[provider]]); - CREATE UNIQUE INDEX _externalAuths_provider_providerId_idx on {{_externalAuths}} ([[provider]], [[providerId]]); + CREATE UNIQUE INDEX _externalAuths_collection_provider_idx on {{_externalAuths}} ([[collectionId]], [[provider]], [[providerId]]); `).Execute() if tablesErr != nil { return tablesErr diff --git a/migrations/1701496825_allow_single_oauth2_provider_in_multiple_auth_collections.go b/migrations/1701496825_allow_single_oauth2_provider_in_multiple_auth_collections.go new file mode 100644 index 00000000..55265f97 --- /dev/null +++ b/migrations/1701496825_allow_single_oauth2_provider_in_multiple_auth_collections.go @@ -0,0 +1,23 @@ +package migrations + +import ( + "github.com/pocketbase/dbx" +) + +// Fixes the unique _externalAuths constraint for old installations +// to allow a single OAuth2 provider to be registered for different auth collections. +func init() { + AppMigrations.Register(func(db dbx.Builder) error { + _, createErr := db.NewQuery("CREATE UNIQUE INDEX IF NOT EXISTS _externalAuths_collection_provider_idx on {{_externalAuths}} ([[collectionId]], [[provider]], [[providerId]])").Execute() + if createErr != nil { + return createErr + } + + _, dropErr := db.NewQuery("DROP INDEX IF EXISTS _externalAuths_provider_providerId_idx").Execute() + if dropErr != nil { + return dropErr + } + + return nil + }, nil) +}