mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-13 23:25:22 +00:00
Allow logging in via email
This commit is contained in:
+26
-9
@@ -153,24 +153,26 @@ func (a *Manager) asyncExpiredMagicLinkReapLoop(interval time.Duration) {
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate checks username and password and returns a User if correct, and the user has not been
|
||||
// marked as deleted. The method returns in constant-ish time, regardless of whether the user exists or
|
||||
// the password is correct or incorrect.
|
||||
func (a *Manager) Authenticate(username, password string) (*User, error) {
|
||||
if username == Everyone {
|
||||
// Authenticate checks a login identifier (a username or a verified primary email) and password, and
|
||||
// returns a User if correct and not marked as deleted. The identifier is resolved in a single query
|
||||
// via userByNameOrEmail, so a user can log in with either their username or their primary
|
||||
// email. The method returns in constant-ish time (one query, one bcrypt compare), regardless of
|
||||
// whether the identifier exists or the password is correct or incorrect.
|
||||
func (a *Manager) Authenticate(identifier, password string) (*User, error) {
|
||||
if identifier == Everyone {
|
||||
return nil, ErrUnauthenticated
|
||||
}
|
||||
user, err := a.User(username)
|
||||
user, err := a.userByNameOrEmail(identifier)
|
||||
if err != nil {
|
||||
log.Tag(tag).Field("user_name", username).Err(err).Trace("Authentication of user failed (1)")
|
||||
log.Tag(tag).Field("user_name", identifier).Err(err).Trace("Authentication of user failed (1)")
|
||||
bcrypt.CompareHashAndPassword([]byte(userAuthIntentionalSlowDownHash), []byte("intentional slow-down to avoid timing attacks"))
|
||||
return nil, ErrUnauthenticated
|
||||
} else if user.Deleted {
|
||||
log.Tag(tag).Field("user_name", username).Trace("Authentication of user failed (2): user marked deleted")
|
||||
log.Tag(tag).Field("user_name", identifier).Trace("Authentication of user failed (2): user marked deleted")
|
||||
bcrypt.CompareHashAndPassword([]byte(userAuthIntentionalSlowDownHash), []byte("intentional slow-down to avoid timing attacks"))
|
||||
return nil, ErrUnauthenticated
|
||||
} else if err := bcrypt.CompareHashAndPassword([]byte(user.Hash), []byte(password)); err != nil {
|
||||
log.Tag(tag).Field("user_name", username).Err(err).Trace("Authentication of user failed (3)")
|
||||
log.Tag(tag).Field("user_name", identifier).Err(err).Trace("Authentication of user failed (3)")
|
||||
return nil, ErrUnauthenticated
|
||||
}
|
||||
return user, nil
|
||||
@@ -532,6 +534,21 @@ func (a *Manager) UserByEmailOrUsername(identifier string) (*User, error) {
|
||||
return a.User(identifier)
|
||||
}
|
||||
|
||||
// userByNameOrEmail resolves a login identifier to a single user in one query, matching it
|
||||
// against the username first and a verified primary email address second. This is the INVERSE
|
||||
// precedence of UserByEmailOrUsername (used by password reset): at login a freely-chosen username
|
||||
// must win over a look-alike primary email, so a user whose username happens to equal another
|
||||
// account's email is not locked out of their own account. Because Authenticate still gates the match
|
||||
// on a password check, returning the username owner here never grants access to the email owner's
|
||||
// account. Returns ErrUserNotFound if neither matches.
|
||||
func (a *Manager) userByNameOrEmail(identifier string) (*User, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserByNameOrPrimaryEmail, identifier, identifier, identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.readUser(rows)
|
||||
}
|
||||
|
||||
// userByToken returns the user with the given token if it exists and is not expired, or ErrUserNotFound otherwise
|
||||
func (a *Manager) userByToken(token string) (*User, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserByToken, token, time.Now().Unix())
|
||||
|
||||
+84
-74
@@ -33,6 +33,15 @@ const (
|
||||
LEFT JOIN tier t on t.id = u.tier_id
|
||||
WHERE user_name = $1
|
||||
`
|
||||
postgresSelectUserByNameOrPrimaryEmailQuery = `
|
||||
SELECT u.id, u.user_name, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, u.deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
||||
FROM "user" u
|
||||
LEFT JOIN tier t on t.id = u.tier_id
|
||||
WHERE u.user_name = $1
|
||||
OR u.id = (SELECT user_id FROM user_email WHERE email = $2 AND is_primary)
|
||||
ORDER BY CASE WHEN u.user_name = $3 THEN 0 ELSE 1 END
|
||||
LIMIT 1
|
||||
`
|
||||
postgresSelectUserByTokenQuery = `
|
||||
SELECT u.id, u.user_name, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, u.deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
||||
FROM "user" u
|
||||
@@ -260,80 +269,81 @@ func postgresSelectAccessCacheUsersQuery(n int) string {
|
||||
|
||||
// NewPostgresManager creates a new Manager backed by a PostgreSQL database using an existing connection pool.
|
||||
var postgresQueries = queries{
|
||||
selectUserByID: postgresSelectUserByIDQuery,
|
||||
selectUserByName: postgresSelectUserByNameQuery,
|
||||
selectUserByToken: postgresSelectUserByTokenQuery,
|
||||
selectUserByStripeCustomerID: postgresSelectUserByStripeCustomerIDQuery,
|
||||
selectUsernames: postgresSelectUsernamesQuery,
|
||||
selectUsers: postgresSelectUsersQuery,
|
||||
selectUserCount: postgresSelectUserCountQuery,
|
||||
selectUserIDFromUsername: postgresSelectUserIDFromUsernameQuery,
|
||||
insertUser: postgresInsertUserQuery,
|
||||
updateUserPass: postgresUpdateUserPassQuery,
|
||||
updateUserRole: postgresUpdateUserRoleQuery,
|
||||
updateUserProvisioned: postgresUpdateUserProvisionedQuery,
|
||||
updateUserPrefs: postgresUpdateUserPrefsQuery,
|
||||
updateUserStats: postgresUpdateUserStatsQuery,
|
||||
updateUserStatsResetAll: postgresUpdateUserStatsResetAllQuery,
|
||||
updateUserTier: postgresUpdateUserTierQuery,
|
||||
updateUserDeleted: postgresUpdateUserDeletedQuery,
|
||||
deleteUser: postgresDeleteUserQuery,
|
||||
deleteUserTier: postgresDeleteUserTierQuery,
|
||||
deleteUsersMarked: postgresDeleteUsersMarkedQuery,
|
||||
deleteUsersProvisioned: postgresDeleteUsersProvisionedQuery,
|
||||
selectTopicPerms: postgresSelectTopicPermsQuery,
|
||||
selectAccessCacheAll: postgresSelectAccessCacheAllQuery,
|
||||
selectAccessCacheUsers: postgresSelectAccessCacheUsersQuery,
|
||||
selectUserAllAccess: postgresSelectUserAllAccessQuery,
|
||||
selectUserAccess: postgresSelectUserAccessQuery,
|
||||
selectUserReservations: postgresSelectUserReservationsQuery,
|
||||
selectUserReservationsCount: postgresSelectUserReservationsCountQuery,
|
||||
selectUserReservationsOwner: postgresSelectUserReservationsOwnerQuery,
|
||||
selectUserHasReservation: postgresSelectUserHasReservationQuery,
|
||||
selectOtherAccessCount: postgresSelectOtherAccessCountQuery,
|
||||
upsertUserAccess: postgresUpsertUserAccessQuery,
|
||||
deleteUserAccess: postgresDeleteUserAccessQuery,
|
||||
deleteUserAccessProvisioned: postgresDeleteUserAccessProvisionedQuery,
|
||||
deleteTopicAccess: postgresDeleteTopicAccessQuery,
|
||||
deleteAllAccess: postgresDeleteAllAccessQuery,
|
||||
selectToken: postgresSelectTokenQuery,
|
||||
selectTokens: postgresSelectTokensQuery,
|
||||
selectTokenCount: postgresSelectTokenCountQuery,
|
||||
selectAllProvisionedTokens: postgresSelectAllProvisionedTokensQuery,
|
||||
upsertToken: postgresUpsertTokenQuery,
|
||||
updateToken: postgresUpdateTokenQuery,
|
||||
updateTokenLastAccess: postgresUpdateTokenLastAccessQuery,
|
||||
deleteToken: postgresDeleteTokenQuery,
|
||||
deleteProvisionedToken: postgresDeleteProvisionedTokenQuery,
|
||||
deleteAllProvisionedTokens: postgresDeleteAllProvisionedTokensQuery,
|
||||
deleteAllToken: postgresDeleteAllTokenQuery,
|
||||
deleteExpiredTokens: postgresDeleteExpiredTokensQuery,
|
||||
deleteExcessTokens: postgresDeleteExcessTokensQuery,
|
||||
insertTier: postgresInsertTierQuery,
|
||||
selectTiers: postgresSelectTiersQuery,
|
||||
selectTierByCode: postgresSelectTierByCodeQuery,
|
||||
selectTierByPriceID: postgresSelectTierByPriceIDQuery,
|
||||
updateTier: postgresUpdateTierQuery,
|
||||
deleteTier: postgresDeleteTierQuery,
|
||||
selectPhoneNumbers: postgresSelectPhoneNumbersQuery,
|
||||
insertPhoneNumber: postgresInsertPhoneNumberQuery,
|
||||
deletePhoneNumber: postgresDeletePhoneNumberQuery,
|
||||
selectEmails: postgresSelectEmailsQuery,
|
||||
insertEmail: postgresInsertEmailQuery,
|
||||
insertEmailIgnore: postgresInsertEmailIgnoreQuery,
|
||||
deleteEmail: postgresDeleteEmailQuery,
|
||||
selectPrimaryEmail: postgresSelectPrimaryEmailQuery,
|
||||
selectUserIDByPrimary: postgresSelectUserIDByPrimaryQuery,
|
||||
updateEmailSetPrimary: postgresUpdateEmailSetPrimaryQuery,
|
||||
updateEmailClearPrimary: postgresUpdateEmailClearPrimaryQuery,
|
||||
insertMagicLink: postgresInsertMagicLinkQuery,
|
||||
selectMagicLinkByHash: postgresSelectMagicLinkByHashQuery,
|
||||
deleteMagicLinkByHash: postgresDeleteMagicLinkByHashQuery,
|
||||
deleteMagicLinkEmailVerify: postgresDeleteVerifyScopeQuery,
|
||||
deleteMagicLinkResetPassword: postgresDeleteResetScopeQuery,
|
||||
selectPendingEmails: postgresSelectPendingEmailsQuery,
|
||||
deleteExpiredMagicLinks: postgresDeleteExpiredMagicLinksQuery,
|
||||
updateBilling: postgresUpdateBillingQuery,
|
||||
selectUserByID: postgresSelectUserByIDQuery,
|
||||
selectUserByName: postgresSelectUserByNameQuery,
|
||||
selectUserByNameOrPrimaryEmail: postgresSelectUserByNameOrPrimaryEmailQuery,
|
||||
selectUserByToken: postgresSelectUserByTokenQuery,
|
||||
selectUserByStripeCustomerID: postgresSelectUserByStripeCustomerIDQuery,
|
||||
selectUsernames: postgresSelectUsernamesQuery,
|
||||
selectUsers: postgresSelectUsersQuery,
|
||||
selectUserCount: postgresSelectUserCountQuery,
|
||||
selectUserIDFromUsername: postgresSelectUserIDFromUsernameQuery,
|
||||
insertUser: postgresInsertUserQuery,
|
||||
updateUserPass: postgresUpdateUserPassQuery,
|
||||
updateUserRole: postgresUpdateUserRoleQuery,
|
||||
updateUserProvisioned: postgresUpdateUserProvisionedQuery,
|
||||
updateUserPrefs: postgresUpdateUserPrefsQuery,
|
||||
updateUserStats: postgresUpdateUserStatsQuery,
|
||||
updateUserStatsResetAll: postgresUpdateUserStatsResetAllQuery,
|
||||
updateUserTier: postgresUpdateUserTierQuery,
|
||||
updateUserDeleted: postgresUpdateUserDeletedQuery,
|
||||
deleteUser: postgresDeleteUserQuery,
|
||||
deleteUserTier: postgresDeleteUserTierQuery,
|
||||
deleteUsersMarked: postgresDeleteUsersMarkedQuery,
|
||||
deleteUsersProvisioned: postgresDeleteUsersProvisionedQuery,
|
||||
selectTopicPerms: postgresSelectTopicPermsQuery,
|
||||
selectAccessCacheAll: postgresSelectAccessCacheAllQuery,
|
||||
selectAccessCacheUsers: postgresSelectAccessCacheUsersQuery,
|
||||
selectUserAllAccess: postgresSelectUserAllAccessQuery,
|
||||
selectUserAccess: postgresSelectUserAccessQuery,
|
||||
selectUserReservations: postgresSelectUserReservationsQuery,
|
||||
selectUserReservationsCount: postgresSelectUserReservationsCountQuery,
|
||||
selectUserReservationsOwner: postgresSelectUserReservationsOwnerQuery,
|
||||
selectUserHasReservation: postgresSelectUserHasReservationQuery,
|
||||
selectOtherAccessCount: postgresSelectOtherAccessCountQuery,
|
||||
upsertUserAccess: postgresUpsertUserAccessQuery,
|
||||
deleteUserAccess: postgresDeleteUserAccessQuery,
|
||||
deleteUserAccessProvisioned: postgresDeleteUserAccessProvisionedQuery,
|
||||
deleteTopicAccess: postgresDeleteTopicAccessQuery,
|
||||
deleteAllAccess: postgresDeleteAllAccessQuery,
|
||||
selectToken: postgresSelectTokenQuery,
|
||||
selectTokens: postgresSelectTokensQuery,
|
||||
selectTokenCount: postgresSelectTokenCountQuery,
|
||||
selectAllProvisionedTokens: postgresSelectAllProvisionedTokensQuery,
|
||||
upsertToken: postgresUpsertTokenQuery,
|
||||
updateToken: postgresUpdateTokenQuery,
|
||||
updateTokenLastAccess: postgresUpdateTokenLastAccessQuery,
|
||||
deleteToken: postgresDeleteTokenQuery,
|
||||
deleteProvisionedToken: postgresDeleteProvisionedTokenQuery,
|
||||
deleteAllProvisionedTokens: postgresDeleteAllProvisionedTokensQuery,
|
||||
deleteAllToken: postgresDeleteAllTokenQuery,
|
||||
deleteExpiredTokens: postgresDeleteExpiredTokensQuery,
|
||||
deleteExcessTokens: postgresDeleteExcessTokensQuery,
|
||||
insertTier: postgresInsertTierQuery,
|
||||
selectTiers: postgresSelectTiersQuery,
|
||||
selectTierByCode: postgresSelectTierByCodeQuery,
|
||||
selectTierByPriceID: postgresSelectTierByPriceIDQuery,
|
||||
updateTier: postgresUpdateTierQuery,
|
||||
deleteTier: postgresDeleteTierQuery,
|
||||
selectPhoneNumbers: postgresSelectPhoneNumbersQuery,
|
||||
insertPhoneNumber: postgresInsertPhoneNumberQuery,
|
||||
deletePhoneNumber: postgresDeletePhoneNumberQuery,
|
||||
selectEmails: postgresSelectEmailsQuery,
|
||||
insertEmail: postgresInsertEmailQuery,
|
||||
insertEmailIgnore: postgresInsertEmailIgnoreQuery,
|
||||
deleteEmail: postgresDeleteEmailQuery,
|
||||
selectPrimaryEmail: postgresSelectPrimaryEmailQuery,
|
||||
selectUserIDByPrimary: postgresSelectUserIDByPrimaryQuery,
|
||||
updateEmailSetPrimary: postgresUpdateEmailSetPrimaryQuery,
|
||||
updateEmailClearPrimary: postgresUpdateEmailClearPrimaryQuery,
|
||||
insertMagicLink: postgresInsertMagicLinkQuery,
|
||||
selectMagicLinkByHash: postgresSelectMagicLinkByHashQuery,
|
||||
deleteMagicLinkByHash: postgresDeleteMagicLinkByHashQuery,
|
||||
deleteMagicLinkEmailVerify: postgresDeleteVerifyScopeQuery,
|
||||
deleteMagicLinkResetPassword: postgresDeleteResetScopeQuery,
|
||||
selectPendingEmails: postgresSelectPendingEmailsQuery,
|
||||
deleteExpiredMagicLinks: postgresDeleteExpiredMagicLinksQuery,
|
||||
updateBilling: postgresUpdateBillingQuery,
|
||||
}
|
||||
|
||||
// NewPostgresManager creates a new Manager backed by a PostgreSQL database
|
||||
|
||||
+84
-74
@@ -37,6 +37,15 @@ const (
|
||||
LEFT JOIN tier t on t.id = u.tier_id
|
||||
WHERE user = ?
|
||||
`
|
||||
sqliteSelectUserByNameOrPrimaryEmailQuery = `
|
||||
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
||||
FROM user u
|
||||
LEFT JOIN tier t on t.id = u.tier_id
|
||||
WHERE u.user = ?
|
||||
OR u.id = (SELECT user_id FROM user_email WHERE email = ? AND is_primary = 1)
|
||||
ORDER BY CASE WHEN u.user = ? THEN 0 ELSE 1 END
|
||||
LIMIT 1
|
||||
`
|
||||
sqliteSelectUserByTokenQuery = `
|
||||
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.provisioned, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
||||
FROM user u
|
||||
@@ -256,80 +265,81 @@ func sqliteSelectAccessCacheUsersQuery(n int) string {
|
||||
}
|
||||
|
||||
var sqliteQueries = queries{
|
||||
selectUserByID: sqliteSelectUserByIDQuery,
|
||||
selectUserByName: sqliteSelectUserByNameQuery,
|
||||
selectUserByToken: sqliteSelectUserByTokenQuery,
|
||||
selectUserByStripeCustomerID: sqliteSelectUserByStripeCustomerIDQuery,
|
||||
selectUsernames: sqliteSelectUsernamesQuery,
|
||||
selectUsers: sqliteSelectUsersQuery,
|
||||
selectUserCount: sqliteSelectUserCountQuery,
|
||||
selectUserIDFromUsername: sqliteSelectUserIDFromUsernameQuery,
|
||||
insertUser: sqliteInsertUserQuery,
|
||||
updateUserPass: sqliteUpdateUserPassQuery,
|
||||
updateUserRole: sqliteUpdateUserRoleQuery,
|
||||
updateUserProvisioned: sqliteUpdateUserProvisionedQuery,
|
||||
updateUserPrefs: sqliteUpdateUserPrefsQuery,
|
||||
updateUserStats: sqliteUpdateUserStatsQuery,
|
||||
updateUserStatsResetAll: sqliteUpdateUserStatsResetAllQuery,
|
||||
updateUserTier: sqliteUpdateUserTierQuery,
|
||||
updateUserDeleted: sqliteUpdateUserDeletedQuery,
|
||||
deleteUser: sqliteDeleteUserQuery,
|
||||
deleteUserTier: sqliteDeleteUserTierQuery,
|
||||
deleteUsersMarked: sqliteDeleteUsersMarkedQuery,
|
||||
deleteUsersProvisioned: sqliteDeleteUsersProvisionedQuery,
|
||||
selectTopicPerms: sqliteSelectTopicPermsQuery,
|
||||
selectAccessCacheAll: sqliteSelectAccessCacheAllQuery,
|
||||
selectAccessCacheUsers: sqliteSelectAccessCacheUsersQuery,
|
||||
selectUserAllAccess: sqliteSelectUserAllAccessQuery,
|
||||
selectUserAccess: sqliteSelectUserAccessQuery,
|
||||
selectUserReservations: sqliteSelectUserReservationsQuery,
|
||||
selectUserReservationsCount: sqliteSelectUserReservationsCountQuery,
|
||||
selectUserReservationsOwner: sqliteSelectUserReservationsOwnerQuery,
|
||||
selectUserHasReservation: sqliteSelectUserHasReservationQuery,
|
||||
selectOtherAccessCount: sqliteSelectOtherAccessCountQuery,
|
||||
upsertUserAccess: sqliteUpsertUserAccessQuery,
|
||||
deleteUserAccess: sqliteDeleteUserAccessQuery,
|
||||
deleteUserAccessProvisioned: sqliteDeleteUserAccessProvisionedQuery,
|
||||
deleteTopicAccess: sqliteDeleteTopicAccessQuery,
|
||||
deleteAllAccess: sqliteDeleteAllAccessQuery,
|
||||
selectToken: sqliteSelectTokenQuery,
|
||||
selectTokens: sqliteSelectTokensQuery,
|
||||
selectTokenCount: sqliteSelectTokenCountQuery,
|
||||
selectAllProvisionedTokens: sqliteSelectAllProvisionedTokensQuery,
|
||||
upsertToken: sqliteUpsertTokenQuery,
|
||||
updateToken: sqliteUpdateTokenQuery,
|
||||
updateTokenLastAccess: sqliteUpdateTokenLastAccessQuery,
|
||||
deleteToken: sqliteDeleteTokenQuery,
|
||||
deleteProvisionedToken: sqliteDeleteProvisionedTokenQuery,
|
||||
deleteAllProvisionedTokens: sqliteDeleteAllProvisionedTokensQuery,
|
||||
deleteAllToken: sqliteDeleteAllTokenQuery,
|
||||
deleteExpiredTokens: sqliteDeleteExpiredTokensQuery,
|
||||
deleteExcessTokens: sqliteDeleteExcessTokensQuery,
|
||||
insertTier: sqliteInsertTierQuery,
|
||||
selectTiers: sqliteSelectTiersQuery,
|
||||
selectTierByCode: sqliteSelectTierByCodeQuery,
|
||||
selectTierByPriceID: sqliteSelectTierByPriceIDQuery,
|
||||
updateTier: sqliteUpdateTierQuery,
|
||||
deleteTier: sqliteDeleteTierQuery,
|
||||
selectPhoneNumbers: sqliteSelectPhoneNumbersQuery,
|
||||
insertPhoneNumber: sqliteInsertPhoneNumberQuery,
|
||||
deletePhoneNumber: sqliteDeletePhoneNumberQuery,
|
||||
selectEmails: sqliteSelectEmailsQuery,
|
||||
insertEmail: sqliteInsertEmailQuery,
|
||||
insertEmailIgnore: sqliteInsertEmailIgnoreQuery,
|
||||
deleteEmail: sqliteDeleteEmailQuery,
|
||||
selectPrimaryEmail: sqliteSelectPrimaryEmailQuery,
|
||||
selectUserIDByPrimary: sqliteSelectUserIDByPrimaryQuery,
|
||||
updateEmailSetPrimary: sqliteUpdateEmailSetPrimaryQuery,
|
||||
updateEmailClearPrimary: sqliteUpdateEmailClearPrimaryQuery,
|
||||
insertMagicLink: sqliteInsertMagicLinkQuery,
|
||||
selectMagicLinkByHash: sqliteSelectMagicLinkByHashQuery,
|
||||
deleteMagicLinkByHash: sqliteDeleteMagicLinkByHashQuery,
|
||||
deleteMagicLinkEmailVerify: sqliteDeleteVerifyScopeQuery,
|
||||
deleteMagicLinkResetPassword: sqliteDeleteResetScopeQuery,
|
||||
selectPendingEmails: sqliteSelectPendingEmailsQuery,
|
||||
deleteExpiredMagicLinks: sqliteDeleteExpiredMagicLinksQuery,
|
||||
updateBilling: sqliteUpdateBillingQuery,
|
||||
selectUserByID: sqliteSelectUserByIDQuery,
|
||||
selectUserByName: sqliteSelectUserByNameQuery,
|
||||
selectUserByNameOrPrimaryEmail: sqliteSelectUserByNameOrPrimaryEmailQuery,
|
||||
selectUserByToken: sqliteSelectUserByTokenQuery,
|
||||
selectUserByStripeCustomerID: sqliteSelectUserByStripeCustomerIDQuery,
|
||||
selectUsernames: sqliteSelectUsernamesQuery,
|
||||
selectUsers: sqliteSelectUsersQuery,
|
||||
selectUserCount: sqliteSelectUserCountQuery,
|
||||
selectUserIDFromUsername: sqliteSelectUserIDFromUsernameQuery,
|
||||
insertUser: sqliteInsertUserQuery,
|
||||
updateUserPass: sqliteUpdateUserPassQuery,
|
||||
updateUserRole: sqliteUpdateUserRoleQuery,
|
||||
updateUserProvisioned: sqliteUpdateUserProvisionedQuery,
|
||||
updateUserPrefs: sqliteUpdateUserPrefsQuery,
|
||||
updateUserStats: sqliteUpdateUserStatsQuery,
|
||||
updateUserStatsResetAll: sqliteUpdateUserStatsResetAllQuery,
|
||||
updateUserTier: sqliteUpdateUserTierQuery,
|
||||
updateUserDeleted: sqliteUpdateUserDeletedQuery,
|
||||
deleteUser: sqliteDeleteUserQuery,
|
||||
deleteUserTier: sqliteDeleteUserTierQuery,
|
||||
deleteUsersMarked: sqliteDeleteUsersMarkedQuery,
|
||||
deleteUsersProvisioned: sqliteDeleteUsersProvisionedQuery,
|
||||
selectTopicPerms: sqliteSelectTopicPermsQuery,
|
||||
selectAccessCacheAll: sqliteSelectAccessCacheAllQuery,
|
||||
selectAccessCacheUsers: sqliteSelectAccessCacheUsersQuery,
|
||||
selectUserAllAccess: sqliteSelectUserAllAccessQuery,
|
||||
selectUserAccess: sqliteSelectUserAccessQuery,
|
||||
selectUserReservations: sqliteSelectUserReservationsQuery,
|
||||
selectUserReservationsCount: sqliteSelectUserReservationsCountQuery,
|
||||
selectUserReservationsOwner: sqliteSelectUserReservationsOwnerQuery,
|
||||
selectUserHasReservation: sqliteSelectUserHasReservationQuery,
|
||||
selectOtherAccessCount: sqliteSelectOtherAccessCountQuery,
|
||||
upsertUserAccess: sqliteUpsertUserAccessQuery,
|
||||
deleteUserAccess: sqliteDeleteUserAccessQuery,
|
||||
deleteUserAccessProvisioned: sqliteDeleteUserAccessProvisionedQuery,
|
||||
deleteTopicAccess: sqliteDeleteTopicAccessQuery,
|
||||
deleteAllAccess: sqliteDeleteAllAccessQuery,
|
||||
selectToken: sqliteSelectTokenQuery,
|
||||
selectTokens: sqliteSelectTokensQuery,
|
||||
selectTokenCount: sqliteSelectTokenCountQuery,
|
||||
selectAllProvisionedTokens: sqliteSelectAllProvisionedTokensQuery,
|
||||
upsertToken: sqliteUpsertTokenQuery,
|
||||
updateToken: sqliteUpdateTokenQuery,
|
||||
updateTokenLastAccess: sqliteUpdateTokenLastAccessQuery,
|
||||
deleteToken: sqliteDeleteTokenQuery,
|
||||
deleteProvisionedToken: sqliteDeleteProvisionedTokenQuery,
|
||||
deleteAllProvisionedTokens: sqliteDeleteAllProvisionedTokensQuery,
|
||||
deleteAllToken: sqliteDeleteAllTokenQuery,
|
||||
deleteExpiredTokens: sqliteDeleteExpiredTokensQuery,
|
||||
deleteExcessTokens: sqliteDeleteExcessTokensQuery,
|
||||
insertTier: sqliteInsertTierQuery,
|
||||
selectTiers: sqliteSelectTiersQuery,
|
||||
selectTierByCode: sqliteSelectTierByCodeQuery,
|
||||
selectTierByPriceID: sqliteSelectTierByPriceIDQuery,
|
||||
updateTier: sqliteUpdateTierQuery,
|
||||
deleteTier: sqliteDeleteTierQuery,
|
||||
selectPhoneNumbers: sqliteSelectPhoneNumbersQuery,
|
||||
insertPhoneNumber: sqliteInsertPhoneNumberQuery,
|
||||
deletePhoneNumber: sqliteDeletePhoneNumberQuery,
|
||||
selectEmails: sqliteSelectEmailsQuery,
|
||||
insertEmail: sqliteInsertEmailQuery,
|
||||
insertEmailIgnore: sqliteInsertEmailIgnoreQuery,
|
||||
deleteEmail: sqliteDeleteEmailQuery,
|
||||
selectPrimaryEmail: sqliteSelectPrimaryEmailQuery,
|
||||
selectUserIDByPrimary: sqliteSelectUserIDByPrimaryQuery,
|
||||
updateEmailSetPrimary: sqliteUpdateEmailSetPrimaryQuery,
|
||||
updateEmailClearPrimary: sqliteUpdateEmailClearPrimaryQuery,
|
||||
insertMagicLink: sqliteInsertMagicLinkQuery,
|
||||
selectMagicLinkByHash: sqliteSelectMagicLinkByHashQuery,
|
||||
deleteMagicLinkByHash: sqliteDeleteMagicLinkByHashQuery,
|
||||
deleteMagicLinkEmailVerify: sqliteDeleteVerifyScopeQuery,
|
||||
deleteMagicLinkResetPassword: sqliteDeleteResetScopeQuery,
|
||||
selectPendingEmails: sqliteSelectPendingEmailsQuery,
|
||||
deleteExpiredMagicLinks: sqliteDeleteExpiredMagicLinksQuery,
|
||||
updateBilling: sqliteUpdateBillingQuery,
|
||||
}
|
||||
|
||||
// NewSQLiteManager creates a new Manager backed by a SQLite database
|
||||
|
||||
@@ -2947,6 +2947,108 @@ func TestUser_MagicLink_PrimaryGlobalUniqueness(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_Authenticate_ByPrimaryEmail(t *testing.T) {
|
||||
forEachBackend(t, func(t *testing.T, newManager newManagerFunc) {
|
||||
a := newTestManager(t, newManager, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
|
||||
phil, err := a.User("phil")
|
||||
require.Nil(t, err)
|
||||
|
||||
// phil verifies phil@example.com -> becomes his primary (recovery) email
|
||||
_, err = a.VerifyEmail(addVerifyLink(t, a, phil.ID, "phil@example.com", 24*time.Hour))
|
||||
require.Nil(t, err)
|
||||
|
||||
// Login by username still works
|
||||
u, err := a.Authenticate("phil", "phil")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "phil", u.Name)
|
||||
|
||||
// Login by primary email works and resolves to the same account
|
||||
u, err = a.Authenticate("phil@example.com", "phil")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "phil", u.Name)
|
||||
|
||||
// Login by primary email with the wrong password fails
|
||||
u, err = a.Authenticate("phil@example.com", "wrong")
|
||||
require.Nil(t, u)
|
||||
require.Equal(t, ErrUnauthenticated, err)
|
||||
|
||||
// An unknown email fails
|
||||
u, err = a.Authenticate("nobody@example.com", "phil")
|
||||
require.Nil(t, u)
|
||||
require.Equal(t, ErrUnauthenticated, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_Authenticate_BySecondaryEmailDenied(t *testing.T) {
|
||||
forEachBackend(t, func(t *testing.T, newManager newManagerFunc) {
|
||||
a := newTestManager(t, newManager, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
phil, err := a.User("phil")
|
||||
require.Nil(t, err)
|
||||
ben, err := a.User("ben")
|
||||
require.Nil(t, err)
|
||||
|
||||
// phil verifies shared@ first -> his primary; ben verifies it too -> only secondary for ben
|
||||
_, err = a.VerifyEmail(addVerifyLink(t, a, phil.ID, "shared@example.com", 24*time.Hour))
|
||||
require.Nil(t, err)
|
||||
_, err = a.VerifyEmail(addVerifyLink(t, a, ben.ID, "shared@example.com", 24*time.Hour))
|
||||
require.Nil(t, err)
|
||||
|
||||
// Login by the shared address resolves to the primary owner (phil), never the secondary (ben)
|
||||
u, err := a.Authenticate("shared@example.com", "phil")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "phil", u.Name)
|
||||
|
||||
// ben's password must not authenticate via the shared address (it is not his primary)
|
||||
u, err = a.Authenticate("shared@example.com", "ben")
|
||||
require.Nil(t, u)
|
||||
require.Equal(t, ErrUnauthenticated, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_Authenticate_UsernameLookalikeEmailPrecedence(t *testing.T) {
|
||||
forEachBackend(t, func(t *testing.T, newManager newManagerFunc) {
|
||||
a := newTestManager(t, newManager, PermissionDenyAll)
|
||||
|
||||
// The collision: a squatter whose USERNAME is literally "phil@example.com" (usernames may
|
||||
// contain '@' and '.'), and a different account (ben) that owns "phil@example.com" as its
|
||||
// verified primary email. Both are reachable; nothing links usernames to email addresses.
|
||||
require.Nil(t, a.AddUser("phil@example.com", "squatterpass", RoleUser, false))
|
||||
require.Nil(t, a.AddUser("ben", "benpass", RoleUser, false))
|
||||
ben, err := a.User("ben")
|
||||
require.Nil(t, err)
|
||||
_, err = a.VerifyEmail(addVerifyLink(t, a, ben.ID, "phil@example.com", 24*time.Hour))
|
||||
require.Nil(t, err)
|
||||
|
||||
// Login resolves the ambiguous identifier username-FIRST (the ORDER BY CASE in the query):
|
||||
// the squatter owns the login, and returns deterministically even though both rows match.
|
||||
squatter, err := a.Authenticate("phil@example.com", "squatterpass")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "phil@example.com", squatter.Name)
|
||||
|
||||
// Consequently the email owner's password does NOT authenticate via the colliding identifier
|
||||
// at login, but the owner is not locked out: their real username still works.
|
||||
u, err := a.Authenticate("phil@example.com", "benpass")
|
||||
require.Nil(t, u)
|
||||
require.Equal(t, ErrUnauthenticated, err)
|
||||
u, err = a.Authenticate("ben", "benpass")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "ben", u.Name)
|
||||
|
||||
// The inverse: password reset (UserByEmailOrUsername) resolves the SAME identifier email-FIRST,
|
||||
// so the reset link goes to the verified email owner (ben), never the look-alike username. The
|
||||
// two flows deliberately use opposite precedence.
|
||||
loginUser, err := a.userByNameOrEmail("phil@example.com")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "phil@example.com", loginUser.Name) // username owner (squatter)
|
||||
resetUser, err := a.UserByEmailOrUsername("phil@example.com")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "ben", resetUser.Name) // email owner
|
||||
})
|
||||
}
|
||||
|
||||
func TestUser_MagicLink_SetPrimary_NotVerified(t *testing.T) {
|
||||
forEachBackend(t, func(t *testing.T, newManager newManagerFunc) {
|
||||
a := newTestManager(t, newManager, PermissionDenyAll)
|
||||
|
||||
+26
-25
@@ -47,10 +47,10 @@ func (u *User) IsUser() bool {
|
||||
|
||||
// Auther is an interface for authentication and authorization
|
||||
type Auther interface {
|
||||
// Authenticate checks username and password and returns a user if correct. The method
|
||||
// returns in constant-ish time, regardless of whether the user exists or the password is
|
||||
// correct or incorrect.
|
||||
Authenticate(username, password string) (*User, error)
|
||||
// Authenticate checks a login identifier (username or verified primary email) and password
|
||||
// and returns a user if correct. The method returns in constant-ish time, regardless of
|
||||
// whether the identifier exists or the password is correct or incorrect.
|
||||
Authenticate(identifier, password string) (*User, error)
|
||||
|
||||
// Authorize returns nil if the given user has access to the given topic using the desired
|
||||
// permission. The user param may be nil to signal an anonymous user.
|
||||
@@ -338,27 +338,28 @@ var (
|
||||
// queries holds the database-specific SQL queries
|
||||
type queries struct {
|
||||
// User queries
|
||||
selectUserByID string
|
||||
selectUserByName string
|
||||
selectUserByToken string
|
||||
selectUserByStripeCustomerID string
|
||||
selectUsernames string
|
||||
selectUsers string
|
||||
selectUserCount string
|
||||
selectUserIDFromUsername string
|
||||
insertUser string
|
||||
updateUserPass string
|
||||
updateUserRole string
|
||||
updateUserProvisioned string
|
||||
updateUserPrefs string
|
||||
updateUserStats string
|
||||
updateUserStatsResetAll string
|
||||
updateUserTier string
|
||||
updateUserDeleted string
|
||||
deleteUser string
|
||||
deleteUserTier string
|
||||
deleteUsersMarked string
|
||||
deleteUsersProvisioned string
|
||||
selectUserByID string
|
||||
selectUserByName string
|
||||
selectUserByNameOrPrimaryEmail string
|
||||
selectUserByToken string
|
||||
selectUserByStripeCustomerID string
|
||||
selectUsernames string
|
||||
selectUsers string
|
||||
selectUserCount string
|
||||
selectUserIDFromUsername string
|
||||
insertUser string
|
||||
updateUserPass string
|
||||
updateUserRole string
|
||||
updateUserProvisioned string
|
||||
updateUserPrefs string
|
||||
updateUserStats string
|
||||
updateUserStatsResetAll string
|
||||
updateUserTier string
|
||||
updateUserDeleted string
|
||||
deleteUser string
|
||||
deleteUserTier string
|
||||
deleteUsersMarked string
|
||||
deleteUsersProvisioned string
|
||||
|
||||
// Access queries
|
||||
selectTopicPerms string // Direct-DB authorizeTopicAccess query; used when the in-memory cache is disabled
|
||||
|
||||
Reference in New Issue
Block a user