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:
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…
x
Reference in New Issue
Block a user