[#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._
 | 
			
		||||
  _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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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