[#3700] allow a single OAuth2 user to be used for authentication in multiple auth collection
This commit is contained in:
		
							parent
							
								
									b283ee2263
								
							
						
					
					
						commit
						aaab643629
					
				| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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`
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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{"*"}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue