authelia/internal/storage/sql_provider_schema_pre1.go
James Elliott ad8e844af6
feat(totp): algorithm and digits config (#2634)
Allow users to configure the TOTP Algorithm and Digits. This should be used with caution as many TOTP applications do not support it. Some will also fail to notify the user that there is an issue. i.e. if the algorithm in the QR code is sha512, they continue to generate one time passwords with sha1. In addition this drastically refactors TOTP in general to be more user friendly by not forcing them to register a new device if the administrator changes the period (or algorithm).

Fixes #1226.
2021-12-01 23:11:29 +11:00

461 lines
11 KiB
Go

package storage
import (
"context"
"database/sql"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/utils"
)
// schemaMigratePre1To1 takes the v1 migration and migrates to this version.
func (p *SQLProvider) schemaMigratePre1To1(ctx context.Context) (err error) {
migration, err := loadMigration(p.name, 1, true)
if err != nil {
return err
}
// Get Tables list.
tables, err := p.SchemaTables(ctx)
if err != nil {
return err
}
tablesRename := []string{
tablePre1Config,
tablePre1TOTPSecrets,
tablePre1IdentityVerificationTokens,
tableU2FDevices,
tableUserPreferences,
tableAuthenticationLogs,
tableAlphaPreferences,
tableAlphaIdentityVerificationTokens,
tableAlphaAuthenticationLogs,
tableAlphaPreferencesTableName,
tableAlphaSecondFactorPreferences,
tableAlphaTOTPSecrets,
tableAlphaU2FDeviceHandles,
}
if err = p.schemaMigratePre1Rename(ctx, tables, tablesRename); err != nil {
return err
}
if _, err = p.db.ExecContext(ctx, migration.Query); err != nil {
return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err)
}
if err = p.setNewEncryptionCheckValue(ctx, &p.key, nil); err != nil {
return err
}
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1InsertUserPreferencesFromSelect),
tableUserPreferences, tablePrefixBackup+tableUserPreferences)); err != nil {
return err
}
if err = p.schemaMigratePre1To1AuthenticationLogs(ctx); err != nil {
return err
}
if err = p.schemaMigratePre1To1U2F(ctx); err != nil {
return err
}
if err = p.schemaMigratePre1To1TOTP(ctx); err != nil {
return err
}
for _, table := range tablesRename {
if _, err = p.db.Exec(fmt.Sprintf(p.db.Rebind(queryFmtDropTableIfExists), tablePrefixBackup+table)); err != nil {
return err
}
}
return p.schemaMigrateFinalizeAdvanced(ctx, -1, 1)
}
func (p *SQLProvider) schemaMigratePre1Rename(ctx context.Context, tables, tablesRename []string) (err error) {
// Rename Tables and Indexes.
for _, table := range tables {
if !utils.IsStringInSlice(table, tablesRename) {
continue
}
tableNew := tablePrefixBackup + table
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.sqlFmtRenameTable, table, tableNew)); err != nil {
return err
}
if p.name == providerPostgres {
if table == tableU2FDevices || table == tableUserPreferences {
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(`ALTER TABLE %s RENAME CONSTRAINT %s_pkey TO %s_pkey;`,
tableNew, table, tableNew)); err != nil {
continue
}
}
}
}
return nil
}
func (p *SQLProvider) schemaMigratePre1To1Rollback(ctx context.Context, up bool) (err error) {
if up {
migration, err := loadMigration(p.name, 1, false)
if err != nil {
return err
}
if _, err = p.db.ExecContext(ctx, migration.Query); err != nil {
return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err)
}
}
tables, err := p.SchemaTables(ctx)
if err != nil {
return err
}
for _, table := range tables {
if !strings.HasPrefix(table, tablePrefixBackup) {
continue
}
tableNew := strings.Replace(table, tablePrefixBackup, "", 1)
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.sqlFmtRenameTable, table, tableNew)); err != nil {
return err
}
if p.name == providerPostgres && (tableNew == tableU2FDevices || tableNew == tableUserPreferences) {
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(`ALTER TABLE %s RENAME CONSTRAINT %s_pkey TO %s_pkey;`,
tableNew, table, tableNew)); err != nil {
continue
}
}
}
return nil
}
func (p *SQLProvider) schemaMigratePre1To1AuthenticationLogs(ctx context.Context) (err error) {
for page := 0; true; page++ {
attempts, err := p.schemaMigratePre1To1AuthenticationLogsGetRows(ctx, page)
if err != nil {
if err == sql.ErrNoRows {
break
}
return err
}
for _, attempt := range attempts {
_, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1To1InsertAuthenticationLogs), tableAuthenticationLogs), attempt.Username, attempt.Successful, attempt.Time)
if err != nil {
return err
}
}
if len(attempts) != 100 {
break
}
}
return nil
}
func (p *SQLProvider) schemaMigratePre1To1AuthenticationLogsGetRows(ctx context.Context, page int) (attempts []models.AuthenticationAttempt, err error) {
rows, err := p.db.QueryxContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1To1SelectAuthenticationLogs), tablePrefixBackup+tableAuthenticationLogs), page*100)
if err != nil {
return nil, err
}
attempts = make([]models.AuthenticationAttempt, 0, 100)
for rows.Next() {
var (
username string
successful bool
timestamp int64
)
err = rows.Scan(&username, &successful, &timestamp)
if err != nil {
return nil, err
}
attempts = append(attempts, models.AuthenticationAttempt{Username: username, Successful: successful, Time: time.Unix(timestamp, 0)})
}
return attempts, nil
}
func (p *SQLProvider) schemaMigratePre1To1TOTP(ctx context.Context) (err error) {
rows, err := p.db.QueryxContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1SelectTOTPConfigurations), tablePrefixBackup+tablePre1TOTPSecrets))
if err != nil {
return err
}
var totpConfigs []models.TOTPConfiguration
defer func() {
if err := rows.Close(); err != nil {
p.log.Errorf(logFmtErrClosingConn, err)
}
}()
for rows.Next() {
var username, secret string
err = rows.Scan(&username, &secret)
if err != nil {
return err
}
encryptedSecret, err := p.encrypt([]byte(secret))
if err != nil {
return err
}
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: encryptedSecret})
}
for _, config := range totpConfigs {
_, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1To1InsertTOTPConfiguration), tableTOTPConfigurations), config.Username, p.config.TOTP.Issuer, p.config.TOTP.Period, config.Secret)
if err != nil {
return err
}
}
return nil
}
func (p *SQLProvider) schemaMigratePre1To1U2F(ctx context.Context) (err error) {
rows, err := p.db.Queryx(fmt.Sprintf(p.db.Rebind(queryFmtPre1To1SelectU2FDevices), tablePrefixBackup+tableU2FDevices))
if err != nil {
return err
}
defer func() {
if err := rows.Close(); err != nil {
p.log.Errorf(logFmtErrClosingConn, err)
}
}()
var devices []models.U2FDevice
for rows.Next() {
var username, keyHandleBase64, publicKeyBase64 string
err = rows.Scan(&username, &keyHandleBase64, &publicKeyBase64)
if err != nil {
return err
}
keyHandle, err := base64.StdEncoding.DecodeString(keyHandleBase64)
if err != nil {
return err
}
publicKey, err := base64.StdEncoding.DecodeString(publicKeyBase64)
if err != nil {
return err
}
devices = append(devices, models.U2FDevice{Username: username, KeyHandle: keyHandle, PublicKey: publicKey})
}
for _, device := range devices {
_, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1To1InsertU2FDevice), tableU2FDevices), device.Username, device.KeyHandle, device.PublicKey)
if err != nil {
return err
}
}
return nil
}
func (p *SQLProvider) schemaMigrate1ToPre1(ctx context.Context) (err error) {
tables, err := p.SchemaTables(ctx)
if err != nil {
return err
}
tablesRename := []string{
tableMigrations,
tableTOTPConfigurations,
tableIdentityVerification,
tableU2FDevices,
tableDuoDevices,
tableUserPreferences,
tableAuthenticationLogs,
tableEncryption,
}
if err = p.schemaMigratePre1Rename(ctx, tables, tablesRename); err != nil {
return err
}
if _, err := p.db.ExecContext(ctx, queryCreatePre1); err != nil {
return err
}
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1InsertUserPreferencesFromSelect),
tableUserPreferences, tablePrefixBackup+tableUserPreferences)); err != nil {
return err
}
if err = p.schemaMigrate1ToPre1AuthenticationLogs(ctx); err != nil {
return err
}
if err = p.schemaMigrate1ToPre1U2F(ctx); err != nil {
return err
}
if err = p.schemaMigrate1ToPre1TOTP(ctx); err != nil {
return err
}
queryFmtDropTableRebound := p.db.Rebind(queryFmtDropTableIfExists)
for _, table := range tablesRename {
if _, err = p.db.Exec(fmt.Sprintf(queryFmtDropTableRebound, tablePrefixBackup+table)); err != nil {
return err
}
}
return nil
}
func (p *SQLProvider) schemaMigrate1ToPre1AuthenticationLogs(ctx context.Context) (err error) {
for page := 0; true; page++ {
attempts, err := p.schemaMigrate1ToPre1AuthenticationLogsGetRows(ctx, page)
if err != nil {
if err == sql.ErrNoRows {
break
}
return err
}
for _, attempt := range attempts {
_, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmt1ToPre1InsertAuthenticationLogs), tableAuthenticationLogs), attempt.Username, attempt.Successful, attempt.Time.Unix())
if err != nil {
return err
}
}
if len(attempts) != 100 {
break
}
}
return nil
}
func (p *SQLProvider) schemaMigrate1ToPre1AuthenticationLogsGetRows(ctx context.Context, page int) (attempts []models.AuthenticationAttempt, err error) {
rows, err := p.db.QueryxContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmt1ToPre1SelectAuthenticationLogs), tablePrefixBackup+tableAuthenticationLogs), page*100)
if err != nil {
return nil, err
}
attempts = make([]models.AuthenticationAttempt, 0, 100)
var attempt models.AuthenticationAttempt
for rows.Next() {
err = rows.StructScan(&attempt)
if err != nil {
return nil, err
}
attempts = append(attempts, attempt)
}
return attempts, nil
}
func (p *SQLProvider) schemaMigrate1ToPre1TOTP(ctx context.Context) (err error) {
rows, err := p.db.QueryxContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1SelectTOTPConfigurations), tablePrefixBackup+tableTOTPConfigurations))
if err != nil {
return err
}
var totpConfigs []models.TOTPConfiguration
defer func() {
if err := rows.Close(); err != nil {
p.log.Errorf(logFmtErrClosingConn, err)
}
}()
for rows.Next() {
var (
username string
secretCipherText []byte
)
err = rows.Scan(&username, &secretCipherText)
if err != nil {
return err
}
secretClearText, err := p.decrypt(secretCipherText)
if err != nil {
return err
}
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: secretClearText})
}
for _, config := range totpConfigs {
_, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmt1ToPre1InsertTOTPConfiguration), tablePre1TOTPSecrets), config.Username, config.Secret)
if err != nil {
return err
}
}
return nil
}
func (p *SQLProvider) schemaMigrate1ToPre1U2F(ctx context.Context) (err error) {
rows, err := p.db.QueryxContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmt1ToPre1SelectU2FDevices), tablePrefixBackup+tableU2FDevices))
if err != nil {
return err
}
defer func() {
if err := rows.Close(); err != nil {
p.log.Errorf(logFmtErrClosingConn, err)
}
}()
var (
devices []models.U2FDevice
device models.U2FDevice
)
for rows.Next() {
err = rows.StructScan(&device)
if err != nil {
return err
}
devices = append(devices, device)
}
for _, device := range devices {
_, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmt1ToPre1InsertU2FDevice), tableU2FDevices), device.Username, base64.StdEncoding.EncodeToString(device.KeyHandle), base64.StdEncoding.EncodeToString(device.PublicKey))
if err != nil {
return err
}
}
return nil
}