feat(commands): totp qr code in png format (#2673)

This allows exporting the TOTP QR code for easy registration when using `authelia storage totp generate` or `authelia storage totp export`.
This commit is contained in:
James Elliott 2022-03-02 18:50:36 +11:00 committed by GitHub
parent 6276883f04
commit 1b2af90e5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 333 additions and 134 deletions

View File

@ -105,6 +105,7 @@ const (
const (
storageExportFormatCSV = "csv"
storageExportFormatURI = "uri"
storageExportFormatPNG = "png"
)
var (

View File

@ -112,6 +112,7 @@ func newStorageTOTPGenerateCmd() (cmd *cobra.Command) {
cmd.Flags().String("algorithm", "SHA1", "set the TOTP algorithm")
cmd.Flags().String("issuer", "Authelia", "set the TOTP issuer")
cmd.Flags().BoolP("force", "f", false, "forces the TOTP configuration to be generated regardless if it exists or not")
cmd.Flags().StringP("path", "p", "", "path to a file to create a PNG file with the QR code (optional)")
return cmd
}
@ -135,6 +136,7 @@ func newStorageTOTPExportCmd() (cmd *cobra.Command) {
}
cmd.Flags().String("format", storageExportFormatURI, "sets the output format")
cmd.Flags().String("dir", "", "used with the png output format to specify which new directory to save the files in")
return cmd
}

View File

@ -4,7 +4,10 @@ import (
"context"
"errors"
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
@ -15,11 +18,13 @@ import (
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/storage"
"github.com/authelia/authelia/v4/internal/totp"
"github.com/authelia/authelia/v4/internal/utils"
)
func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
configs, err := cmd.Flags().GetStringSlice("config")
if err != nil {
var configs []string
if configs, err = cmd.Flags().GetStringSlice("config"); err != nil {
return err
}
@ -72,8 +77,7 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
config = &schema.Configuration{}
_, err = configuration.LoadAdvanced(val, "", &config, sources...)
if err != nil {
if _, err = configuration.LoadAdvanced(val, "", &config, sources...); err != nil {
return err
}
@ -117,7 +121,9 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
func storageSchemaEncryptionCheckRunE(cmd *cobra.Command, args []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
verbose bool
ctx = context.Background()
)
provider = getStorageProvider()
@ -126,8 +132,7 @@ func storageSchemaEncryptionCheckRunE(cmd *cobra.Command, args []string) (err er
_ = provider.Close()
}()
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
if verbose, err = cmd.Flags().GetBool("verbose"); err != nil {
return err
}
@ -150,7 +155,10 @@ func storageSchemaEncryptionCheckRunE(cmd *cobra.Command, args []string) (err er
func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
key string
version int
ctx = context.Background()
)
provider = getStorageProvider()
@ -163,8 +171,7 @@ func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (er
return err
}
version, err := provider.SchemaVersion(ctx)
if err != nil {
if version, err = provider.SchemaVersion(ctx); err != nil {
return err
}
@ -172,17 +179,15 @@ func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (er
return errors.New("schema version must be at least version 1 to change the encryption key")
}
key, err := cmd.Flags().GetString("new-encryption-key")
if err != nil {
key, err = cmd.Flags().GetString("new-encryption-key")
switch {
case err != nil:
return err
}
if key == "" {
case key == "":
return errors.New("you must set the --new-encryption-key flag")
}
if len(key) < 20 {
return errors.New("the encryption key must be at least 20 characters")
case len(key) < 20:
return errors.New("the new encryption key must be at least 20 characters")
}
if err = provider.SchemaEncryptionChangeKey(ctx, key); err != nil {
@ -200,6 +205,9 @@ func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
ctx = context.Background()
c *models.TOTPConfiguration
force bool
filename string
file *os.File
img image.Image
)
provider = getStorageProvider()
@ -208,10 +216,15 @@ func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
_ = provider.Close()
}()
force, err = cmd.Flags().GetBool("force")
if force, err = cmd.Flags().GetBool("force"); err != nil {
return err
}
_, err = provider.LoadTOTPConfiguration(ctx, args[0])
if err == nil && !force {
if filename, err = cmd.Flags().GetString("path"); err != nil {
return err
}
if _, err = provider.LoadTOTPConfiguration(ctx, args[0]); err == nil && !force {
return fmt.Errorf("%s already has a TOTP configuration, use --force to overwrite", args[0])
}
@ -225,12 +238,35 @@ func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {
return err
}
err = provider.SaveTOTPConfiguration(ctx, *c)
if err != nil {
extraInfo := ""
if filename != "" {
if _, err = os.Stat(filename); !os.IsNotExist(err) {
return errors.New("image output filepath already exists")
}
if file, err = os.Create(filename); err != nil {
return err
}
defer file.Close()
if img, err = c.Image(256, 256); err != nil {
return err
}
if err = png.Encode(file, img); err != nil {
return err
}
extraInfo = fmt.Sprintf(" and saved it as a PNG image at the path '%s'", filename)
}
if err = provider.SaveTOTPConfiguration(ctx, *c); err != nil {
return err
}
fmt.Printf("Generated TOTP configuration for user '%s': %s", args[0], c.URI())
fmt.Printf("Generated TOTP configuration for user '%s' with URI '%s'%s\n", args[0], c.URI(), extraInfo)
return nil
}
@ -249,13 +285,11 @@ func storageTOTPDeleteRunE(cmd *cobra.Command, args []string) (err error) {
_ = provider.Close()
}()
_, err = provider.LoadTOTPConfiguration(ctx, user)
if err != nil {
if _, err = provider.LoadTOTPConfiguration(ctx, user); err != nil {
return fmt.Errorf("can't delete configuration for user '%s': %+v", user, err)
}
err = provider.DeleteTOTPConfiguration(ctx, user)
if err != nil {
if err = provider.DeleteTOTPConfiguration(ctx, user); err != nil {
return fmt.Errorf("can't delete configuration for user '%s': %+v", user, err)
}
@ -266,8 +300,12 @@ func storageTOTPDeleteRunE(cmd *cobra.Command, args []string) (err error) {
func storageTOTPExportRunE(cmd *cobra.Command, args []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
provider storage.Provider
format, dir string
configurations []models.TOTPConfiguration
img image.Image
ctx = context.Background()
)
provider = getStorageProvider()
@ -280,25 +318,14 @@ func storageTOTPExportRunE(cmd *cobra.Command, args []string) (err error) {
return err
}
format, err := cmd.Flags().GetString("format")
if err != nil {
if format, dir, err = storageTOTPExportGetConfigFromFlags(cmd); err != nil {
return err
}
switch format {
case storageExportFormatCSV, storageExportFormatURI:
break
default:
return errors.New("format must be csv or uri")
}
limit := 10
var configurations []models.TOTPConfiguration
for page := 0; true; page++ {
configurations, err = provider.LoadTOTPConfigurations(ctx, limit, page)
if err != nil {
if configurations, err = provider.LoadTOTPConfigurations(ctx, limit, page); err != nil {
return err
}
@ -312,6 +339,17 @@ func storageTOTPExportRunE(cmd *cobra.Command, args []string) (err error) {
fmt.Printf("%s,%s,%s,%d,%d,%s\n", c.Issuer, c.Username, c.Algorithm, c.Digits, c.Period, string(c.Secret))
case storageExportFormatURI:
fmt.Println(c.URI())
case storageExportFormatPNG:
file, _ := os.Create(filepath.Join(dir, fmt.Sprintf("%s.png", c.Username)))
defer file.Close()
if img, err = c.Image(256, 256); err != nil {
return err
}
if err = png.Encode(file, img); err != nil {
return err
}
}
}
@ -320,13 +358,51 @@ func storageTOTPExportRunE(cmd *cobra.Command, args []string) (err error) {
}
}
if format == storageExportFormatPNG {
fmt.Printf("Exported TOTP QR codes in PNG format in the '%s' directory\n", dir)
}
return nil
}
func storageTOTPExportGetConfigFromFlags(cmd *cobra.Command) (format, dir string, err error) {
if format, err = cmd.Flags().GetString("format"); err != nil {
return "", "", err
}
if dir, err = cmd.Flags().GetString("dir"); err != nil {
return "", "", err
}
switch format {
case storageExportFormatCSV, storageExportFormatURI:
break
case storageExportFormatPNG:
if dir == "" {
dir = utils.RandomString(8, utils.AlphaNumericCharacters, false)
}
if _, err = os.Stat(dir); !os.IsNotExist(err) {
return "", "", errors.New("output directory must not exist")
}
if err = os.MkdirAll(dir, 0700); err != nil {
return "", "", err
}
default:
return "", "", errors.New("format must be csv, uri, or png")
}
return format, dir, nil
}
func storageMigrateHistoryRunE(_ *cobra.Command, _ []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
provider storage.Provider
version int
migrations []models.Migration
ctx = context.Background()
)
provider = getStorageProvider()
@ -338,8 +414,7 @@ func storageMigrateHistoryRunE(_ *cobra.Command, _ []string) (err error) {
_ = provider.Close()
}()
version, err := provider.SchemaVersion(ctx)
if err != nil {
if version, err = provider.SchemaVersion(ctx); err != nil {
return err
}
@ -348,8 +423,7 @@ func storageMigrateHistoryRunE(_ *cobra.Command, _ []string) (err error) {
return
}
migrations, err := provider.SchemaMigrationHistory(ctx)
if err != nil {
if migrations, err = provider.SchemaMigrationHistory(ctx); err != nil {
return err
}
@ -411,7 +485,10 @@ func newStorageMigrationRunE(up bool) func(cmd *cobra.Command, args []string) (e
return func(cmd *cobra.Command, args []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
target int
pre1 bool
ctx = context.Background()
)
provider = getStorageProvider()
@ -420,8 +497,7 @@ func newStorageMigrationRunE(up bool) func(cmd *cobra.Command, args []string) (e
_ = provider.Close()
}()
target, err := cmd.Flags().GetInt("target")
if err != nil {
if target, err = cmd.Flags().GetInt("target"); err != nil {
return err
}
@ -434,8 +510,7 @@ func newStorageMigrationRunE(up bool) func(cmd *cobra.Command, args []string) (e
return provider.SchemaMigrate(ctx, true, storage.SchemaLatest)
}
default:
pre1, err := cmd.Flags().GetBool("pre1")
if err != nil {
if pre1, err = cmd.Flags().GetBool("pre1"); err != nil {
return err
}
@ -458,8 +533,9 @@ func newStorageMigrationRunE(up bool) func(cmd *cobra.Command, args []string) (e
}
func storageMigrateDownConfirmDestroy(cmd *cobra.Command) (err error) {
destroy, err := cmd.Flags().GetBool("destroy-data")
if err != nil {
var destroy bool
if destroy, err = cmd.Flags().GetBool("destroy-data"); err != nil {
return err
}
@ -480,10 +556,13 @@ func storageMigrateDownConfirmDestroy(cmd *cobra.Command) (err error) {
func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
var (
provider storage.Provider
ctx = context.Background()
upgradeStr string
tablesStr string
upgradeStr, tablesStr string
provider storage.Provider
tables []string
version, latest int
ctx = context.Background()
)
provider = getStorageProvider()
@ -492,13 +571,11 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
_ = provider.Close()
}()
version, err := provider.SchemaVersion(ctx)
if err != nil && err.Error() != "unknown schema state" {
if version, err = provider.SchemaVersion(ctx); err != nil && err.Error() != "unknown schema state" {
return err
}
tables, err := provider.SchemaTables(ctx)
if err != nil {
if tables, err = provider.SchemaTables(ctx); err != nil {
return err
}
@ -508,8 +585,7 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
tablesStr = strings.Join(tables, ", ")
}
latest, err := provider.SchemaLatestVersion()
if err != nil {
if latest, err = provider.SchemaLatestVersion(); err != nil {
return err
}
@ -537,13 +613,13 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
}
func checkStorageSchemaUpToDate(ctx context.Context, provider storage.Provider) (err error) {
version, err := provider.SchemaVersion(ctx)
if err != nil {
var version, latest int
if version, err = provider.SchemaVersion(ctx); err != nil {
return err
}
latest, err := provider.SchemaLatestVersion()
if err != nil {
if latest, err = provider.SchemaLatestVersion(); err != nil {
return err
}

View File

@ -1,8 +1,11 @@
package models
import (
"image"
"net/url"
"strconv"
"github.com/pquerna/otp"
)
// TOTPConfiguration represents a users TOTP configuration row in the database.
@ -34,3 +37,19 @@ func (c TOTPConfiguration) URI() (uri string) {
return u.String()
}
// Key returns the *otp.Key using TOTPConfiguration.URI with otp.NewKeyFromURL.
func (c TOTPConfiguration) Key() (key *otp.Key, err error) {
return otp.NewKeyFromURL(c.URI())
}
// Image returns the image.Image of the TOTPConfiguration using the Image func from the return of TOTPConfiguration.Key.
func (c TOTPConfiguration) Image(width, height int) (img image.Image, err error) {
var key *otp.Key
if key, err = c.Key(); err != nil {
return nil, err
}
return key.Image(width, height)
}

View File

@ -38,3 +38,40 @@ func TestShouldOnlyMarshalPeriodAndDigitsAndAbsolutelyNeverSecret(t *testing.T)
require.NotContains(t, string(data), "secret")
require.NotContains(t, string(data), "ABC123")
}
func TestShouldReturnErrWhenImageTooSmall(t *testing.T) {
object := TOTPConfiguration{
ID: 1,
Username: "john",
Issuer: "Authelia",
Algorithm: "SHA1",
Digits: 6,
Period: 30,
Secret: []byte("ABC123"),
}
img, err := object.Image(10, 10)
assert.EqualError(t, err, "can not scale barcode to an image smaller than 41x41")
assert.Nil(t, img)
}
func TestShouldReturnImage(t *testing.T) {
object := TOTPConfiguration{
ID: 1,
Username: "john",
Issuer: "Authelia",
Algorithm: "SHA1",
Digits: 6,
Period: 30,
Secret: []byte("ABC123"),
}
img, err := object.Image(41, 41)
assert.NoError(t, err)
require.NotNil(t, img)
assert.Equal(t, 41, img.Bounds().Dx())
assert.Equal(t, 41, img.Bounds().Dy())
}

View File

@ -38,6 +38,8 @@ func init() {
err := dockerEnvironment.Down()
_ = os.Remove("/tmp/db.sqlite3")
_ = os.Remove("/tmp/db.sqlite")
_ = os.RemoveAll("/tmp/qr/")
_ = os.Remove("/tmp/qr.png")
return err
}

View File

@ -66,13 +66,13 @@ func (s *CLISuite) TestShouldPrintVersion() {
}
func (s *CLISuite) TestShouldValidateConfig() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "--config", "/config/configuration.yml"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "--config=/config/configuration.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Configuration parsed and loaded successfully without errors.")
}
func (s *CLISuite) TestShouldFailValidateConfig() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "--config", "/config/invalid.yml"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "--config=/config/invalid.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "failed to load configuration from yaml file(/config/invalid.yml) source: open /config/invalid.yml: no such file or directory")
}
@ -90,75 +90,75 @@ func (s *CLISuite) TestShouldHashPasswordSHA512() {
}
func (s *CLISuite) TestShouldGenerateCertificateRSA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateRSAWithIPAddress() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "127.0.0.1", "--dir", "/tmp/"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=127.0.0.1", "--dir=/tmp/"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateRSAWithStartDate() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--start-date", "'Jan 1 15:04:05 2011'"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--start-date='Jan 1 15:04:05 2011'"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldFailGenerateCertificateRSAWithStartDate() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--start-date", "Jan"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--start-date=Jan"})
s.Assert().NotNil(err)
s.Assert().Contains(output, "Failed to parse start date: parsing time \"Jan\" as \"Jan 2 15:04:05 2006\": cannot parse \"\" as \"2\"")
}
func (s *CLISuite) TestShouldGenerateCertificateCA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ca"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ca"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateEd25519() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ed25519"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ed25519"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldFailGenerateCertificateECDSA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "invalid"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ecdsa-curve=invalid"})
s.Assert().NotNil(err)
s.Assert().Contains(output, "Failed to generate private key: unrecognized elliptic curve: \"invalid\"")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP224() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P224"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ecdsa-curve=P224"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP256() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P256"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ecdsa-curve=P256"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP384() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P384"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ecdsa-curve=P384"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP521() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P521"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host=*.example.com", "--dir=/tmp/", "--ecdsa-curve=P521"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
@ -179,7 +179,7 @@ func (s *CLISuite) TestStorageShouldShowErrWithoutConfig() {
func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() {
_ = os.Remove("/tmp/db.sqlite3")
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
pattern := regexp.MustCompile(`^Schema Version: N/A\nSchema Upgrade Available: yes - version \d+\nSchema Tables: N/A\nSchema Encryption Key: unsupported \(schema version\)`)
@ -188,45 +188,45 @@ func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() {
patternOutdated := regexp.MustCompile(`Error: schema is version \d+ which is outdated please migrate to version \d+ in order to use this command or use an older binary`)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Regexp(patternOutdated, output)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Regexp(patternOutdated, output)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Could not check encryption key for validity. The schema version doesn't support encryption.")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target", "0", "--destroy-data", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target=0", "--destroy-data", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: schema migration target version 0 is the same current version 0")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "up", "--target", "2147483640", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "up", "--target=2147483640", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: schema up migration target version 2147483640 is greater then the latest version ")
s.Assert().Contains(output, " which indicates it doesn't exist")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "history"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "history", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "No migration history is available for schemas that not version 1 or above.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-up"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "list-up", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Storage Schema Migration List (Up)\n\nVersion\t\tDescription\n1\t\tInitial Schema\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-down"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "list-down", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Storage Schema Migration List (Down)\n\nNo Migrations Available\n")
}
func (s *CLISuite) TestStorage01ShouldMigrateUp() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "up"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "up", "--config=/config/configuration.storage.yml"})
s.Require().NoError(err)
pattern0 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is being attempted"`)
@ -235,23 +235,23 @@ func (s *CLISuite) TestStorage01ShouldMigrateUp() {
s.Regexp(pattern0, output)
s.Regexp(pattern1, output)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "up"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "up", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: schema already up to date\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "history"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "history", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Migration History:\n\nID\tDate\t\t\t\tBefore\tAfter\tAuthelia Version\n")
s.Assert().Contains(output, "0\t1")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-up"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "list-up", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Storage Schema Migration List (Up)\n\nNo Migrations Available")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-down"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "list-down", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Storage Schema Migration List (Down)\n\nVersion\t\tDescription\n")
@ -259,7 +259,7 @@ func (s *CLISuite) TestStorage01ShouldMigrateUp() {
}
func (s *CLISuite) TestStorage02ShouldShowSchemaInfo() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
pattern := regexp.MustCompile(`^Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification, totp_configurations, u2f_devices, duo_devices, user_preferences, migrations, encryption\nSchema Encryption Key: valid`)
@ -284,95 +284,157 @@ func (s *CLISuite) TestStorage03ShouldExportTOTP() {
expectedLinesCSV = append(expectedLinesCSV, "issuer,username,algorithm,digits,period,secret")
configs := []*models.TOTPConfiguration{
testCases := []struct {
config models.TOTPConfiguration
png bool
}{
{
Username: "john",
Period: 30,
Digits: 6,
Algorithm: "SHA1",
config: models.TOTPConfiguration{
Username: "john",
Period: 30,
Digits: 6,
Algorithm: "SHA1",
},
},
{
Username: "mary",
Period: 45,
Digits: 6,
Algorithm: "SHA1",
config: models.TOTPConfiguration{
Username: "mary",
Period: 45,
Digits: 6,
Algorithm: "SHA1",
},
},
{
Username: "fred",
Period: 30,
Digits: 8,
Algorithm: "SHA1",
config: models.TOTPConfiguration{
Username: "fred",
Period: 30,
Digits: 8,
Algorithm: "SHA1",
},
},
{
Username: "jone",
Period: 30,
Digits: 6,
Algorithm: "SHA512",
config: models.TOTPConfiguration{
Username: "jone",
Period: 30,
Digits: 6,
Algorithm: "SHA512",
},
png: true,
},
}
for _, config := range configs {
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "generate", config.Username, "--period", strconv.Itoa(int(config.Period)), "--algorithm", config.Algorithm, "--digits", strconv.Itoa(int(config.Digits)), "--config", "/config/configuration.storage.yml"})
var (
config *models.TOTPConfiguration
fileInfo os.FileInfo
)
for _, testCase := range testCases {
if testCase.png {
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "generate", testCase.config.Username, "--period", strconv.Itoa(int(testCase.config.Period)), "--algorithm", testCase.config.Algorithm, "--digits", strconv.Itoa(int(testCase.config.Digits)), "--path=/tmp/qr.png", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, " and saved it as a PNG image at the path '/tmp/qr.png'")
fileInfo, err = os.Stat("/tmp/qr.png")
s.Assert().NoError(err)
s.Require().NotNil(fileInfo)
s.Assert().False(fileInfo.IsDir())
s.Assert().Greater(fileInfo.Size(), int64(1000))
} else {
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "generate", testCase.config.Username, "--period", strconv.Itoa(int(testCase.config.Period)), "--algorithm", testCase.config.Algorithm, "--digits", strconv.Itoa(int(testCase.config.Digits)), "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
}
config, err = storageProvider.LoadTOTPConfiguration(ctx, testCase.config.Username)
s.Assert().NoError(err)
config, err = storageProvider.LoadTOTPConfiguration(ctx, config.Username)
s.Assert().NoError(err)
s.Assert().Contains(output, config.URI())
expectedLinesCSV = append(expectedLinesCSV, fmt.Sprintf("%s,%s,%s,%d,%d,%s", "Authelia", config.Username, config.Algorithm, config.Digits, config.Period, string(config.Secret)))
expectedLines = append(expectedLines, config.URI())
}
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format", "uri", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format=uri", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
for _, expectedLine := range expectedLines {
s.Assert().Contains(output, expectedLine)
}
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format", "csv", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format=csv", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
for _, expectedLine := range expectedLinesCSV {
s.Assert().Contains(output, expectedLine)
}
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format=wrong", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: format must be csv, uri, or png")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "export", "--format=png", "--dir=/tmp/qr", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Exported TOTP QR codes in PNG format in the '/tmp/qr' directory")
for _, testCase := range testCases {
fileInfo, err = os.Stat(fmt.Sprintf("/tmp/qr/%s.png", testCase.config.Username))
s.Assert().NoError(err)
s.Require().NotNil(fileInfo)
s.Assert().False(fileInfo.IsDir())
s.Assert().Greater(fileInfo.Size(), int64(1000))
}
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "totp", "generate", "test", "--period=30", "--algorithm=SHA1", "--digits=6", "--path=/tmp/qr.png", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: image output filepath already exists")
}
func (s *CLISuite) TestStorage04ShouldChangeEncryptionKey() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--new-encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--new-encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Completed the encryption key change. Please adjust your configuration to use the new key.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
pattern := regexp.MustCompile(`Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification, totp_configurations, u2f_devices, duo_devices, user_preferences, migrations, encryption\nSchema Encryption Key: invalid`)
s.Assert().Regexp(pattern, output)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Encryption key validation: failed.\n\nError: the encryption key is not valid against the schema check value.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Encryption key validation: failed.\n\nError: the encryption key is not valid against the schema check value, 4 of 4 total TOTP secrets were invalid.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Encryption key validation: success.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
s.Assert().Contains(output, "Encryption key validation: success.\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: you must set the --new-encryption-key flag\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--encryption-key=apple-apple-apple-apple", "--new-encryption-key=abc", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: the new encryption key must be at least 20 characters\n")
}
func (s *CLISuite) TestStorage05ShouldMigrateDown() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target", "0", "--destroy-data", "--config", "/config/configuration.storage.yml"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target=0", "--destroy-data", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err)
pattern0 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is being attempted"`)
@ -383,7 +445,7 @@ func (s *CLISuite) TestStorage05ShouldMigrateDown() {
}
func (s *CLISuite) TestACLPolicyCheckVerbose() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://public.example.com", "--verbose", "--config", "/config/configuration.yml"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://public.example.com", "--verbose", "--config=/config/configuration.yml"})
s.Assert().NoError(err)
// This is an example of `authelia access-control check-policy --config .\internal\suites\CLI\configuration.yml --url=https://public.example.com --verbose`.
@ -400,7 +462,7 @@ func (s *CLISuite) TestACLPolicyCheckVerbose() {
s.Contains(output, " 9\tmiss\thit\t\thit\thit\tmay\n")
s.Contains(output, "The policy 'bypass' from rule #1 will be applied to this request.")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://admin.example.com", "--method=HEAD", "--username=tom", "--groups=basic,test", "--ip=192.168.2.3", "--verbose", "--config", "/config/configuration.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://admin.example.com", "--method=HEAD", "--username=tom", "--groups=basic,test", "--ip=192.168.2.3", "--verbose", "--config=/config/configuration.yml"})
s.Assert().NoError(err)
// This is an example of `authelia access-control check-policy --config .\internal\suites\CLI\configuration.yml --url=https://admin.example.com --method=HEAD --username=tom --groups=basic,test --ip=192.168.2.3 --verbose`.
@ -418,7 +480,7 @@ func (s *CLISuite) TestACLPolicyCheckVerbose() {
s.Contains(output, " 9\tmiss\thit\t\thit\thit\tmiss\n")
s.Contains(output, "The policy 'two_factor' from rule #2 will be applied to this request.")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://resources.example.com/resources/test", "--method=POST", "--username=john", "--groups=admin,test", "--ip=192.168.1.3", "--verbose", "--config", "/config/configuration.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://resources.example.com/resources/test", "--method=POST", "--username=john", "--groups=admin,test", "--ip=192.168.1.3", "--verbose", "--config=/config/configuration.yml"})
s.Assert().NoError(err)
// This is an example of `authelia access-control check-policy --config .\internal\suites\CLI\configuration.yml --url=https://resources.example.com/resources/test --method=POST --username=john --groups=admin,test --ip=192.168.1.3 --verbose`.
@ -435,7 +497,7 @@ func (s *CLISuite) TestACLPolicyCheckVerbose() {
s.Contains(output, " 9\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, "The policy 'one_factor' from rule #5 will be applied to this request.")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://user.example.com/resources/test", "--method=HEAD", "--username=john", "--groups=admin,test", "--ip=192.168.1.3", "--verbose", "--config", "/config/configuration.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://user.example.com/resources/test", "--method=HEAD", "--username=john", "--groups=admin,test", "--ip=192.168.1.3", "--verbose", "--config=/config/configuration.yml"})
s.Assert().NoError(err)
// This is an example of `access-control check-policy --config .\internal\suites\CLI\configuration.yml --url=https://user.example.com --method=HEAD --username=john --groups=admin,test --ip=192.168.1.3 --verbose`.
@ -452,7 +514,7 @@ func (s *CLISuite) TestACLPolicyCheckVerbose() {
s.Contains(output, "* 9\thit\thit\t\thit\thit\thit\n")
s.Contains(output, "The policy 'one_factor' from rule #9 will be applied to this request.")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://user.example.com", "--method=HEAD", "--ip=192.168.1.3", "--verbose", "--config", "/config/configuration.yml"})
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://user.example.com", "--method=HEAD", "--ip=192.168.1.3", "--verbose", "--config=/config/configuration.yml"})
s.Assert().NoError(err)
// This is an example of `authelia access-control check-policy --config .\internal\suites\CLI\configuration.yml --url=https://user.example.com --method=HEAD --ip=192.168.1.3 --verbose`.