Adding upstream version 0.28.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
88f1d47ab6
commit
e28c88ef14
933 changed files with 194711 additions and 0 deletions
543
core/collection_model_auth_options.go
Normal file
543
core/collection_model_auth_options.go
Normal file
|
@ -0,0 +1,543 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/pocketbase/pocketbase/tools/auth"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func (m *Collection) unsetMissingOAuth2MappedFields() {
|
||||
if !m.IsAuth() {
|
||||
return
|
||||
}
|
||||
|
||||
if m.OAuth2.MappedFields.Id != "" {
|
||||
if m.Fields.GetByName(m.OAuth2.MappedFields.Id) == nil {
|
||||
m.OAuth2.MappedFields.Id = ""
|
||||
}
|
||||
}
|
||||
|
||||
if m.OAuth2.MappedFields.Name != "" {
|
||||
if m.Fields.GetByName(m.OAuth2.MappedFields.Name) == nil {
|
||||
m.OAuth2.MappedFields.Name = ""
|
||||
}
|
||||
}
|
||||
|
||||
if m.OAuth2.MappedFields.Username != "" {
|
||||
if m.Fields.GetByName(m.OAuth2.MappedFields.Username) == nil {
|
||||
m.OAuth2.MappedFields.Username = ""
|
||||
}
|
||||
}
|
||||
|
||||
if m.OAuth2.MappedFields.AvatarURL != "" {
|
||||
if m.Fields.GetByName(m.OAuth2.MappedFields.AvatarURL) == nil {
|
||||
m.OAuth2.MappedFields.AvatarURL = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Collection) setDefaultAuthOptions() {
|
||||
m.collectionAuthOptions = collectionAuthOptions{
|
||||
VerificationTemplate: defaultVerificationTemplate,
|
||||
ResetPasswordTemplate: defaultResetPasswordTemplate,
|
||||
ConfirmEmailChangeTemplate: defaultConfirmEmailChangeTemplate,
|
||||
AuthRule: types.Pointer(""),
|
||||
AuthAlert: AuthAlertConfig{
|
||||
Enabled: true,
|
||||
EmailTemplate: defaultAuthAlertTemplate,
|
||||
},
|
||||
PasswordAuth: PasswordAuthConfig{
|
||||
Enabled: true,
|
||||
IdentityFields: []string{FieldNameEmail},
|
||||
},
|
||||
MFA: MFAConfig{
|
||||
Enabled: false,
|
||||
Duration: 1800, // 30min
|
||||
},
|
||||
OTP: OTPConfig{
|
||||
Enabled: false,
|
||||
Duration: 180, // 3min
|
||||
Length: 8,
|
||||
EmailTemplate: defaultOTPTemplate,
|
||||
},
|
||||
AuthToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 604800, // 7 days
|
||||
},
|
||||
PasswordResetToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 1800, // 30min
|
||||
},
|
||||
EmailChangeToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 1800, // 30min
|
||||
},
|
||||
VerificationToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 259200, // 3days
|
||||
},
|
||||
FileToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 180, // 3min
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var _ optionsValidator = (*collectionAuthOptions)(nil)
|
||||
|
||||
// collectionAuthOptions defines the options for the "auth" type collection.
|
||||
type collectionAuthOptions struct {
|
||||
// AuthRule could be used to specify additional record constraints
|
||||
// applied after record authentication and right before returning the
|
||||
// auth token response to the client.
|
||||
//
|
||||
// For example, to allow only verified users you could set it to
|
||||
// "verified = true".
|
||||
//
|
||||
// Set it to empty string to allow any Auth collection record to authenticate.
|
||||
//
|
||||
// Set it to nil to disallow authentication altogether for the collection
|
||||
// (that includes password, OAuth2, etc.).
|
||||
AuthRule *string `form:"authRule" json:"authRule"`
|
||||
|
||||
// ManageRule gives admin-like permissions to allow fully managing
|
||||
// the auth record(s), eg. changing the password without requiring
|
||||
// to enter the old one, directly updating the verified state and email, etc.
|
||||
//
|
||||
// This rule is executed in addition to the Create and Update API rules.
|
||||
ManageRule *string `form:"manageRule" json:"manageRule"`
|
||||
|
||||
// AuthAlert defines options related to the auth alerts on new device login.
|
||||
AuthAlert AuthAlertConfig `form:"authAlert" json:"authAlert"`
|
||||
|
||||
// OAuth2 specifies whether OAuth2 auth is enabled for the collection
|
||||
// and which OAuth2 providers are allowed.
|
||||
OAuth2 OAuth2Config `form:"oauth2" json:"oauth2"`
|
||||
|
||||
// PasswordAuth defines options related to the collection password authentication.
|
||||
PasswordAuth PasswordAuthConfig `form:"passwordAuth" json:"passwordAuth"`
|
||||
|
||||
// MFA defines options related to the Multi-factor authentication (MFA).
|
||||
MFA MFAConfig `form:"mfa" json:"mfa"`
|
||||
|
||||
// OTP defines options related to the One-time password authentication (OTP).
|
||||
OTP OTPConfig `form:"otp" json:"otp"`
|
||||
|
||||
// Various token configurations
|
||||
// ---
|
||||
AuthToken TokenConfig `form:"authToken" json:"authToken"`
|
||||
PasswordResetToken TokenConfig `form:"passwordResetToken" json:"passwordResetToken"`
|
||||
EmailChangeToken TokenConfig `form:"emailChangeToken" json:"emailChangeToken"`
|
||||
VerificationToken TokenConfig `form:"verificationToken" json:"verificationToken"`
|
||||
FileToken TokenConfig `form:"fileToken" json:"fileToken"`
|
||||
|
||||
// Default email templates
|
||||
// ---
|
||||
VerificationTemplate EmailTemplate `form:"verificationTemplate" json:"verificationTemplate"`
|
||||
ResetPasswordTemplate EmailTemplate `form:"resetPasswordTemplate" json:"resetPasswordTemplate"`
|
||||
ConfirmEmailChangeTemplate EmailTemplate `form:"confirmEmailChangeTemplate" json:"confirmEmailChangeTemplate"`
|
||||
}
|
||||
|
||||
func (o *collectionAuthOptions) validate(cv *collectionValidator) error {
|
||||
err := validation.ValidateStruct(o,
|
||||
validation.Field(
|
||||
&o.AuthRule,
|
||||
validation.By(cv.checkRule),
|
||||
validation.By(cv.ensureNoSystemRuleChange(cv.original.AuthRule)),
|
||||
),
|
||||
validation.Field(
|
||||
&o.ManageRule,
|
||||
validation.NilOrNotEmpty,
|
||||
validation.By(cv.checkRule),
|
||||
validation.By(cv.ensureNoSystemRuleChange(cv.original.ManageRule)),
|
||||
),
|
||||
validation.Field(&o.AuthAlert),
|
||||
validation.Field(&o.PasswordAuth),
|
||||
validation.Field(&o.OAuth2),
|
||||
validation.Field(&o.OTP),
|
||||
validation.Field(&o.MFA),
|
||||
validation.Field(&o.AuthToken),
|
||||
validation.Field(&o.PasswordResetToken),
|
||||
validation.Field(&o.EmailChangeToken),
|
||||
validation.Field(&o.VerificationToken),
|
||||
validation.Field(&o.FileToken),
|
||||
validation.Field(&o.VerificationTemplate, validation.Required),
|
||||
validation.Field(&o.ResetPasswordTemplate, validation.Required),
|
||||
validation.Field(&o.ConfirmEmailChangeTemplate, validation.Required),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.MFA.Enabled {
|
||||
// if MFA is enabled require at least 2 auth methods
|
||||
//
|
||||
// @todo maybe consider disabling the check because if custom auth methods
|
||||
// are registered it may fail since we don't have mechanism to detect them at the moment
|
||||
authsEnabled := 0
|
||||
if o.PasswordAuth.Enabled {
|
||||
authsEnabled++
|
||||
}
|
||||
if o.OAuth2.Enabled {
|
||||
authsEnabled++
|
||||
}
|
||||
if o.OTP.Enabled {
|
||||
authsEnabled++
|
||||
}
|
||||
if authsEnabled < 2 {
|
||||
return validation.Errors{
|
||||
"mfa": validation.Errors{
|
||||
"enabled": validation.NewError("validation_mfa_not_enough_auths", "MFA requires at least 2 auth methods to be enabled."),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if o.MFA.Rule != "" {
|
||||
mfaRuleValidators := []validation.RuleFunc{
|
||||
cv.checkRule,
|
||||
cv.ensureNoSystemRuleChange(&cv.original.MFA.Rule),
|
||||
}
|
||||
|
||||
for _, validator := range mfaRuleValidators {
|
||||
err := validator(&o.MFA.Rule)
|
||||
if err != nil {
|
||||
return validation.Errors{
|
||||
"mfa": validation.Errors{
|
||||
"rule": err,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extra check to ensure that only unique identity fields are used
|
||||
if o.PasswordAuth.Enabled {
|
||||
err = validation.Validate(o.PasswordAuth.IdentityFields, validation.By(cv.checkFieldsForUniqueIndex))
|
||||
if err != nil {
|
||||
return validation.Errors{
|
||||
"passwordAuth": validation.Errors{
|
||||
"identityFields": err,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type EmailTemplate struct {
|
||||
Subject string `form:"subject" json:"subject"`
|
||||
Body string `form:"body" json:"body"`
|
||||
}
|
||||
|
||||
// Validate makes EmailTemplate validatable by implementing [validation.Validatable] interface.
|
||||
func (t EmailTemplate) Validate() error {
|
||||
return validation.ValidateStruct(&t,
|
||||
validation.Field(&t.Subject, validation.Required),
|
||||
validation.Field(&t.Body, validation.Required),
|
||||
)
|
||||
}
|
||||
|
||||
// Resolve replaces the placeholder parameters in the current email
|
||||
// template and returns its components as ready-to-use strings.
|
||||
func (t EmailTemplate) Resolve(placeholders map[string]any) (subject, body string) {
|
||||
body = t.Body
|
||||
subject = t.Subject
|
||||
|
||||
for k, v := range placeholders {
|
||||
vStr := cast.ToString(v)
|
||||
|
||||
// replace subject placeholder params (if any)
|
||||
subject = strings.ReplaceAll(subject, k, vStr)
|
||||
|
||||
// replace body placeholder params (if any)
|
||||
body = strings.ReplaceAll(body, k, vStr)
|
||||
}
|
||||
|
||||
return subject, body
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type AuthAlertConfig struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
EmailTemplate EmailTemplate `form:"emailTemplate" json:"emailTemplate"`
|
||||
}
|
||||
|
||||
// Validate makes AuthAlertConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c AuthAlertConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
// note: for now always run the email template validations even
|
||||
// if not enabled since it could be used separately
|
||||
validation.Field(&c.EmailTemplate),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type TokenConfig struct {
|
||||
Secret string `form:"secret" json:"secret,omitempty"`
|
||||
|
||||
// Duration specifies how long an issued token to be valid (in seconds)
|
||||
Duration int64 `form:"duration" json:"duration"`
|
||||
}
|
||||
|
||||
// Validate makes TokenConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c TokenConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Secret, validation.Required, validation.Length(30, 255)),
|
||||
validation.Field(&c.Duration, validation.Required, validation.Min(10), validation.Max(94670856)), // ~3y max
|
||||
)
|
||||
}
|
||||
|
||||
// DurationTime returns the current Duration as [time.Duration].
|
||||
func (c TokenConfig) DurationTime() time.Duration {
|
||||
return time.Duration(c.Duration) * time.Second
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type OTPConfig struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
|
||||
// Duration specifies how long the OTP to be valid (in seconds)
|
||||
Duration int64 `form:"duration" json:"duration"`
|
||||
|
||||
// Length specifies the auto generated password length.
|
||||
Length int `form:"length" json:"length"`
|
||||
|
||||
// EmailTemplate is the default OTP email template that will be send to the auth record.
|
||||
//
|
||||
// In addition to the system placeholders you can also make use of
|
||||
// [core.EmailPlaceholderOTPId] and [core.EmailPlaceholderOTP].
|
||||
EmailTemplate EmailTemplate `form:"emailTemplate" json:"emailTemplate"`
|
||||
}
|
||||
|
||||
// Validate makes OTPConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c OTPConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Duration, validation.When(c.Enabled, validation.Required, validation.Min(10), validation.Max(86400))),
|
||||
validation.Field(&c.Length, validation.When(c.Enabled, validation.Required, validation.Min(4))),
|
||||
// note: for now always run the email template validations even
|
||||
// if not enabled since it could be used separately
|
||||
validation.Field(&c.EmailTemplate),
|
||||
)
|
||||
}
|
||||
|
||||
// DurationTime returns the current Duration as [time.Duration].
|
||||
func (c OTPConfig) DurationTime() time.Duration {
|
||||
return time.Duration(c.Duration) * time.Second
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type MFAConfig struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
|
||||
// Duration specifies how long an issued MFA to be valid (in seconds)
|
||||
Duration int64 `form:"duration" json:"duration"`
|
||||
|
||||
// Rule is an optional field to restrict MFA only for the records that satisfy the rule.
|
||||
//
|
||||
// Leave it empty to enable MFA for everyone.
|
||||
Rule string `form:"rule" json:"rule"`
|
||||
}
|
||||
|
||||
// Validate makes MFAConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c MFAConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Duration, validation.When(c.Enabled, validation.Required, validation.Min(10), validation.Max(86400))),
|
||||
)
|
||||
}
|
||||
|
||||
// DurationTime returns the current Duration as [time.Duration].
|
||||
func (c MFAConfig) DurationTime() time.Duration {
|
||||
return time.Duration(c.Duration) * time.Second
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type PasswordAuthConfig struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
|
||||
// IdentityFields is a list of field names that could be used as
|
||||
// identity during password authentication.
|
||||
//
|
||||
// Usually only fields that has single column UNIQUE index are accepted as values.
|
||||
IdentityFields []string `form:"identityFields" json:"identityFields"`
|
||||
}
|
||||
|
||||
// Validate makes PasswordAuthConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c PasswordAuthConfig) Validate() error {
|
||||
// strip duplicated values
|
||||
c.IdentityFields = list.ToUniqueStringSlice(c.IdentityFields)
|
||||
|
||||
if !c.Enabled {
|
||||
return nil // no need to validate
|
||||
}
|
||||
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.IdentityFields, validation.Required),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type OAuth2KnownFields struct {
|
||||
Id string `form:"id" json:"id"`
|
||||
Name string `form:"name" json:"name"`
|
||||
Username string `form:"username" json:"username"`
|
||||
AvatarURL string `form:"avatarURL" json:"avatarURL"`
|
||||
}
|
||||
|
||||
type OAuth2Config struct {
|
||||
Providers []OAuth2ProviderConfig `form:"providers" json:"providers"`
|
||||
|
||||
MappedFields OAuth2KnownFields `form:"mappedFields" json:"mappedFields"`
|
||||
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
}
|
||||
|
||||
// GetProviderConfig returns the first OAuth2ProviderConfig that matches the specified name.
|
||||
//
|
||||
// Returns false and zero config if no such provider is available in c.Providers.
|
||||
func (c OAuth2Config) GetProviderConfig(name string) (config OAuth2ProviderConfig, exists bool) {
|
||||
for _, p := range c.Providers {
|
||||
if p.Name == name {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Validate makes OAuth2Config validatable by implementing [validation.Validatable] interface.
|
||||
func (c OAuth2Config) Validate() error {
|
||||
if !c.Enabled {
|
||||
return nil // no need to validate
|
||||
}
|
||||
|
||||
return validation.ValidateStruct(&c,
|
||||
// note: don't require providers for now as they could be externally registered/removed
|
||||
validation.Field(&c.Providers, validation.By(checkForDuplicatedProviders)),
|
||||
)
|
||||
}
|
||||
|
||||
func checkForDuplicatedProviders(value any) error {
|
||||
configs, _ := value.([]OAuth2ProviderConfig)
|
||||
|
||||
existing := map[string]struct{}{}
|
||||
|
||||
for i, c := range configs {
|
||||
if c.Name == "" {
|
||||
continue // the name nonempty state is validated separately
|
||||
}
|
||||
if _, ok := existing[c.Name]; ok {
|
||||
return validation.Errors{
|
||||
strconv.Itoa(i): validation.Errors{
|
||||
"name": validation.NewError("validation_duplicated_provider", "The provider {{.name}} is already registered.").
|
||||
SetParams(map[string]any{"name": c.Name}),
|
||||
},
|
||||
}
|
||||
}
|
||||
existing[c.Name] = struct{}{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type OAuth2ProviderConfig struct {
|
||||
// PKCE overwrites the default provider PKCE config option.
|
||||
//
|
||||
// This usually shouldn't be needed but some OAuth2 vendors, like the LinkedIn OIDC,
|
||||
// may require manual adjustment due to returning error if extra parameters are added to the request
|
||||
// (https://github.com/pocketbase/pocketbase/discussions/3799#discussioncomment-7640312)
|
||||
PKCE *bool `form:"pkce" json:"pkce"`
|
||||
|
||||
Name string `form:"name" json:"name"`
|
||||
ClientId string `form:"clientId" json:"clientId"`
|
||||
ClientSecret string `form:"clientSecret" json:"clientSecret,omitempty"`
|
||||
AuthURL string `form:"authURL" json:"authURL"`
|
||||
TokenURL string `form:"tokenURL" json:"tokenURL"`
|
||||
UserInfoURL string `form:"userInfoURL" json:"userInfoURL"`
|
||||
DisplayName string `form:"displayName" json:"displayName"`
|
||||
Extra map[string]any `form:"extra" json:"extra"`
|
||||
}
|
||||
|
||||
// Validate makes OAuth2ProviderConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c OAuth2ProviderConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Name, validation.Required, validation.By(checkProviderName)),
|
||||
validation.Field(&c.ClientId, validation.Required),
|
||||
validation.Field(&c.ClientSecret, validation.Required),
|
||||
validation.Field(&c.AuthURL, is.URL),
|
||||
validation.Field(&c.TokenURL, is.URL),
|
||||
validation.Field(&c.UserInfoURL, is.URL),
|
||||
)
|
||||
}
|
||||
|
||||
func checkProviderName(value any) error {
|
||||
name, _ := value.(string)
|
||||
if name == "" {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
if _, err := auth.NewProviderByName(name); err != nil {
|
||||
return validation.NewError("validation_missing_provider", "Invalid or missing provider with name {{.name}}.").
|
||||
SetParams(map[string]any{"name": name})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitProvider returns a new auth.Provider instance loaded with the current OAuth2ProviderConfig options.
|
||||
func (c OAuth2ProviderConfig) InitProvider() (auth.Provider, error) {
|
||||
provider, err := auth.NewProviderByName(c.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.ClientId != "" {
|
||||
provider.SetClientId(c.ClientId)
|
||||
}
|
||||
|
||||
if c.ClientSecret != "" {
|
||||
provider.SetClientSecret(c.ClientSecret)
|
||||
}
|
||||
|
||||
if c.AuthURL != "" {
|
||||
provider.SetAuthURL(c.AuthURL)
|
||||
}
|
||||
|
||||
if c.UserInfoURL != "" {
|
||||
provider.SetUserInfoURL(c.UserInfoURL)
|
||||
}
|
||||
|
||||
if c.TokenURL != "" {
|
||||
provider.SetTokenURL(c.TokenURL)
|
||||
}
|
||||
|
||||
if c.DisplayName != "" {
|
||||
provider.SetDisplayName(c.DisplayName)
|
||||
}
|
||||
|
||||
if c.PKCE != nil {
|
||||
provider.SetPKCE(*c.PKCE)
|
||||
}
|
||||
|
||||
if c.Extra != nil {
|
||||
provider.SetExtra(c.Extra)
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue