1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-04-23 16:12:53 +02:00

[#3700] allow a single OAuth2 user to be used for authentication in multiple auth collection

This commit is contained in:
Gani Georgiev 2023-12-02 12:43:22 +02:00
parent b283ee2263
commit aaab643629
8 changed files with 72 additions and 41 deletions

View File

@ -43,6 +43,9 @@
_The PKCE value is currently configurable from the UI only for the OIDC providers._ _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))._ _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 ## v0.20.0-rc3

View File

@ -38,7 +38,7 @@ your own custom app specific business logic and still have a single portable exe
### Installation ### Installation
```sh ```sh
# go 1.19+ # go 1.21+
go get github.com/pocketbase/pocketbase 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: 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 1. Clone/download the repo
2. Navigate to `examples/base` 2. Navigate to `examples/base`
3. Run `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build` 3. Run `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build`

View File

@ -52,11 +52,11 @@ type ServeConfig struct {
// //
// Example: // Example:
// //
// app.Bootstrap() // app.Bootstrap()
// apis.Serve(app, apis.ServeConfig{ // apis.Serve(app, apis.ServeConfig{
// HttpAddr: "127.0.0.1:8080", // HttpAddr: "127.0.0.1:8080",
// ShowStartBanner: false, // ShowStartBanner: false,
// }) // })
func Serve(app core.App, config ServeConfig) (*http.Server, error) { func Serve(app core.App, config ServeConfig) (*http.Server, error) {
if len(config.AllowedOrigins) == 0 { if len(config.AllowedOrigins) == 0 {
config.AllowedOrigins = []string{"*"} config.AllowedOrigins = []string{"*"}

View File

@ -32,27 +32,6 @@ func (dao *Dao) FindAllExternalAuthsByRecord(authRecord *models.Record) ([]*mode
return auths, nil 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 // FindExternalAuthByRecordAndProvider returns the first available
// ExternalAuth model for the specified record data and provider. // ExternalAuth model for the specified record data and provider.
func (dao *Dao) FindExternalAuthByRecordAndProvider(authRecord *models.Record, provider string) (*models.ExternalAuth, error) { 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 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. // SaveExternalAuth upserts the provided ExternalAuth model.
func (dao *Dao) SaveExternalAuth(model *models.ExternalAuth) error { func (dao *Dao) SaveExternalAuth(model *models.ExternalAuth) error {
// extra check the model data in case the provider's API response // extra check the model data in case the provider's API response

View File

@ -3,6 +3,7 @@ package daos_test
import ( import (
"testing" "testing"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tests" "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() app, _ := tests.NewTestApp()
defer app.Cleanup() defer app.Cleanup()
scenarios := []struct { scenarios := []struct {
provider string expr dbx.Expression
providerId string
expectedId string expectedId string
}{ }{
{"", "", ""}, {dbx.HashExp{"provider": "github", "providerId": ""}, ""},
{"github", "", ""}, {dbx.HashExp{"provider": "github", "providerId": "id1"}, ""},
{"github", "id1", ""}, {dbx.HashExp{"provider": "github", "providerId": "id2"}, ""},
{"github", "id2", ""}, {dbx.HashExp{"provider": "google", "providerId": "test123"}, "clmflokuq1xl341"},
{"google", "test123", "clmflokuq1xl341"}, {dbx.HashExp{"provider": "gitlab", "providerId": "test123"}, "dlmflokuq1xl342"},
{"gitlab", "test123", "dlmflokuq1xl342"},
} }
for i, s := range scenarios { for i, s := range scenarios {
auth, err := app.Dao().FindExternalAuthByProvider(s.provider, s.providerId) auth, err := app.Dao().FindFirstExternalAuthByExpr(s.expr)
hasErr := err != nil hasErr := err != nil
expectErr := s.expectedId == "" expectErr := s.expectedId == ""
@ -147,7 +146,11 @@ func TestSaveExternalAuth(t *testing.T) {
} }
// check if it was really saved // 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -7,6 +7,7 @@ import (
"time" "time"
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
@ -162,7 +163,11 @@ func (form *RecordOAuth2Login) Submit(
var authRecord *models.Record var authRecord *models.Record
// check for existing relation with the auth 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 { switch {
case rel != nil: case rel != nil:
authRecord, err = form.dao.FindRecordById(form.collection.Id, rel.RecordId) authRecord, err = form.dao.FindRecordById(form.collection.Id, rel.RecordId)

View File

@ -85,7 +85,7 @@ func init() {
); );
CREATE UNIQUE INDEX _externalAuths_record_provider_idx on {{_externalAuths}} ([[collectionId]], [[recordId]], [[provider]]); 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() `).Execute()
if tablesErr != nil { if tablesErr != nil {
return tablesErr return tablesErr

View File

@ -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)
}