pocketbase/forms/user_oauth2_login.go

195 lines
5.4 KiB
Go
Raw Normal View History

2022-07-07 05:19:05 +08:00
package forms
import (
"errors"
"fmt"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/pocketbase/pocketbase/core"
2022-08-07 20:38:21 +08:00
"github.com/pocketbase/pocketbase/daos"
2022-07-07 05:19:05 +08:00
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/auth"
"github.com/pocketbase/pocketbase/tools/security"
"golang.org/x/oauth2"
)
2022-08-07 20:38:21 +08:00
// UserOauth2Login specifies a user Oauth2 login form.
2022-07-07 05:19:05 +08:00
type UserOauth2Login struct {
2022-08-07 20:38:21 +08:00
config UserOauth2LoginConfig
2022-07-07 05:19:05 +08:00
// The name of the OAuth2 client provider (eg. "google")
Provider string `form:"provider" json:"provider"`
// The authorization code returned from the initial request.
Code string `form:"code" json:"code"`
// The code verifier sent with the initial request as part of the code_challenge.
CodeVerifier string `form:"codeVerifier" json:"codeVerifier"`
// The redirect url sent with the initial request.
RedirectUrl string `form:"redirectUrl" json:"redirectUrl"`
}
2022-08-07 20:38:21 +08:00
// UserOauth2LoginConfig is the [UserOauth2Login] factory initializer config.
//
// NB! App is required struct member.
type UserOauth2LoginConfig struct {
App core.App
Dao *daos.Dao
2022-08-07 20:38:21 +08:00
}
// NewUserOauth2Login creates a new [UserOauth2Login] form with
// initializer config created from the provided [core.App] instance.
//
// If you want to submit the form as part of another transaction, use
// [NewUserOauth2LoginWithConfig] with explicitly set Dao.
2022-07-07 05:19:05 +08:00
func NewUserOauth2Login(app core.App) *UserOauth2Login {
2022-08-07 20:38:21 +08:00
return NewUserOauth2LoginWithConfig(UserOauth2LoginConfig{
App: app,
})
}
// NewUserOauth2LoginWithConfig creates a new [UserOauth2Login]
// form with the provided config or panics on invalid configuration.
func NewUserOauth2LoginWithConfig(config UserOauth2LoginConfig) *UserOauth2Login {
form := &UserOauth2Login{config: config}
if form.config.App == nil {
panic("Missing required config.App instance.")
}
if form.config.Dao == nil {
form.config.Dao = form.config.App.Dao()
2022-08-07 20:38:21 +08:00
}
return form
2022-07-07 05:19:05 +08:00
}
// Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *UserOauth2Login) Validate() error {
return validation.ValidateStruct(form,
validation.Field(&form.Provider, validation.Required, validation.By(form.checkProviderName)),
validation.Field(&form.Code, validation.Required),
validation.Field(&form.CodeVerifier, validation.Required),
validation.Field(&form.RedirectUrl, validation.Required, is.URL),
)
}
func (form *UserOauth2Login) checkProviderName(value any) error {
name, _ := value.(string)
2022-08-07 20:38:21 +08:00
config, ok := form.config.App.Settings().NamedAuthProviderConfigs()[name]
2022-07-07 05:19:05 +08:00
if !ok || !config.Enabled {
return validation.NewError("validation_invalid_provider", fmt.Sprintf("%q is missing or is not enabled.", name))
}
return nil
}
// Submit validates and submits the form.
// On success returns the authorized user model and the fetched provider's data.
func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) {
if err := form.Validate(); err != nil {
return nil, nil, err
}
provider, err := auth.NewProviderByName(form.Provider)
if err != nil {
return nil, nil, err
}
// load provider configuration
2022-08-07 20:38:21 +08:00
config := form.config.App.Settings().NamedAuthProviderConfigs()[form.Provider]
if err := config.SetupProvider(provider); err != nil {
return nil, nil, err
}
2022-07-07 05:19:05 +08:00
provider.SetRedirectUrl(form.RedirectUrl)
// fetch token
token, err := provider.FetchToken(
form.Code,
oauth2.SetAuthURLParam("code_verifier", form.CodeVerifier),
)
if err != nil {
return nil, nil, err
}
// fetch external auth user
2022-07-07 05:19:05 +08:00
authData, err := provider.FetchAuthUser(token)
if err != nil {
return nil, nil, err
}
var user *models.User
// check for existing relation with the external auth user
rel, _ := form.config.Dao.FindExternalAuthByProvider(form.Provider, authData.Id)
if rel != nil {
user, err = form.config.Dao.FindUserById(rel.UserId)
if err != nil {
return nil, authData, err
2022-07-07 05:19:05 +08:00
}
} else if authData.Email != "" {
// look for an existing user by the external user's email
user, _ = form.config.Dao.FindUserByEmail(authData.Email)
}
if user == nil && !config.AllowRegistrations {
return nil, authData, errors.New("New users registration is not allowed for the authorized provider.")
}
2022-07-07 05:19:05 +08:00
saveErr := form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error {
if user == nil {
user = &models.User{}
user.Verified = true
user.Email = authData.Email
user.SetPassword(security.RandomString(30))
2022-07-07 05:19:05 +08:00
// create the new user
if err := txDao.SaveUser(user); err != nil {
return err
}
} else {
// update the existing user verified state
if !user.Verified {
user.Verified = true
if err := txDao.SaveUser(user); err != nil {
return err
}
}
2022-07-07 05:19:05 +08:00
// update the existing user empty email if the authData has one
// (this in case previously the user was created with
// an OAuth2 provider that didn't return an email address)
if user.Email == "" && authData.Email != "" {
user.Email = authData.Email
if err := txDao.SaveUser(user); err != nil {
return err
}
}
}
// create ExternalAuth relation if missing
if rel == nil {
rel = &models.ExternalAuth{
UserId: user.Id,
Provider: form.Provider,
ProviderId: authData.Id,
}
if err := txDao.SaveExternalAuth(rel); err != nil {
return err
}
}
return nil
})
2022-07-07 05:19:05 +08:00
if saveErr != nil {
return nil, authData, saveErr
2022-07-07 05:19:05 +08:00
}
return user, authData, nil
}