Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
1733 lines
71 KiB
Go
1733 lines
71 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"context"
|
|
"crypto/fips140"
|
|
"crypto/sha512"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
"github.com/go-webauthn/webauthn/webauthn"
|
|
"github.com/gofrs/uuid"
|
|
"github.com/inhies/go-bytesize"
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/cors"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ory/herodot"
|
|
"github.com/ory/jsonschema/v3"
|
|
"github.com/ory/jsonschema/v3/httploader"
|
|
"github.com/ory/kratos/embedx"
|
|
"github.com/ory/kratos/request"
|
|
"github.com/ory/x/configx"
|
|
"github.com/ory/x/contextx"
|
|
"github.com/ory/x/corsx"
|
|
"github.com/ory/x/crdbx"
|
|
"github.com/ory/x/httpx"
|
|
"github.com/ory/x/jsonschemax"
|
|
"github.com/ory/x/logrusx"
|
|
"github.com/ory/x/otelx"
|
|
"github.com/ory/x/region"
|
|
"github.com/ory/x/watcherx"
|
|
)
|
|
|
|
const (
|
|
DefaultIdentityTraitsSchemaID = "default"
|
|
DefaultBrowserReturnURL = "default_browser_return_url"
|
|
DefaultSQLiteMemoryDSN = "sqlite://file::memory:?_fk=true"
|
|
DefaultPasswordHashingAlgorithm = "argon2"
|
|
DefaultCipherAlgorithm = "noop"
|
|
UnknownVersion = "unknown version"
|
|
ViperKeyDSN = "dsn"
|
|
ViperKeyCourierSMTPURL = "courier.smtp.connection_uri"
|
|
ViperKeyCourierSMTPClientCertPath = "courier.smtp.client_cert_path"
|
|
ViperKeyCourierSMTPClientKeyPath = "courier.smtp.client_key_path"
|
|
ViperKeyCourierTemplatesPath = "courier.template_override_path"
|
|
ViperKeyCourierTemplatesRecoveryInvalidEmail = "courier.templates.recovery.invalid.email"
|
|
ViperKeyCourierTemplatesRecoveryValidEmail = "courier.templates.recovery.valid.email"
|
|
ViperKeyCourierTemplatesRecoveryCodeInvalidEmail = "courier.templates.recovery_code.invalid.email"
|
|
ViperKeyCourierTemplatesRecoveryCodeValidEmail = "courier.templates.recovery_code.valid.email"
|
|
ViperKeyCourierTemplatesVerificationInvalidEmail = "courier.templates.verification.invalid.email"
|
|
ViperKeyCourierTemplatesVerificationValidEmail = "courier.templates.verification.valid.email"
|
|
ViperKeyCourierTemplatesVerificationCodeInvalidEmail = "courier.templates.verification_code.invalid.email"
|
|
ViperKeyCourierTemplatesVerificationCodeValidEmail = "courier.templates.verification_code.valid.email"
|
|
ViperKeyCourierTemplatesVerificationCodeValidSMS = "courier.templates.verification_code.valid.sms"
|
|
ViperKeyCourierTemplatesRecoveryCodeValidSMS = "courier.templates.recovery_code.valid.sms"
|
|
ViperKeyCourierTemplatesLoginCodeValidSMS = "courier.templates.login_code.valid.sms"
|
|
ViperKeyCourierTemplatesRegistrationCodeValidSMS = "courier.templates.registration_code.valid.sms"
|
|
ViperKeyCourierTemplatesVerifiableAddressChangedEmail = "courier.templates.verifiable_address_changed.email"
|
|
ViperKeyCourierTemplatesVerifiableAddressChangedSMS = "courier.templates.verifiable_address_changed.sms"
|
|
ViperKeyCourierTemplatesAuthenticatorKeyAddedEmail = "courier.templates.authenticator_key_added.email"
|
|
ViperKeyCourierTemplatesAuthenticatorKeyAddedSMS = "courier.templates.authenticator_key_added.sms"
|
|
ViperKeyCourierDeliveryStrategy = "courier.delivery_strategy"
|
|
ViperKeyCourierHTTPRequestConfig = "courier.http.request_config"
|
|
ViperKeyCourierTemplatesLoginCodeValidEmail = "courier.templates.login_code.valid.email"
|
|
ViperKeyCourierTemplatesRegistrationCodeValidEmail = "courier.templates.registration_code.valid.email"
|
|
ViperKeyCourierSMTP = "courier.smtp"
|
|
ViperKeyCourierSMTPFrom = "courier.smtp.from_address"
|
|
ViperKeyCourierSMTPFromName = "courier.smtp.from_name"
|
|
ViperKeyCourierSMTPHeaders = "courier.smtp.headers"
|
|
ViperKeyCourierSMTPLocalName = "courier.smtp.local_name"
|
|
ViperKeyCourierMessageRetries = "courier.message_retries"
|
|
ViperKeyCourierWorkerPullCount = "courier.worker.pull_count"
|
|
ViperKeyCourierWorkerPullWait = "courier.worker.pull_wait"
|
|
ViperKeyCourierChannels = "courier.channels"
|
|
ViperKeySecretsDefault = "secrets.default"
|
|
ViperKeySecretsCookie = "secrets.cookie"
|
|
ViperKeySecretsCipher = "secrets.cipher"
|
|
ViperKeySecretsPagination = "secrets.pagination"
|
|
ViperKeyPublicBaseURL = "serve.public.base_url"
|
|
ViperKeyAdminBaseURL = "serve.admin.base_url"
|
|
ViperKeySessionLifespan = "session.lifespan"
|
|
ViperKeySessionSameSite = "session.cookie.same_site"
|
|
ViperKeySessionSecure = "session.cookie.secure"
|
|
ViperKeySessionDomain = "session.cookie.domain"
|
|
ViperKeySessionName = "session.cookie.name"
|
|
ViperKeySessionPath = "session.cookie.path"
|
|
ViperKeySessionPersistentCookie = "session.cookie.persistent"
|
|
ViperKeySessionTokenizerTemplates = "session.whoami.tokenizer.templates"
|
|
ViperKeySessionWhoAmIAAL = "session.whoami.required_aal"
|
|
ViperKeySessionWhoAmICaching = "feature_flags.cacheable_sessions"
|
|
ViperKeyFeatureFlagFasterSessionExtend = "feature_flags.faster_session_extend"
|
|
ViperKeySessionWhoAmICachingMaxAge = "feature_flags.cacheable_sessions_max_age"
|
|
ViperKeyUseContinueWithTransitions = "feature_flags.use_continue_with_transitions"
|
|
ViperKeyChooseRecoveryAddress = "feature_flags.choose_recovery_address"
|
|
ViperKeyUseLegacyShowVerificationUI = "feature_flags.legacy_continue_with_verification_ui"
|
|
ViperKeyLegacyOIDCRegistrationGroup = "feature_flags.legacy_oidc_registration_node_group"
|
|
ViperKeyUseLegacyRequireVerifiedLoginError = "feature_flags.legacy_require_verified_login_error"
|
|
ViperKeyLegacyAllowInsecureOrigins = "feature_flags.legacy_allow_insecure_origins"
|
|
ViperKeyRefreshLoginChooseAddress = "feature_flags.refresh_login_choose_address"
|
|
ViperKeySessionRefreshMinTimeLeft = "session.earliest_possible_extend"
|
|
ViperKeyCookieSameSite = "cookies.same_site"
|
|
ViperKeyCookieDomain = "cookies.domain"
|
|
ViperKeyCookiePath = "cookies.path"
|
|
ViperKeyCookieSecure = "cookies.secure"
|
|
ViperKeySelfServiceStrategyConfig = "selfservice.methods"
|
|
ViperKeySelfServiceBrowserDefaultReturnTo = "selfservice." + DefaultBrowserReturnURL
|
|
ViperKeyURLsAllowedReturnToDomains = "selfservice.allowed_return_urls"
|
|
ViperKeySelfServiceRegistrationEnabled = "selfservice.flows.registration.enabled"
|
|
ViperKeySelfServiceRegistrationLoginHints = "selfservice.flows.registration.login_hints"
|
|
ViperKeySelfServiceRegistrationEnableLegacyOneStep = "selfservice.flows.registration.enable_legacy_one_step"
|
|
ViperKeySelfServiceRegistrationFlowStyle = "selfservice.flows.registration.style"
|
|
ViperKeySelfServiceRegistrationUI = "selfservice.flows.registration.ui_url"
|
|
ViperKeySelfServiceRegistrationRequestLifespan = "selfservice.flows.registration.lifespan"
|
|
ViperKeySelfServiceRegistrationAfter = "selfservice.flows.registration.after"
|
|
ViperKeySelfServiceRegistrationBeforeHooks = "selfservice.flows.registration.before.hooks"
|
|
ViperKeySelfServiceLoginUI = "selfservice.flows.login.ui_url"
|
|
ViperKeySelfServiceLoginFlowStyle = "selfservice.flows.login.style"
|
|
ViperKeySecurityAccountEnumerationMitigate = "security.account_enumeration.mitigate"
|
|
ViperKeySecurityDisallowRefInIdentitySchemas = "security.disallow_ref_in_identity_schemas"
|
|
ViperKeySelfServiceLoginRequestLifespan = "selfservice.flows.login.lifespan"
|
|
ViperKeySelfServiceLoginAfter = "selfservice.flows.login.after"
|
|
ViperKeySelfServiceLoginBeforeHooks = "selfservice.flows.login.before.hooks"
|
|
ViperKeySelfServiceErrorUI = "selfservice.flows.error.ui_url"
|
|
ViperKeySelfServiceLogoutBrowserDefaultReturnTo = "selfservice.flows.logout.after." + DefaultBrowserReturnURL
|
|
ViperKeySelfServiceLogoutClearBrowserData = "selfservice.flows.logout.clear_browser_data"
|
|
ViperKeySelfServiceSettingsURL = "selfservice.flows.settings.ui_url"
|
|
ViperKeySelfServiceSettingsAfter = "selfservice.flows.settings.after"
|
|
ViperKeySelfServiceSettingsBeforeHooks = "selfservice.flows.settings.before.hooks"
|
|
ViperKeySelfServiceSettingsRequestLifespan = "selfservice.flows.settings.lifespan"
|
|
ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter = "selfservice.flows.settings.privileged_session_max_age"
|
|
ViperKeySelfServiceSettingsRequiredAAL = "selfservice.flows.settings.required_aal"
|
|
ViperKeySelfServiceRecoveryAfter = "selfservice.flows.recovery.after"
|
|
ViperKeySelfServiceRecoveryBeforeHooks = "selfservice.flows.recovery.before.hooks"
|
|
ViperKeySelfServiceRecoveryEnabled = "selfservice.flows.recovery.enabled"
|
|
ViperKeySelfServiceRecoveryUse = "selfservice.flows.recovery.use"
|
|
ViperKeySelfServiceRecoveryUI = "selfservice.flows.recovery.ui_url"
|
|
ViperKeySelfServiceRecoveryRequestLifespan = "selfservice.flows.recovery.lifespan"
|
|
ViperKeySelfServiceRecoveryBrowserDefaultReturnTo = "selfservice.flows.recovery.after." + DefaultBrowserReturnURL
|
|
ViperKeySelfServiceRecoveryNotifyUnknownRecipients = "selfservice.flows.recovery.notify_unknown_recipients"
|
|
ViperKeySelfServiceVerificationEnabled = "selfservice.flows.verification.enabled"
|
|
ViperKeySelfServiceVerificationUI = "selfservice.flows.verification.ui_url"
|
|
ViperKeySelfServiceVerificationRequestLifespan = "selfservice.flows.verification.lifespan"
|
|
ViperKeySelfServiceVerificationBrowserDefaultReturnTo = "selfservice.flows.verification.after." + DefaultBrowserReturnURL
|
|
ViperKeySelfServiceVerificationAfter = "selfservice.flows.verification.after"
|
|
ViperKeySelfServiceVerificationBeforeHooks = "selfservice.flows.verification.before.hooks"
|
|
ViperKeySelfServiceVerificationUse = "selfservice.flows.verification.use"
|
|
ViperKeySelfServiceVerificationNotifyUnknownRecipients = "selfservice.flows.verification.notify_unknown_recipients"
|
|
ViperKeyDefaultIdentitySchemaID = "identity.default_schema_id"
|
|
ViperKeyIdentitySchemas = "identity.schemas"
|
|
ViperKeyHasherAlgorithm = "hashers.algorithm"
|
|
ViperKeyHasherArgon2ConfigMemory = "hashers.argon2.memory"
|
|
ViperKeyHasherArgon2ConfigIterations = "hashers.argon2.iterations"
|
|
ViperKeyHasherArgon2ConfigParallelism = "hashers.argon2.parallelism"
|
|
ViperKeyHasherArgon2ConfigSaltLength = "hashers.argon2.salt_length"
|
|
ViperKeyHasherArgon2ConfigKeyLength = "hashers.argon2.key_length"
|
|
ViperKeyHasherArgon2ConfigExpectedDuration = "hashers.argon2.expected_duration"
|
|
ViperKeyHasherArgon2ConfigExpectedDeviation = "hashers.argon2.expected_deviation"
|
|
ViperKeyHasherArgon2ConfigDedicatedMemory = "hashers.argon2.dedicated_memory"
|
|
ViperKeyHasherBcryptCost = "hashers.bcrypt.cost"
|
|
ViperKeyCipherAlgorithm = "ciphers.algorithm"
|
|
ViperKeyDatabaseCleanupSleepTables = "database.cleanup.sleep.tables"
|
|
ViperKeyDatabaseCleanupBatchSize = "database.cleanup.batch_size"
|
|
ViperKeyLinkLifespan = "selfservice.methods.link.config.lifespan"
|
|
ViperKeyCodeLifespan = "selfservice.methods.code.config.lifespan"
|
|
ViperKeyCodeMaxSubmissions = "selfservice.methods.code.config.max_submissions"
|
|
ViperKeyCodeConfigMissingCredentialFallbackEnabled = "selfservice.methods.code.config.missing_credential_fallback_enabled"
|
|
ViperKeyPasswordHaveIBeenPwnedHost = "selfservice.methods.password.config.haveibeenpwned_host"
|
|
ViperKeyPasswordHaveIBeenPwnedEnabled = "selfservice.methods.password.config.haveibeenpwned_enabled"
|
|
ViperKeyPasswordMaxBreaches = "selfservice.methods.password.config.max_breaches"
|
|
ViperKeyPasswordMinLength = "selfservice.methods.password.config.min_password_length"
|
|
ViperKeyPasswordIdentifierSimilarityCheckEnabled = "selfservice.methods.password.config.identifier_similarity_check_enabled"
|
|
ViperKeyIgnoreNetworkErrors = "selfservice.methods.password.config.ignore_network_errors"
|
|
ViperKeyPasswordRegistrationProfileGroup = "selfservice.methods.password.config.password_profile_registration_node_group"
|
|
ViperKeyTOTPIssuer = "selfservice.methods.totp.config.issuer"
|
|
ViperKeyOIDCBaseRedirectURL = "selfservice.methods.oidc.config.base_redirect_uri"
|
|
ViperKeySAMLBaseRedirectURL = "selfservice.methods.saml.config.base_redirect_uri"
|
|
ViperKeyWebAuthnRPDisplayName = "selfservice.methods.webauthn.config.rp.display_name"
|
|
ViperKeyWebAuthnRPID = "selfservice.methods.webauthn.config.rp.id"
|
|
ViperKeyWebAuthnRPOrigin = "selfservice.methods.webauthn.config.rp.origin"
|
|
ViperKeyWebAuthnRPOrigins = "selfservice.methods.webauthn.config.rp.origins"
|
|
ViperKeyWebAuthnPasswordless = "selfservice.methods.webauthn.config.passwordless"
|
|
ViperKeyPasskeyEnabled = "selfservice.methods.passkey.enabled"
|
|
ViperKeyPasskeyRPDisplayName = "selfservice.methods.passkey.config.rp.display_name"
|
|
ViperKeyPasskeyRPID = "selfservice.methods.passkey.config.rp.id"
|
|
ViperKeyPasskeyRPOrigins = "selfservice.methods.passkey.config.rp.origins"
|
|
ViperKeyPasskeyAuthenticatorAttachment = "selfservice.methods.passkey.config.authenticator_selection.attachment"
|
|
ViperKeyPasskeyResidentKey = "selfservice.methods.passkey.config.authenticator_selection.resident_key"
|
|
ViperKeyPasskeyUserVerification = "selfservice.methods.passkey.config.authenticator_selection.user_verification"
|
|
ViperKeyPasskeyAttestationPreference = "selfservice.methods.passkey.config.attestation.preference"
|
|
ViperKeyPasskeyRegistrationTimeout = "selfservice.methods.passkey.config.timeouts.registration"
|
|
ViperKeyPasskeyLoginTimeout = "selfservice.methods.passkey.config.timeouts.login"
|
|
ViperKeyOrganizations = "selfservice.methods.b2b.config.organizations"
|
|
ViperKeyOAuth2ProviderURL = "oauth2_provider.url"
|
|
ViperKeyOAuth2ProviderHeader = "oauth2_provider.headers"
|
|
ViperKeyOAuth2ProviderOverrideReturnTo = "oauth2_provider.override_return_to"
|
|
ViperKeyClientHTTPNoPrivateIPRanges = "clients.http.disallow_private_ip_ranges"
|
|
ViperKeyClientHTTPPrivateIPExceptionURLs = "clients.http.private_ip_exception_urls"
|
|
ViperKeyClientSMTPNoPrivateIPRanges = "clients.smtp.disallow_private_ip_ranges"
|
|
ViperKeyWebhookHeaderAllowlist = "clients.web_hook.header_allowlist"
|
|
ViperKeyPreviewDefaultReadConsistencyLevel = "preview.default_read_consistency_level"
|
|
ViperKeyVersion = "version"
|
|
ViperKeyPasswordMigrationHook = "selfservice.methods.password.config.migrate_hook"
|
|
)
|
|
|
|
const (
|
|
HighestAvailableAAL = "highest_available"
|
|
Argon2DefaultMemory = 128 * bytesize.MB
|
|
Argon2DefaultIterations uint32 = 1
|
|
Argon2DefaultSaltLength uint32 = 16
|
|
Argon2DefaultKeyLength uint32 = 32
|
|
Argon2DefaultDuration = 500 * time.Millisecond
|
|
Argon2DefaultDeviation = 500 * time.Millisecond
|
|
Argon2DefaultDedicatedMemory = 1 * bytesize.GB
|
|
BcryptDefaultCost uint32 = 12
|
|
)
|
|
|
|
// DefaultSessionCookieName returns the default cookie name for the kratos session.
|
|
const DefaultSessionCookieName = "ory_kratos_session"
|
|
|
|
type (
|
|
Argon2 struct {
|
|
Memory bytesize.ByteSize `json:"memory"`
|
|
Iterations uint32 `json:"iterations"`
|
|
Parallelism uint8 `json:"parallelism"`
|
|
SaltLength uint32 `json:"salt_length"`
|
|
KeyLength uint32 `json:"key_length"`
|
|
ExpectedDuration time.Duration `json:"expected_duration"`
|
|
ExpectedDeviation time.Duration `json:"expected_deviation"`
|
|
DedicatedMemory bytesize.ByteSize `json:"dedicated_memory"`
|
|
}
|
|
Bcrypt struct {
|
|
Cost uint32 `json:"cost"`
|
|
}
|
|
SelfServiceHook struct {
|
|
Name string `json:"hook"`
|
|
Config json.RawMessage `json:"config"`
|
|
}
|
|
SelfServiceStrategy struct {
|
|
Enabled bool `json:"enabled"`
|
|
Config json.RawMessage `json:"config"`
|
|
}
|
|
SelfServiceStrategyCode struct {
|
|
*SelfServiceStrategy
|
|
PasswordlessEnabled bool `json:"passwordless_enabled"`
|
|
MFAEnabled bool `json:"mfa_enabled"`
|
|
}
|
|
Schema struct {
|
|
ID string `json:"id" koanf:"id"`
|
|
URL string `json:"url" koanf:"url"`
|
|
SelfserviceSelectable bool `json:"selfservice_selectable" koanf:"selfservice_selectable"`
|
|
}
|
|
PasswordPolicy struct {
|
|
HaveIBeenPwnedHost string `json:"haveibeenpwned_host"`
|
|
HaveIBeenPwnedEnabled bool `json:"haveibeenpwned_enabled"`
|
|
MaxBreaches uint `json:"max_breaches"`
|
|
IgnoreNetworkErrors bool `json:"ignore_network_errors"`
|
|
MinPasswordLength uint `json:"min_password_length"`
|
|
IdentifierSimilarityCheckEnabled bool `json:"identifier_similarity_check_enabled"`
|
|
}
|
|
Schemas []Schema
|
|
CourierEmailBodyTemplate struct {
|
|
PlainText string `json:"plaintext"`
|
|
HTML string `json:"html"`
|
|
}
|
|
CourierEmailTemplate struct {
|
|
Body *CourierEmailBodyTemplate `json:"body"`
|
|
Subject string `json:"subject"`
|
|
}
|
|
CourierSMSTemplate struct {
|
|
Body *CourierSMSTemplateBody `json:"body"`
|
|
}
|
|
CourierSMSTemplateBody struct {
|
|
PlainText string `json:"plaintext"`
|
|
}
|
|
CourierChannel struct {
|
|
ID string `json:"id" koanf:"id"`
|
|
Type string `json:"type" koanf:"type"`
|
|
SMTPConfig *SMTPConfig `json:"smtp_config" koanf:"smtp_config"`
|
|
RequestConfig request.Config `json:"request_config" koanf:"request_config"`
|
|
}
|
|
SMTPConfig struct {
|
|
ConnectionURI string `json:"connection_uri" koanf:"connection_uri"`
|
|
ClientCertPath string `json:"client_cert_path" koanf:"client_cert_path"`
|
|
ClientKeyPath string `json:"client_key_path" koanf:"client_key_path"`
|
|
FromAddress string `json:"from_address" koanf:"from_address"`
|
|
FromName string `json:"from_name" koanf:"from_name"`
|
|
Headers map[string]string `json:"headers" koanf:"headers"`
|
|
LocalName string `json:"local_name" koanf:"local_name"`
|
|
}
|
|
PasswordMigrationHook struct {
|
|
Enabled bool `json:"enabled" koanf:"enabled"`
|
|
Config request.Config `json:"config" koanf:"config"`
|
|
}
|
|
Config struct {
|
|
l *logrusx.Logger
|
|
p *configx.Provider
|
|
c contextx.Contextualizer
|
|
identityMetaSchema *jsonschema.Schema
|
|
stdOutOrErr io.Writer
|
|
}
|
|
Provider interface {
|
|
Config() *Config
|
|
}
|
|
CourierConfigs interface {
|
|
CourierTemplatesRoot(ctx context.Context) string
|
|
CourierTemplatesVerificationInvalid(ctx context.Context) *CourierEmailTemplate
|
|
CourierTemplatesVerificationValid(ctx context.Context) *CourierEmailTemplate
|
|
CourierTemplatesRecoveryInvalid(ctx context.Context) *CourierEmailTemplate
|
|
CourierTemplatesRecoveryValid(ctx context.Context) *CourierEmailTemplate
|
|
CourierTemplatesRecoveryCodeInvalid(ctx context.Context) *CourierEmailTemplate
|
|
CourierTemplatesRecoveryCodeValid(ctx context.Context) *CourierEmailTemplate
|
|
CourierTemplatesVerificationCodeInvalid(ctx context.Context) *CourierEmailTemplate
|
|
CourierTemplatesVerificationCodeValid(ctx context.Context) *CourierEmailTemplate
|
|
CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate
|
|
CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate
|
|
CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *CourierSMSTemplate
|
|
CourierSMSTemplatesRecoveryCodeValid(ctx context.Context) *CourierSMSTemplate
|
|
CourierSMSTemplatesLoginCodeValid(ctx context.Context) *CourierSMSTemplate
|
|
CourierSMSTemplatesRegistrationCodeValid(ctx context.Context) *CourierSMSTemplate
|
|
CourierTemplatesVerifiableAddressChanged(ctx context.Context) *CourierEmailTemplate
|
|
CourierSMSTemplatesVerifiableAddressChanged(ctx context.Context) *CourierSMSTemplate
|
|
CourierTemplatesAuthenticatorKeyAdded(ctx context.Context) *CourierEmailTemplate
|
|
CourierSMSTemplatesAuthenticatorKeyAdded(ctx context.Context) *CourierSMSTemplate
|
|
CourierMessageRetries(ctx context.Context) int
|
|
CourierWorkerPullCount(ctx context.Context) int
|
|
CourierWorkerPullWait(ctx context.Context) time.Duration
|
|
CourierChannels(context.Context) ([]*CourierChannel, error)
|
|
ClientSMTPNoPrivateIPRanges(ctx context.Context) bool
|
|
}
|
|
)
|
|
|
|
func (c *Argon2) MarshalJSON() ([]byte, error) {
|
|
type encoded struct {
|
|
Memory string `json:"memory"`
|
|
Iterations uint32 `json:"iterations"`
|
|
Parallelism uint8 `json:"parallelism"`
|
|
SaltLength uint32 `json:"salt_length"`
|
|
KeyLength uint32 `json:"key_length"`
|
|
ExpectedDuration string `json:"minimal_duration"`
|
|
ExpectedDeviation string `json:"expected_deviation"`
|
|
DedicatedMemory string `json:"dedicated_memory"`
|
|
}
|
|
|
|
return json.Marshal(&encoded{
|
|
Memory: c.Memory.String(),
|
|
Iterations: c.Iterations,
|
|
Parallelism: c.Parallelism,
|
|
SaltLength: c.SaltLength,
|
|
KeyLength: c.KeyLength,
|
|
ExpectedDuration: c.ExpectedDuration.String(),
|
|
ExpectedDeviation: c.ExpectedDeviation.String(),
|
|
DedicatedMemory: c.DedicatedMemory.String(),
|
|
})
|
|
}
|
|
|
|
var Argon2DefaultParallelism = uint8(runtime.NumCPU() * 2)
|
|
|
|
const HookGlobal = "global"
|
|
|
|
func HookStrategyKey(key, strategy string) string {
|
|
if strategy == HookGlobal {
|
|
return fmt.Sprintf("%s.hooks", key)
|
|
} else {
|
|
return fmt.Sprintf("%s.%s.hooks", key, strategy)
|
|
}
|
|
}
|
|
|
|
func (s Schemas) FindSchemaByID(id string) (*Schema, error) {
|
|
for _, sc := range s {
|
|
if sc.ID == id {
|
|
return &sc, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.Errorf("unable to find identity schema with id: %s", id)
|
|
}
|
|
|
|
func MustNew(t testing.TB, l *logrusx.Logger, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) *Config {
|
|
p, err := New(t.Context(), l, os.Stderr, ctxer, opts...)
|
|
require.NoError(t, err)
|
|
return p
|
|
}
|
|
|
|
func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) (*Config, error) {
|
|
var c *Config
|
|
|
|
opts = append([]configx.OptionModifier{
|
|
configx.WithStderrValidationReporter(),
|
|
configx.OmitKeysFromTracing("dsn", "courier.smtp.connection_uri", "secrets.default", "secrets.cookie", "secrets.cipher", "client_secret"),
|
|
configx.WithImmutables("serve", "profiling", "log"),
|
|
configx.WithExceptImmutables("serve.public.cors.allowed_origins"),
|
|
configx.WithLogrusWatcher(l),
|
|
configx.WithLogger(l),
|
|
configx.WithContext(ctx),
|
|
configx.AttachWatcher(func(event watcherx.Event, err error) {
|
|
if c == nil {
|
|
panic(errors.New("the config provider did not initialise correctly in time"))
|
|
}
|
|
if err := c.validateIdentitySchemas(ctx); err != nil {
|
|
l.WithError(err).
|
|
Errorf("The changed identity schema configuration is invalid and could not be loaded. Rolling back to the last working configuration revision. Please address the validation errors before restarting the process.")
|
|
}
|
|
}),
|
|
}, opts...)
|
|
|
|
p, err := configx.New(ctx, embedx.ConfigSchema, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
l.UseConfig(p)
|
|
|
|
c = NewCustom(l, p, stdOutOrErr, ctxer)
|
|
|
|
if !p.SkipValidation() {
|
|
if err := c.validateIdentitySchemas(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if fips140.Enabled() {
|
|
if len(p.Strings(ViperKeySecretsPagination)) == 0 {
|
|
return nil, errors.New("you must provide `secrets.pagination` for FIPS compliance")
|
|
}
|
|
if len(p.Strings(ViperKeySecretsCipher)) == 0 {
|
|
return nil, errors.New("you must provide `secrets.cipher` for FIPS compliance")
|
|
}
|
|
if len(p.Strings(ViperKeySecretsCookie)) == 0 {
|
|
return nil, errors.New("you must provide `secrets.cookie` for FIPS compliance")
|
|
}
|
|
if len(p.Strings(ViperKeySecretsDefault)) == 0 {
|
|
return nil, errors.New("you must provide `secrets.default` for FIPS compliance")
|
|
}
|
|
}
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func NewCustom(l *logrusx.Logger, p *configx.Provider, stdOutOrErr io.Writer, ctxt contextx.Contextualizer) *Config {
|
|
l.UseConfig(p)
|
|
return &Config{l: l, p: p, c: ctxt, stdOutOrErr: stdOutOrErr}
|
|
}
|
|
|
|
func (p *Config) getIdentitySchemaValidator(ctx context.Context) (*jsonschema.Schema, error) {
|
|
if p.identityMetaSchema == nil {
|
|
c := jsonschema.NewCompiler()
|
|
err := embedx.AddSchemaResources(c, embedx.IdentityMeta)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.identityMetaSchema, err = c.Compile(ctx, embedx.IdentityMeta.GetSchemaID())
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
return p.identityMetaSchema, nil
|
|
}
|
|
|
|
type validateIdentitySchemasContextKey int
|
|
|
|
const validateIdentitySchemasClientKey validateIdentitySchemasContextKey = 1
|
|
|
|
func SetValidateIdentitySchemaResilientClientOptions(ctx context.Context, options []httpx.ResilientOptions) context.Context {
|
|
return context.WithValue(ctx, validateIdentitySchemasClientKey, options)
|
|
}
|
|
|
|
func (p *Config) validateIdentitySchemas(ctx context.Context) error {
|
|
opts := []httpx.ResilientOptions{
|
|
httpx.ResilientClientWithLogger(p.l),
|
|
httpx.ResilientClientWithMaxRetry(2),
|
|
httpx.ResilientClientWithConnectionTimeout(30 * time.Second),
|
|
}
|
|
|
|
if o, ok := ctx.Value(validateIdentitySchemasClientKey).([]httpx.ResilientOptions); ok {
|
|
opts = o
|
|
}
|
|
|
|
if p.ClientHTTPNoPrivateIPRanges(ctx) {
|
|
opts = append(opts, httpx.ResilientClientDisallowInternalIPs())
|
|
}
|
|
|
|
ctx = context.WithValue(ctx, httploader.ContextKey, httpx.NewResilientClient(opts...))
|
|
|
|
j, err := p.getIdentitySchemaValidator(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ss, err := p.IdentityTraitsSchemas(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, s := range ss {
|
|
resource, err := jsonschema.LoadURL(ctx, s.URL)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
defer func() { _ = resource.Close() }()
|
|
|
|
schema, err := io.ReadAll(io.LimitReader(resource, 1024*1024))
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
if err = j.Validate(bytes.NewBuffer(schema)); err != nil {
|
|
p.formatJsonErrors(schema, err)
|
|
return errors.WithStack(err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Config) formatJsonErrors(schema []byte, err error) {
|
|
_, _ = fmt.Fprintln(p.stdOutOrErr, "")
|
|
jsonschemax.FormatValidationErrorForCLI(p.stdOutOrErr, schema, err)
|
|
}
|
|
|
|
func (p *Config) ServePublic(ctx context.Context) *configx.Serve {
|
|
return p.GetProvider(ctx).Serve("serve.public", p.IsInsecureDevMode(ctx), configx.Serve{
|
|
Port: 4433,
|
|
})
|
|
}
|
|
|
|
func (p *Config) ServeAdmin(ctx context.Context) *configx.Serve {
|
|
return p.GetProvider(ctx).Serve("serve.admin", p.IsInsecureDevMode(ctx), configx.Serve{
|
|
Port: 4434,
|
|
})
|
|
}
|
|
|
|
func (p *Config) CORSPublic(ctx context.Context) (cors.Options, bool) {
|
|
return p.GetProvider(ctx).CORS("serve.public", cors.Options{
|
|
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
|
|
AllowedHeaders: []string{"Authorization", "Content-Type", "Cookie"},
|
|
ExposedHeaders: []string{"Content-Type", "Set-Cookie"},
|
|
AllowCredentials: true,
|
|
})
|
|
}
|
|
|
|
// Deprecated: use context-based [contextx.WithConfigValue] instead.
|
|
func (p *Config) Set(_ context.Context, key string, value interface{}) error {
|
|
return p.p.Set(key, value)
|
|
}
|
|
|
|
// Deprecated: use context-based [contextx.WithConfigValue] instead.
|
|
func (p *Config) MustSet(_ context.Context, key string, value interface{}) {
|
|
if err := p.p.Set(key, value); err != nil {
|
|
p.l.WithError(err).Fatalf("Unable to set %q to %q.", key, value)
|
|
}
|
|
}
|
|
|
|
func (p *Config) SessionName(ctx context.Context) string {
|
|
return cmp.Or(p.GetProvider(ctx).String(ViperKeySessionName), DefaultSessionCookieName)
|
|
}
|
|
|
|
func (p *Config) HasherArgon2(ctx context.Context) *Argon2 {
|
|
// warn about usage of default values and point to the docs
|
|
// warning will require https://github.com/ory/viper/issues/19
|
|
return &Argon2{
|
|
Memory: p.GetProvider(ctx).ByteSizeF(ViperKeyHasherArgon2ConfigMemory, Argon2DefaultMemory),
|
|
//nolint:gosec // disable G115
|
|
Iterations: uint32(p.GetProvider(ctx).IntF(ViperKeyHasherArgon2ConfigIterations, int(Argon2DefaultIterations))),
|
|
//nolint:gosec // disable G115
|
|
Parallelism: uint8(p.GetProvider(ctx).IntF(ViperKeyHasherArgon2ConfigParallelism, int(Argon2DefaultParallelism))),
|
|
//nolint:gosec // disable G115
|
|
SaltLength: uint32(p.GetProvider(ctx).IntF(ViperKeyHasherArgon2ConfigSaltLength, int(Argon2DefaultSaltLength))),
|
|
//nolint:gosec // disable G115
|
|
KeyLength: uint32(p.GetProvider(ctx).IntF(ViperKeyHasherArgon2ConfigKeyLength, int(Argon2DefaultKeyLength))),
|
|
ExpectedDuration: p.GetProvider(ctx).DurationF(ViperKeyHasherArgon2ConfigExpectedDuration, Argon2DefaultDuration),
|
|
ExpectedDeviation: p.GetProvider(ctx).DurationF(ViperKeyHasherArgon2ConfigExpectedDeviation, Argon2DefaultDeviation),
|
|
DedicatedMemory: p.GetProvider(ctx).ByteSizeF(ViperKeyHasherArgon2ConfigDedicatedMemory, Argon2DefaultDedicatedMemory),
|
|
}
|
|
}
|
|
|
|
func (p *Config) HasherBcrypt(ctx context.Context) *Bcrypt {
|
|
cost := uint32(p.GetProvider(ctx).IntF(ViperKeyHasherBcryptCost, int(BcryptDefaultCost))) // #nosec G115 -- if the user configures a cost > MaxUint32, go falls back to MaxUint32
|
|
if !p.IsInsecureDevMode(ctx) && cost < BcryptDefaultCost {
|
|
cost = BcryptDefaultCost
|
|
}
|
|
|
|
return &Bcrypt{Cost: cost}
|
|
}
|
|
|
|
func (p *Config) DefaultIdentityTraitsSchemaURL(ctx context.Context) (*url.URL, error) {
|
|
ss, err := p.IdentityTraitsSchemas(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
search := p.GetProvider(ctx).String(ViperKeyDefaultIdentitySchemaID)
|
|
found, err := ss.FindSchemaByID(search)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p.ParseURI(found.URL)
|
|
}
|
|
|
|
func (p *Config) DefaultIdentityTraitsSchemaID(ctx context.Context) string {
|
|
return p.GetProvider(ctx).String(ViperKeyDefaultIdentitySchemaID)
|
|
}
|
|
|
|
func (p *Config) IdentityTraitsSchemaURL(ctx context.Context, schemaID string) (*url.URL, error) {
|
|
ss, err := p.IdentityTraitsSchemas(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
found, err := ss.FindSchemaByID(schemaID)
|
|
if err != nil {
|
|
// default to default schema
|
|
search := p.GetProvider(ctx).String(ViperKeyDefaultIdentitySchemaID)
|
|
found, err = ss.FindSchemaByID(search)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return p.ParseURI(found.URL)
|
|
}
|
|
|
|
func (p *Config) TOTPIssuer(ctx context.Context) string {
|
|
return p.GetProvider(ctx).StringF(ViperKeyTOTPIssuer, p.SelfPublicURL(ctx).Hostname())
|
|
}
|
|
|
|
func (p *Config) OIDCRedirectURIBase(ctx context.Context) *url.URL {
|
|
return p.GetProvider(ctx).URIF(ViperKeyOIDCBaseRedirectURL, p.SelfPublicURL(ctx))
|
|
}
|
|
|
|
func (p *Config) SAMLRedirectURIBase(ctx context.Context) *url.URL {
|
|
return p.GetProvider(ctx).URIF(ViperKeySAMLBaseRedirectURL, p.SelfPublicURL(ctx))
|
|
}
|
|
|
|
func (p *Config) IdentityTraitsSchemas(ctx context.Context) (ss Schemas, err error) {
|
|
if err = p.GetProvider(ctx).Unmarshal(ViperKeyIdentitySchemas, &ss); err != nil {
|
|
return ss, nil
|
|
}
|
|
|
|
return ss, nil
|
|
}
|
|
|
|
func (p *Config) DSN(ctx context.Context) string {
|
|
pp := p.GetProvider(ctx)
|
|
dsn := pp.String(ViperKeyDSN)
|
|
|
|
if dsn == "memory" {
|
|
return DefaultSQLiteMemoryDSN
|
|
}
|
|
|
|
if len(dsn) > 0 {
|
|
return dsn
|
|
}
|
|
|
|
// Print a stack trace to aid debugging.
|
|
p.l.Fatalf("%+v", errors.Errorf("dsn must be set"))
|
|
return ""
|
|
}
|
|
|
|
func (p *Config) DisableAPIFlowEnforcement(ctx context.Context) bool {
|
|
if p.IsInsecureDevMode(ctx) && os.Getenv("DEV_DISABLE_API_FLOW_ENFORCEMENT") == "true" {
|
|
p.l.Warn("Because \"DEV_DISABLE_API_FLOW_ENFORCEMENT=true\" and the \"--dev\" flag are set, self-service API flows will no longer check if the interaction is actually a browser flow. This is very dangerous as it allows bypassing of anti-CSRF measures, leaving the deployment highly vulnerable. This option should only be used for automated testing and never come close to real user data anywhere.")
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *Config) ClientHTTPNoPrivateIPRanges(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).BoolF(ViperKeyClientHTTPNoPrivateIPRanges, false)
|
|
}
|
|
|
|
func (p *Config) ClientHTTPPrivateIPExceptionURLs(ctx context.Context) []string {
|
|
return p.GetProvider(ctx).Strings(ViperKeyClientHTTPPrivateIPExceptionURLs)
|
|
}
|
|
|
|
func (p *Config) ClientSMTPNoPrivateIPRanges(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).BoolF(ViperKeyClientSMTPNoPrivateIPRanges, false)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRegistrationEnabled(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeySelfServiceRegistrationEnabled)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRegistrationLoginHints(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeySelfServiceRegistrationLoginHints)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRegistrationPasswordMethodProfileGroup(ctx context.Context) string {
|
|
switch g := p.GetProvider(ctx).String(ViperKeyPasswordRegistrationProfileGroup); g {
|
|
case "password":
|
|
return "password"
|
|
default:
|
|
return "default"
|
|
}
|
|
}
|
|
|
|
func (p *Config) SelfServiceLegacyOIDCRegistrationGroup(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeyLegacyOIDCRegistrationGroup)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRegistrationTwoSteps(ctx context.Context) bool {
|
|
// The default in previous versions that legacy one-step would be disabled. If legacy is enabled, it means the
|
|
// user has explicitly set the key to true, in which case we respect it.
|
|
if useOneStep := p.GetProvider(ctx).Bool(ViperKeySelfServiceRegistrationEnableLegacyOneStep); useOneStep {
|
|
p.l.Warnf("Found use of deprecated configuration key %q. Please use key %q instead and delete key %[1]q. Will use value from %[1]q to configure registration style.", ViperKeySelfServiceRegistrationEnableLegacyOneStep, ViperKeySelfServiceRegistrationFlowStyle)
|
|
return false
|
|
}
|
|
|
|
// In all other cases, we use the new key which (like the old key) defaults to `profile_first` / two-step registration.
|
|
switch style := p.GetProvider(ctx).String(ViperKeySelfServiceRegistrationFlowStyle); style {
|
|
case "profile_first":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowIdentitySchema(ctx context.Context, requestedSchema string) (string, error) {
|
|
if requestedSchema == p.GetProvider(ctx).String(ViperKeyDefaultIdentitySchemaID) {
|
|
return requestedSchema, nil
|
|
}
|
|
schemas, err := p.IdentityTraitsSchemas(ctx)
|
|
if err != nil {
|
|
return "", errors.WithStack(err)
|
|
}
|
|
for _, schema := range schemas {
|
|
if schema.ID == requestedSchema {
|
|
if !schema.SelfserviceSelectable {
|
|
return "", errors.WithStack(herodot.ErrBadRequest().WithReasonf("Requested identity schema %q is not enabled for self-service flows.", requestedSchema))
|
|
}
|
|
return requestedSchema, nil
|
|
}
|
|
}
|
|
return "", errors.WithStack(herodot.ErrBadRequest().WithReasonf("Requested identity schema %q does not exist.", requestedSchema))
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowVerificationEnabled(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeySelfServiceVerificationEnabled)
|
|
}
|
|
|
|
func (p *Config) UseLegacyShowVerificationUI(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeyUseLegacyShowVerificationUI)
|
|
}
|
|
|
|
func (p *Config) UseLegacyRequireVerifiedLoginError(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeyUseLegacyRequireVerifiedLoginError)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRecoveryEnabled(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeySelfServiceRecoveryEnabled)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRecoveryUse(ctx context.Context) string {
|
|
return p.GetProvider(ctx).String(ViperKeySelfServiceRecoveryUse)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowLoginBeforeHooks(ctx context.Context) []SelfServiceHook {
|
|
return p.selfServiceHooks(ctx, ViperKeySelfServiceLoginBeforeHooks)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRecoveryBeforeHooks(ctx context.Context) []SelfServiceHook {
|
|
return p.selfServiceHooks(ctx, ViperKeySelfServiceRecoveryBeforeHooks)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowVerificationBeforeHooks(ctx context.Context) []SelfServiceHook {
|
|
return p.selfServiceHooks(ctx, ViperKeySelfServiceVerificationBeforeHooks)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowVerificationUse(ctx context.Context) string {
|
|
return p.GetProvider(ctx).String(ViperKeySelfServiceVerificationUse)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowVerificationNotifyUnknownRecipients(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).BoolF(ViperKeySelfServiceVerificationNotifyUnknownRecipients, false)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowSettingsBeforeHooks(ctx context.Context) []SelfServiceHook {
|
|
return p.selfServiceHooks(ctx, ViperKeySelfServiceSettingsBeforeHooks)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRegistrationBeforeHooks(ctx context.Context) []SelfServiceHook {
|
|
hooks := p.selfServiceHooks(ctx, ViperKeySelfServiceRegistrationBeforeHooks)
|
|
if p.SelfServiceFlowRegistrationTwoSteps(ctx) {
|
|
hooks = append(hooks, SelfServiceHook{"two_step_registration", json.RawMessage("{}")})
|
|
}
|
|
|
|
return hooks
|
|
}
|
|
|
|
func (p *Config) selfServiceHooks(ctx context.Context, key string) []SelfServiceHook {
|
|
pp := p.GetProvider(ctx)
|
|
val := pp.Get(key)
|
|
if val == nil {
|
|
return []SelfServiceHook{}
|
|
}
|
|
|
|
config, err := json.Marshal(val)
|
|
if err != nil {
|
|
p.l.WithError(err).Fatalf("Unable to decode values from configuration key: %s", key)
|
|
}
|
|
|
|
var hooks []SelfServiceHook
|
|
if err := json.Unmarshal(config, &hooks); err != nil {
|
|
p.l.WithError(err).Fatalf("Unable to encode value \"%s\" from configuration key: %s", config, key)
|
|
}
|
|
|
|
for k := range hooks {
|
|
if len(hooks[k].Config) == 0 {
|
|
hooks[k].Config = json.RawMessage("{}")
|
|
}
|
|
}
|
|
|
|
return hooks
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowLoginAfterHooks(ctx context.Context, strategy string) []SelfServiceHook {
|
|
return p.selfServiceHooks(ctx, HookStrategyKey(ViperKeySelfServiceLoginAfter, strategy))
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowSettingsAfterHooks(ctx context.Context, strategy string) []SelfServiceHook {
|
|
return p.selfServiceHooks(ctx, HookStrategyKey(ViperKeySelfServiceSettingsAfter, strategy))
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRegistrationAfterHooks(ctx context.Context, strategy string) []SelfServiceHook {
|
|
return p.selfServiceHooks(ctx, HookStrategyKey(ViperKeySelfServiceRegistrationAfter, strategy))
|
|
}
|
|
|
|
func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *SelfServiceStrategy {
|
|
pp := p.GetProvider(ctx)
|
|
config := json.RawMessage("{}")
|
|
basePath := fmt.Sprintf("%s.%s", ViperKeySelfServiceStrategyConfig, strategy)
|
|
|
|
var err error
|
|
config, err = json.Marshal(pp.GetF(basePath+".config", config))
|
|
if err != nil {
|
|
p.l.WithError(err).Warn("Unable to marshal self-service strategy configuration.")
|
|
config = json.RawMessage("{}")
|
|
}
|
|
|
|
// The default value can easily be overwritten by setting e.g. `{"selfservice": "null"}` which means that
|
|
// we need to forcibly set these values here:
|
|
defaultEnabled := false
|
|
switch strategy {
|
|
case "identifier_first":
|
|
defaultEnabled = p.SelfServiceLoginFlowIdentifierFirstEnabled(ctx)
|
|
case "code", "password", "profile":
|
|
defaultEnabled = true
|
|
}
|
|
|
|
// Backwards compatibility for the old "passwordless_enabled" key
|
|
// This force-enables the code strategy, if passwordless is enabled, because in earlier versions it was possible to
|
|
// disable the code strategy, but enable passwordless
|
|
enabled := pp.BoolF(basePath+".enabled", defaultEnabled)
|
|
if strategy == "code" {
|
|
enabled = enabled || pp.Bool(basePath+".passwordless_enabled")
|
|
}
|
|
return &SelfServiceStrategy{
|
|
Enabled: enabled,
|
|
Config: config,
|
|
}
|
|
}
|
|
|
|
func (p *Config) SelfServiceCodeStrategy(ctx context.Context) *SelfServiceStrategyCode {
|
|
pp := p.GetProvider(ctx)
|
|
config := json.RawMessage("{}")
|
|
basePath := ViperKeySelfServiceStrategyConfig + ".code"
|
|
|
|
var err error
|
|
config, err = json.Marshal(pp.GetF(basePath+".config", config))
|
|
if err != nil {
|
|
p.l.WithError(err).Warn("Unable to marshal self service strategy configuration.")
|
|
config = json.RawMessage("{}")
|
|
}
|
|
|
|
return &SelfServiceStrategyCode{
|
|
SelfServiceStrategy: &SelfServiceStrategy{
|
|
Enabled: pp.BoolF(basePath+".enabled", true),
|
|
Config: config,
|
|
},
|
|
PasswordlessEnabled: pp.BoolF(basePath+".passwordless_enabled", false),
|
|
MFAEnabled: pp.BoolF(basePath+".mfa_enabled", false),
|
|
}
|
|
}
|
|
|
|
func (p *Config) SecretsDefault(ctx context.Context) [][]byte {
|
|
pp := p.GetProvider(ctx)
|
|
secrets := pp.Strings(ViperKeySecretsDefault)
|
|
|
|
if len(secrets) == 0 {
|
|
secrets = []string{uuid.Must(uuid.NewV4()).String()}
|
|
p.MustSet(ctx, ViperKeySecretsDefault, secrets)
|
|
}
|
|
|
|
result := make([][]byte, len(secrets))
|
|
for k, v := range secrets {
|
|
result[k] = []byte(v)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (p *Config) SecretsSession(ctx context.Context) [][]byte {
|
|
secrets := p.GetProvider(ctx).Strings(ViperKeySecretsCookie)
|
|
if len(secrets) == 0 {
|
|
return p.SecretsDefault(ctx)
|
|
}
|
|
|
|
result := make([][]byte, len(secrets))
|
|
for k, v := range secrets {
|
|
result[k] = []byte(v)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (p *Config) SecretsCipher(ctx context.Context) [][32]byte {
|
|
secrets := p.GetProvider(ctx).Strings(ViperKeySecretsCipher)
|
|
return ToCipherSecrets(secrets)
|
|
}
|
|
|
|
func ToCipherSecrets(secrets []string) [][32]byte {
|
|
var cleanSecrets []string
|
|
for k := range secrets {
|
|
if len(secrets[k]) == 32 {
|
|
cleanSecrets = append(cleanSecrets, secrets[k])
|
|
}
|
|
}
|
|
if len(cleanSecrets) == 0 {
|
|
return [][32]byte{}
|
|
}
|
|
result := make([][32]byte, len(cleanSecrets))
|
|
for n, s := range secrets {
|
|
for k, v := range []byte(s) {
|
|
result[n][k] = v
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (p *Config) SecretsPagination(ctx context.Context) [][32]byte {
|
|
secrets := p.GetProvider(ctx).Strings(ViperKeySecretsPagination)
|
|
|
|
encryptionKeys := make([][32]byte, len(secrets))
|
|
for i, key := range secrets {
|
|
encryptionKeys[i] = sha512.Sum512_256([]byte(key))
|
|
}
|
|
|
|
return encryptionKeys
|
|
}
|
|
|
|
func (p *Config) SelfServiceBrowserDefaultReturnTo(ctx context.Context) *url.URL {
|
|
return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceBrowserDefaultReturnTo)
|
|
}
|
|
|
|
func (p *Config) SelfPublicURL(ctx context.Context) *url.URL {
|
|
serve := p.ServePublic(ctx)
|
|
return serve.BaseURL
|
|
}
|
|
|
|
func (p *Config) SelfAdminURL(ctx context.Context) *url.URL {
|
|
serve := p.ServeAdmin(ctx)
|
|
return serve.BaseURL
|
|
}
|
|
|
|
func (p *Config) WebhookHeaderAllowlist(ctx context.Context) []string {
|
|
return p.GetProvider(ctx).Strings(ViperKeyWebhookHeaderAllowlist)
|
|
}
|
|
|
|
func (p *Config) OAuth2ProviderHeader(ctx context.Context) http.Header {
|
|
hh := map[string]string{}
|
|
if err := p.GetProvider(ctx).Unmarshal(ViperKeyOAuth2ProviderHeader, &hh); err != nil {
|
|
p.l.WithError(errors.WithStack(err)).
|
|
Errorf("Configuration value from key %s could not be decoded.", ViperKeyOAuth2ProviderHeader)
|
|
return nil
|
|
}
|
|
|
|
h := make(http.Header)
|
|
for k, v := range hh {
|
|
h.Set(k, v)
|
|
}
|
|
|
|
return h
|
|
}
|
|
|
|
func (p *Config) OAuth2ProviderOverrideReturnTo(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeyOAuth2ProviderOverrideReturnTo)
|
|
}
|
|
|
|
func (p *Config) OAuth2ProviderURL(ctx context.Context) *url.URL {
|
|
k := ViperKeyOAuth2ProviderURL
|
|
v := p.GetProvider(ctx).String(k)
|
|
if v == "" {
|
|
return nil
|
|
}
|
|
parsed, err := p.ParseAbsoluteOrRelativeURI(v)
|
|
if err != nil {
|
|
p.l.WithError(errors.WithStack(err)).
|
|
Errorf("Configuration value from key %s is not a valid URL: %s", k, v)
|
|
return nil
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowLoginUI(ctx context.Context) *url.URL {
|
|
return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceLoginUI)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowSettingsUI(ctx context.Context) *url.URL {
|
|
return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceSettingsURL)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowErrorURL(ctx context.Context) *url.URL {
|
|
return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceErrorUI)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRegistrationUI(ctx context.Context) *url.URL {
|
|
return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceRegistrationUI)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRecoveryUI(ctx context.Context) *url.URL {
|
|
return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceRecoveryUI)
|
|
}
|
|
|
|
// SessionLifespan returns time.Hour*24 when the value is not set.
|
|
func (p *Config) SessionLifespan(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeySessionLifespan, time.Hour*24)
|
|
}
|
|
|
|
// OrganizationSessionLifespan returns the effective session lifespan for a
|
|
// session issued to the given organization. If orgID is uuid.Nil, or no
|
|
// matching organization is configured, or the organization has no
|
|
// session_lifespan override, the project-level session.lifespan is returned.
|
|
func (p *Config) OrganizationSessionLifespan(ctx context.Context, orgID uuid.UUID) time.Duration {
|
|
if orgID != uuid.Nil {
|
|
for _, org := range p.Organizations(ctx) {
|
|
if org.ID == orgID && org.SessionLifespan > 0 {
|
|
return org.SessionLifespan
|
|
}
|
|
}
|
|
}
|
|
return p.SessionLifespan(ctx)
|
|
}
|
|
|
|
func (p *Config) SessionPersistentCookie(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeySessionPersistentCookie)
|
|
}
|
|
|
|
func (p *Config) SelfServiceBrowserAllowedReturnToDomains(ctx context.Context) (us []url.URL) {
|
|
src := p.GetProvider(ctx).Strings(ViperKeyURLsAllowedReturnToDomains)
|
|
for k, u := range src {
|
|
if len(u) == 0 {
|
|
continue
|
|
}
|
|
|
|
parsed, err := url.ParseRequestURI(u)
|
|
if err != nil {
|
|
p.l.WithError(err).Warnf("Ignoring URL \"%s\" from configuration key \"%s.%d\".", u, ViperKeyURLsAllowedReturnToDomains, k)
|
|
continue
|
|
}
|
|
// A wildcard host must use the labelled form "*.example.com" bounded at a
|
|
// registrable domain. Reject bare "*", dot-less "*foo.com", public-suffix
|
|
// "*.com", and mid-label "foo.*.com" wildcards — each would match an
|
|
// attacker-registrable host. See corsx.ClassifyOrigin.
|
|
if corsx.ClassifyOrigin(parsed.Host).IsUnsafeWildcard() && !p.LegacyAllowInsecureOrigins(ctx) {
|
|
p.l.Warnf("Ignoring wildcard \"%s\" from configuration key \"%s.%d\".", u, ViperKeyURLsAllowedReturnToDomains, k)
|
|
continue
|
|
}
|
|
|
|
us = append(us, *parsed)
|
|
}
|
|
|
|
return us
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowLoginRequestLifespan(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeySelfServiceLoginRequestLifespan, time.Hour)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowSettingsFlowLifespan(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeySelfServiceSettingsRequestLifespan, time.Hour)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRegistrationRequestLifespan(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeySelfServiceRegistrationRequestLifespan, time.Hour)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowLogoutRedirectURL(ctx context.Context) *url.URL {
|
|
return p.GetProvider(ctx).RequestURIF(ViperKeySelfServiceLogoutBrowserDefaultReturnTo, p.SelfServiceBrowserDefaultReturnTo(ctx))
|
|
}
|
|
|
|
func (p *Config) CourierEmailStrategy(ctx context.Context) string {
|
|
return p.GetProvider(ctx).StringF(ViperKeyCourierDeliveryStrategy, "smtp")
|
|
}
|
|
|
|
func (p *Config) CourierEmailRequestConfig(ctx context.Context) json.RawMessage {
|
|
if p.CourierEmailStrategy(ctx) != "http" {
|
|
return nil
|
|
}
|
|
|
|
config, err := json.Marshal(p.GetProvider(ctx).Get(ViperKeyCourierHTTPRequestConfig))
|
|
if err != nil {
|
|
p.l.WithError(err).Warn("Unable to marshal mailer request configuration.")
|
|
return nil
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesRoot(ctx context.Context) string {
|
|
return p.GetProvider(ctx).StringF(ViperKeyCourierTemplatesPath, "courier/builtin/templates")
|
|
}
|
|
|
|
func (p *Config) CourierEmailTemplatesHelper(ctx context.Context, key string) *CourierEmailTemplate {
|
|
courierTemplate := &CourierEmailTemplate{
|
|
Body: &CourierEmailBodyTemplate{
|
|
PlainText: "",
|
|
HTML: "",
|
|
},
|
|
Subject: "",
|
|
}
|
|
|
|
if !p.GetProvider(ctx).Exists(key) {
|
|
return courierTemplate
|
|
}
|
|
|
|
config, err := json.Marshal(p.GetProvider(ctx).Get(key))
|
|
if err != nil {
|
|
p.l.WithError(err).Fatalf("Unable to decode values from %s.", key)
|
|
return courierTemplate
|
|
}
|
|
|
|
if err := json.Unmarshal(config, courierTemplate); err != nil {
|
|
p.l.WithError(err).Fatalf("Unable to encode values from %s.", key)
|
|
return courierTemplate
|
|
}
|
|
return courierTemplate
|
|
}
|
|
|
|
func (p *Config) CourierSMSTemplatesHelper(ctx context.Context, key string) *CourierSMSTemplate {
|
|
courierTemplate := &CourierSMSTemplate{
|
|
Body: &CourierSMSTemplateBody{
|
|
PlainText: "",
|
|
},
|
|
}
|
|
|
|
if !p.GetProvider(ctx).Exists(key) {
|
|
return courierTemplate
|
|
}
|
|
|
|
config, err := json.Marshal(p.GetProvider(ctx).Get(key))
|
|
if err != nil {
|
|
p.l.WithError(err).Fatalf("Unable to decode values from %s.", key)
|
|
return courierTemplate
|
|
}
|
|
|
|
if err := json.Unmarshal(config, courierTemplate); err != nil {
|
|
p.l.WithError(err).Fatalf("Unable to encode values from %s.", key)
|
|
return courierTemplate
|
|
}
|
|
return courierTemplate
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesVerificationInvalid(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationInvalidEmail)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesVerificationValid(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationValidEmail)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesRecoveryInvalid(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryInvalidEmail)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesRecoveryValid(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryValidEmail)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesRecoveryCodeInvalid(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryCodeInvalidEmail)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesRecoveryCodeValid(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryCodeValidEmail)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesVerificationCodeInvalid(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeInvalidEmail)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesVerificationCodeValid(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidEmail)
|
|
}
|
|
|
|
func (p *Config) CourierSMSTemplatesVerificationCodeValid(ctx context.Context) *CourierSMSTemplate {
|
|
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesVerificationCodeValidSMS)
|
|
}
|
|
|
|
func (p *Config) CourierSMSTemplatesRecoveryCodeValid(ctx context.Context) *CourierSMSTemplate {
|
|
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesRecoveryCodeValidSMS)
|
|
}
|
|
|
|
func (p *Config) CourierSMSTemplatesLoginCodeValid(ctx context.Context) *CourierSMSTemplate {
|
|
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidSMS)
|
|
}
|
|
|
|
func (p *Config) CourierSMSTemplatesRegistrationCodeValid(ctx context.Context) *CourierSMSTemplate {
|
|
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationCodeValidSMS)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesVerifiableAddressChanged(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesVerifiableAddressChangedEmail)
|
|
}
|
|
|
|
func (p *Config) CourierSMSTemplatesVerifiableAddressChanged(ctx context.Context) *CourierSMSTemplate {
|
|
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesVerifiableAddressChangedSMS)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesAuthenticatorKeyAdded(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesAuthenticatorKeyAddedEmail)
|
|
}
|
|
|
|
func (p *Config) CourierSMSTemplatesAuthenticatorKeyAdded(ctx context.Context) *CourierSMSTemplate {
|
|
return p.CourierSMSTemplatesHelper(ctx, ViperKeyCourierTemplatesAuthenticatorKeyAddedSMS)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesLoginCodeValid(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesLoginCodeValidEmail)
|
|
}
|
|
|
|
func (p *Config) CourierTemplatesRegistrationCodeValid(ctx context.Context) *CourierEmailTemplate {
|
|
return p.CourierEmailTemplatesHelper(ctx, ViperKeyCourierTemplatesRegistrationCodeValidEmail)
|
|
}
|
|
|
|
func (p *Config) CourierMessageRetries(ctx context.Context) int {
|
|
return p.GetProvider(ctx).IntF(ViperKeyCourierMessageRetries, 5)
|
|
}
|
|
|
|
func (p *Config) CourierWorkerPullCount(ctx context.Context) int {
|
|
return p.GetProvider(ctx).Int(ViperKeyCourierWorkerPullCount)
|
|
}
|
|
|
|
func (p *Config) CourierWorkerPullWait(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).Duration(ViperKeyCourierWorkerPullWait)
|
|
}
|
|
|
|
func (p *Config) CourierSMTPHeaders(ctx context.Context) map[string]string {
|
|
return p.GetProvider(ctx).StringMap(ViperKeyCourierSMTPHeaders)
|
|
}
|
|
|
|
func (p *Config) CourierChannels(ctx context.Context) (ccs []*CourierChannel, _ error) {
|
|
if err := p.GetProvider(ctx).Unmarshal(ViperKeyCourierChannels, &ccs); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
// load legacy configs
|
|
channel := CourierChannel{
|
|
ID: "email",
|
|
Type: p.CourierEmailStrategy(ctx),
|
|
}
|
|
if channel.Type == "smtp" {
|
|
if err := p.GetProvider(ctx).Unmarshal(ViperKeyCourierSMTP, &channel.SMTPConfig); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
} else {
|
|
if err := p.GetProvider(ctx).Unmarshal(ViperKeyCourierHTTPRequestConfig, &channel.RequestConfig); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
}
|
|
ccs = append(ccs, &channel)
|
|
return ccs, nil
|
|
}
|
|
|
|
func splitUrlAndFragment(s string) (string, string) {
|
|
i := strings.IndexByte(s, '#')
|
|
if i < 0 {
|
|
return s, ""
|
|
}
|
|
return s[:i], s[i+1:]
|
|
}
|
|
|
|
func (p *Config) ParseAbsoluteOrRelativeURIOrFail(ctx context.Context, key string) *url.URL {
|
|
parsed, err := p.ParseAbsoluteOrRelativeURI(p.GetProvider(ctx).String(key))
|
|
if err != nil {
|
|
p.l.WithError(errors.WithStack(err)).
|
|
Fatalf("Configuration value from key %s is not a valid URL: %s", key, p.GetProvider(ctx).String(key))
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func (p *Config) ParseURIOrFail(ctx context.Context, key string) *url.URL {
|
|
parsed, err := p.ParseURI(p.GetProvider(ctx).String(key))
|
|
if err != nil {
|
|
p.l.WithField("reason", "expected scheme to be set").
|
|
Fatalf("Configuration value from key %s is not a valid URL: %s", key, p.GetProvider(ctx).String(key))
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func (p *Config) ParseAbsoluteOrRelativeURI(rawUrl string) (*url.URL, error) {
|
|
u, frag := splitUrlAndFragment(rawUrl)
|
|
parsed, err := url.ParseRequestURI(u)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "configuration value not a valid URL: %s", rawUrl)
|
|
}
|
|
|
|
if frag != "" {
|
|
parsed.Fragment = frag
|
|
}
|
|
|
|
return parsed, nil
|
|
}
|
|
|
|
func (p *Config) ParseURI(rawUrl string) (*url.URL, error) {
|
|
parsed, err := p.ParseAbsoluteOrRelativeURI(rawUrl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if parsed.Scheme == "" {
|
|
return nil, errors.Errorf("configuration value is not a valid URL: %s", rawUrl)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func (p *Config) Tracing(ctx context.Context) *otelx.Config {
|
|
return p.GetProvider(ctx).TracingConfig("Ory Kratos")
|
|
}
|
|
|
|
func (p *Config) IsInsecureDevMode(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool("dev")
|
|
}
|
|
|
|
func (p *Config) IsBackgroundCourierEnabled(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool("watch-courier")
|
|
}
|
|
|
|
func (p *Config) CourierExposeMetricsPort(ctx context.Context) int {
|
|
return p.GetProvider(ctx).Int("expose-metrics-port")
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowVerificationUI(ctx context.Context) *url.URL {
|
|
return p.ParseAbsoluteOrRelativeURIOrFail(ctx, ViperKeySelfServiceVerificationUI)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowVerificationRequestLifespan(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeySelfServiceVerificationRequestLifespan, time.Hour)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowVerificationReturnTo(ctx context.Context, defaultReturnTo *url.URL) *url.URL {
|
|
return p.GetProvider(ctx).RequestURIF(ViperKeySelfServiceVerificationBrowserDefaultReturnTo, defaultReturnTo)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowVerificationAfterHooks(ctx context.Context, strategy string) []SelfServiceHook {
|
|
return p.selfServiceHooks(ctx, HookStrategyKey(ViperKeySelfServiceVerificationAfter, strategy))
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRecoveryReturnTo(ctx context.Context, defaultReturnTo *url.URL) *url.URL {
|
|
return p.GetProvider(ctx).RequestURIF(ViperKeySelfServiceRecoveryBrowserDefaultReturnTo, defaultReturnTo)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRecoveryRequestLifespan(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeySelfServiceRecoveryRequestLifespan, time.Hour)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).BoolF(ViperKeySelfServiceRecoveryNotifyUnknownRecipients, false)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowLogoutClearBrowserData(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).BoolF(ViperKeySelfServiceLogoutClearBrowserData, false)
|
|
}
|
|
|
|
func (p *Config) SelfServiceLinkMethodLifespan(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeyLinkLifespan, time.Hour)
|
|
}
|
|
|
|
func (p *Config) SelfServiceCodeMethodLifespan(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeyCodeLifespan, time.Hour)
|
|
}
|
|
|
|
func (p *Config) SelfServiceCodeMethodMaxSubmissions(ctx context.Context) int {
|
|
return p.GetProvider(ctx).IntF(ViperKeyCodeMaxSubmissions, 5)
|
|
}
|
|
|
|
func (p *Config) SelfServiceCodeMethodMissingCredentialFallbackEnabled(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeyCodeConfigMissingCredentialFallbackEnabled)
|
|
}
|
|
|
|
func (p *Config) DatabaseCleanupSleepTables(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).Duration(ViperKeyDatabaseCleanupSleepTables)
|
|
}
|
|
|
|
func (p *Config) DatabaseCleanupBatchSize(ctx context.Context) int {
|
|
return p.GetProvider(ctx).Int(ViperKeyDatabaseCleanupBatchSize)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRecoveryAfterHooks(ctx context.Context, strategy string) []SelfServiceHook {
|
|
return p.selfServiceHooks(ctx, HookStrategyKey(ViperKeySelfServiceRecoveryAfter, strategy))
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, time.Hour)
|
|
}
|
|
|
|
func (p *Config) SessionSameSiteMode(ctx context.Context) http.SameSite {
|
|
if !p.GetProvider(ctx).Exists(ViperKeySessionSameSite) {
|
|
return p.CookieSameSiteMode(ctx)
|
|
}
|
|
|
|
switch p.GetProvider(ctx).StringF(ViperKeySessionSameSite, "Lax") {
|
|
case "Lax":
|
|
return http.SameSiteLaxMode
|
|
case "Strict":
|
|
return http.SameSiteStrictMode
|
|
case "None":
|
|
return http.SameSiteNoneMode
|
|
}
|
|
return http.SameSiteDefaultMode
|
|
}
|
|
|
|
func (p *Config) SessionDomain(ctx context.Context) string {
|
|
if !p.GetProvider(ctx).Exists(ViperKeySessionDomain) {
|
|
return p.CookieDomain(ctx)
|
|
}
|
|
return p.GetProvider(ctx).String(ViperKeySessionDomain)
|
|
}
|
|
|
|
func (p *Config) SessionCookieSecure(ctx context.Context) bool {
|
|
if !p.GetProvider(ctx).Exists(ViperKeySessionSecure) {
|
|
return !p.IsInsecureDevMode(ctx)
|
|
}
|
|
return p.GetProvider(ctx).Bool(ViperKeySessionSecure)
|
|
}
|
|
|
|
func (p *Config) CookieDomain(ctx context.Context) string {
|
|
return p.GetProvider(ctx).String(ViperKeyCookieDomain)
|
|
}
|
|
|
|
func (p *Config) SessionWhoAmIAAL(ctx context.Context) string {
|
|
return p.GetProvider(ctx).String(ViperKeySessionWhoAmIAAL)
|
|
}
|
|
|
|
func (p *Config) SessionWhoAmICaching(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeySessionWhoAmICaching)
|
|
}
|
|
|
|
func (p *Config) FeatureFlagFasterSessionExtend(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeyFeatureFlagFasterSessionExtend)
|
|
}
|
|
|
|
// LegacyAllowInsecureOrigins restores legacy behavior where wildcard return URLs
|
|
// and CORS origins need not be bounded at a registrable domain. Insecure; only
|
|
// set by the Ory Network for grandfathered projects. Default false.
|
|
func (p *Config) LegacyAllowInsecureOrigins(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeyLegacyAllowInsecureOrigins)
|
|
}
|
|
|
|
func (p *Config) SessionWhoAmICachingMaxAge(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeySessionWhoAmICachingMaxAge, 0)
|
|
}
|
|
|
|
func (p *Config) UseContinueWithTransitions(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeyUseContinueWithTransitions)
|
|
}
|
|
|
|
func (p *Config) ChooseRecoveryAddress(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeyChooseRecoveryAddress)
|
|
}
|
|
|
|
func (p *Config) RefreshLoginChooseAddress(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeyRefreshLoginChooseAddress)
|
|
}
|
|
|
|
func (p *Config) SessionRefreshMinTimeLeft(ctx context.Context) time.Duration {
|
|
return p.GetProvider(ctx).DurationF(ViperKeySessionRefreshMinTimeLeft, p.SessionLifespan(ctx))
|
|
}
|
|
|
|
func (p *Config) SelfServiceSettingsRequiredAAL(ctx context.Context) string {
|
|
return p.GetProvider(ctx).String(ViperKeySelfServiceSettingsRequiredAAL)
|
|
}
|
|
|
|
func (p *Config) CookieSameSiteMode(ctx context.Context) http.SameSite {
|
|
switch p.GetProvider(ctx).StringF(ViperKeyCookieSameSite, "Lax") {
|
|
case "Lax":
|
|
return http.SameSiteLaxMode
|
|
case "Strict":
|
|
return http.SameSiteStrictMode
|
|
case "None":
|
|
return http.SameSiteNoneMode
|
|
}
|
|
return http.SameSiteDefaultMode
|
|
}
|
|
|
|
func (p *Config) SessionPath(ctx context.Context) string {
|
|
if !p.GetProvider(ctx).Exists(ViperKeySessionPath) {
|
|
return p.CookiePath(ctx)
|
|
}
|
|
return p.GetProvider(ctx).String(ViperKeySessionPath)
|
|
}
|
|
|
|
func (p *Config) CookiePath(ctx context.Context) string {
|
|
return p.GetProvider(ctx).String(ViperKeyCookiePath)
|
|
}
|
|
|
|
func (p *Config) CookieSecure(ctx context.Context) bool {
|
|
if !p.GetProvider(ctx).Exists(ViperKeyCookieSecure) {
|
|
return !p.IsInsecureDevMode(ctx)
|
|
}
|
|
return p.GetProvider(ctx).Bool(ViperKeyCookieSecure)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowLoginReturnTo(ctx context.Context, strategy string) *url.URL {
|
|
return p.selfServiceReturnTo(ctx, ViperKeySelfServiceLoginAfter, strategy)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowRegistrationReturnTo(ctx context.Context, strategy string) *url.URL {
|
|
return p.selfServiceReturnTo(ctx, ViperKeySelfServiceRegistrationAfter, strategy)
|
|
}
|
|
|
|
func (p *Config) SelfServiceFlowSettingsReturnTo(ctx context.Context, strategy string, defaultReturnTo *url.URL) *url.URL {
|
|
return p.GetProvider(ctx).RequestURIF(
|
|
ViperKeySelfServiceSettingsAfter+"."+strategy+"."+DefaultBrowserReturnURL,
|
|
p.GetProvider(ctx).RequestURIF(ViperKeySelfServiceSettingsAfter+"."+DefaultBrowserReturnURL,
|
|
defaultReturnTo,
|
|
),
|
|
)
|
|
}
|
|
|
|
func (p *Config) selfServiceReturnTo(ctx context.Context, key string, strategy string) *url.URL {
|
|
return p.GetProvider(ctx).RequestURIF(
|
|
key+"."+strategy+"."+DefaultBrowserReturnURL,
|
|
p.GetProvider(ctx).RequestURIF(key+"."+DefaultBrowserReturnURL,
|
|
p.SelfServiceBrowserDefaultReturnTo(ctx),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (p *Config) ConfigVersion(ctx context.Context) string {
|
|
return p.GetProvider(ctx).StringF(ViperKeyVersion, UnknownVersion)
|
|
}
|
|
|
|
func (p *Config) PasswordPolicyConfig(ctx context.Context) *PasswordPolicy {
|
|
return &PasswordPolicy{
|
|
HaveIBeenPwnedHost: p.GetProvider(ctx).StringF(ViperKeyPasswordHaveIBeenPwnedHost, "api.pwnedpasswords.com"),
|
|
HaveIBeenPwnedEnabled: p.GetProvider(ctx).BoolF(ViperKeyPasswordHaveIBeenPwnedEnabled, true),
|
|
MaxBreaches: uint(p.GetProvider(ctx).Int(ViperKeyPasswordMaxBreaches)), // #nosec G115 -- negative values are prevented by the schema validation
|
|
IgnoreNetworkErrors: p.GetProvider(ctx).BoolF(ViperKeyIgnoreNetworkErrors, true),
|
|
MinPasswordLength: uint(p.GetProvider(ctx).IntF(ViperKeyPasswordMinLength, 8)), // #nosec G115 -- negative values are prevented by the schema validation
|
|
IdentifierSimilarityCheckEnabled: p.GetProvider(ctx).BoolF(ViperKeyPasswordIdentifierSimilarityCheckEnabled, true),
|
|
}
|
|
}
|
|
|
|
func (p *Config) WebAuthnForPasswordless(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).BoolF(ViperKeyWebAuthnPasswordless, false)
|
|
}
|
|
|
|
func (p *Config) WebAuthnConfig(ctx context.Context) *webauthn.Config {
|
|
scheme := p.SelfPublicURL(ctx).Scheme
|
|
id := p.GetProvider(ctx).String(ViperKeyWebAuthnRPID)
|
|
origin := p.GetProvider(ctx).String(ViperKeyWebAuthnRPOrigin)
|
|
origins := p.GetProvider(ctx).StringsF(ViperKeyWebAuthnRPOrigins, []string{cmp.Or(origin, scheme+"://"+id)})
|
|
return &webauthn.Config{
|
|
RPDisplayName: p.GetProvider(ctx).String(ViperKeyWebAuthnRPDisplayName),
|
|
RPID: id,
|
|
RPOrigins: origins,
|
|
AuthenticatorSelection: protocol.AuthenticatorSelection{
|
|
UserVerification: protocol.VerificationDiscouraged,
|
|
},
|
|
EncodeUserIDAsString: false,
|
|
}
|
|
}
|
|
|
|
func (p *Config) PasskeyConfig(ctx context.Context) *webauthn.Config {
|
|
scheme := p.SelfPublicURL(ctx).Scheme
|
|
id := p.GetProvider(ctx).String(ViperKeyPasskeyRPID)
|
|
origins := p.GetProvider(ctx).StringsF(ViperKeyPasskeyRPOrigins, []string{scheme + "://" + id})
|
|
|
|
residentKey := protocol.ResidentKeyRequirement(
|
|
p.GetProvider(ctx).StringF(ViperKeyPasskeyResidentKey, string(protocol.ResidentKeyRequirementRequired)))
|
|
requireResidentKey := residentKey == protocol.ResidentKeyRequirementRequired
|
|
|
|
authSel := protocol.AuthenticatorSelection{
|
|
RequireResidentKey: &requireResidentKey,
|
|
ResidentKey: residentKey,
|
|
UserVerification: protocol.UserVerificationRequirement(
|
|
p.GetProvider(ctx).StringF(ViperKeyPasskeyUserVerification, string(protocol.VerificationPreferred))),
|
|
}
|
|
// Only constrain authenticator attachment when the operator explicitly
|
|
// configures it. Omitting the field lets users register either platform
|
|
// or cross-platform authenticators, which matches the WebAuthn spec's
|
|
// "no preference" behavior.
|
|
if attachment := p.GetProvider(ctx).String(ViperKeyPasskeyAuthenticatorAttachment); attachment != "" {
|
|
authSel.AuthenticatorAttachment = protocol.AuthenticatorAttachment(attachment)
|
|
}
|
|
|
|
cfg := &webauthn.Config{
|
|
RPDisplayName: p.GetProvider(ctx).String(ViperKeyPasskeyRPDisplayName),
|
|
RPID: id,
|
|
RPOrigins: origins,
|
|
AuthenticatorSelection: authSel,
|
|
AttestationPreference: protocol.ConveyancePreference(
|
|
p.GetProvider(ctx).StringF(ViperKeyPasskeyAttestationPreference, string(protocol.PreferNoAttestation))),
|
|
EncodeUserIDAsString: false,
|
|
}
|
|
|
|
if d := p.GetProvider(ctx).Duration(ViperKeyPasskeyRegistrationTimeout); d > 0 {
|
|
cfg.Timeouts.Registration = webauthn.TimeoutConfig{
|
|
Timeout: d,
|
|
TimeoutUVD: d,
|
|
}
|
|
}
|
|
if d := p.GetProvider(ctx).Duration(ViperKeyPasskeyLoginTimeout); d > 0 {
|
|
cfg.Timeouts.Login = webauthn.TimeoutConfig{
|
|
Timeout: d,
|
|
TimeoutUVD: d,
|
|
}
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
type Organization struct {
|
|
ID uuid.UUID `koanf:"id"`
|
|
Domains []string `koanf:"domains"`
|
|
DefaultRegion region.Region `koanf:"default_region"`
|
|
SessionLifespan time.Duration `koanf:"session_lifespan"`
|
|
}
|
|
|
|
func (p *Config) Organizations(ctx context.Context) (orgs []Organization) {
|
|
if err := p.GetProvider(ctx).Unmarshal(ViperKeyOrganizations, &orgs); err != nil {
|
|
return nil
|
|
}
|
|
return orgs
|
|
}
|
|
|
|
func (p *Config) HasherPasswordHashingAlgorithm(ctx context.Context) string {
|
|
configValue := p.GetProvider(ctx).StringF(ViperKeyHasherAlgorithm, DefaultPasswordHashingAlgorithm)
|
|
switch configValue {
|
|
case "bcrypt":
|
|
return configValue
|
|
case "argon2":
|
|
fallthrough
|
|
default:
|
|
return configValue
|
|
}
|
|
}
|
|
|
|
func (p *Config) CipherAlgorithm(ctx context.Context) string {
|
|
configValue := p.GetProvider(ctx).StringF(ViperKeyCipherAlgorithm, DefaultCipherAlgorithm)
|
|
switch configValue {
|
|
case "noop":
|
|
return configValue
|
|
case "xchacha20-poly1305":
|
|
return configValue
|
|
case "aes":
|
|
fallthrough
|
|
default:
|
|
return configValue
|
|
}
|
|
}
|
|
|
|
func (p *Config) GetProvider(ctx context.Context) *configx.Provider {
|
|
return p.c.Config(ctx, p.p)
|
|
}
|
|
|
|
type SessionTokenizeFormat struct {
|
|
TTL time.Duration `koanf:"ttl" json:"ttl"`
|
|
ClaimsMapperURL string `koanf:"claims_mapper_url" json:"claims_mapper_url"`
|
|
JWKSURL string `koanf:"jwks_url" json:"jwks_url"`
|
|
SubjectSource string `koanf:"subject_source" json:"subject_source"`
|
|
}
|
|
|
|
func (p *Config) TokenizeTemplate(ctx context.Context, key string) (_ *SessionTokenizeFormat, err error) {
|
|
var result SessionTokenizeFormat
|
|
path := ViperKeySessionTokenizerTemplates + "." + key
|
|
if !p.GetProvider(ctx).Exists(path) {
|
|
return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to find tokenizer template \"%s\".", key))
|
|
}
|
|
|
|
if err := p.GetProvider(ctx).Unmarshal(path, &result); err != nil {
|
|
return nil, errors.WithStack(herodot.ErrMisconfiguration().WithReasonf("Unable to decode tokenizer template \"%s\": %s", key, err))
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (p *Config) DefaultConsistencyLevel(ctx context.Context) crdbx.ConsistencyLevel {
|
|
return crdbx.ConsistencyLevelFromString(p.GetProvider(ctx).String(ViperKeyPreviewDefaultReadConsistencyLevel))
|
|
}
|
|
|
|
func (p *Config) PasswordMigrationHook(ctx context.Context) *PasswordMigrationHook {
|
|
hook := &PasswordMigrationHook{
|
|
Enabled: p.GetProvider(ctx).BoolF(ViperKeyPasswordMigrationHook+".enabled", false),
|
|
}
|
|
if !hook.Enabled {
|
|
return hook
|
|
}
|
|
|
|
_ = p.GetProvider(ctx).Unmarshal(ViperKeyPasswordMigrationHook+".config", &hook.Config)
|
|
|
|
return hook
|
|
}
|
|
|
|
func (p *Config) SelfServiceLoginFlowIdentifierFirstEnabled(ctx context.Context) bool {
|
|
switch p.GetProvider(ctx).String(ViperKeySelfServiceLoginFlowStyle) {
|
|
case "identifier_first":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (p *Config) SecurityAccountEnumerationMitigate(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeySecurityAccountEnumerationMitigate)
|
|
}
|
|
|
|
func (p *Config) SecurityDisallowRefInIdentitySchemas(ctx context.Context) bool {
|
|
return p.GetProvider(ctx).Bool(ViperKeySecurityDisallowRefInIdentitySchemas)
|
|
}
|