From 9ceee6c660ac240f68d83b515dac91835751b3b5 Mon Sep 17 00:00:00 2001 From: James Elliott <james-d-elliott@users.noreply.github.com> Date: Tue, 30 Nov 2021 17:58:21 +1100 Subject: [PATCH] feat(storage): only store identity token metadata (#2627) This change makes it so only metadata about tokens is stored. Tokens can still be resigned due to conversion methods that convert from the JWT type to the database type. This should be more efficient and should mean we don't have to encrypt tokens or token info in the database at least for now. --- .../handler_register_u2f_step1_test.go | 34 +++++----- internal/middlewares/const.go | 2 - internal/middlewares/identity_verification.go | 59 ++++++++--------- .../middlewares/identity_verification_test.go | 58 ++++++++--------- internal/middlewares/types.go | 12 ---- .../models/model_identity_verification.go | 64 ++++++++++++++++++- internal/storage/const.go | 2 +- .../V0001.Initial_Schema.all.down.sql | 2 +- .../V0001.Initial_Schema.mysql.up.sql | 12 ++-- .../V0001.Initial_Schema.postgres.up.sql | 12 ++-- .../V0001.Initial_Schema.sqlite.up.sql | 12 ++-- internal/storage/sql_provider.go | 14 ++-- internal/storage/sql_provider_queries.go | 11 ++-- internal/suites/suite_cli_test.go | 4 +- 14 files changed, 170 insertions(+), 128 deletions(-) diff --git a/internal/handlers/handler_register_u2f_step1_test.go b/internal/handlers/handler_register_u2f_step1_test.go index 1706eada..b9c7c81b 100644 --- a/internal/handlers/handler_register_u2f_step1_test.go +++ b/internal/handlers/handler_register_u2f_step1_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/mocks" + "github.com/authelia/authelia/v4/internal/models" ) type HandlerRegisterU2FStep1Suite struct { @@ -34,34 +34,30 @@ func (s *HandlerRegisterU2FStep1Suite) TearDownTest() { s.mock.Close() } -func createToken(secret string, username string, action string, expiresAt time.Time) string { - claims := &middlewares.IdentityVerificationClaim{ - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: &jwt.NumericDate{ - Time: expiresAt, - }, - Issuer: "Authelia", - }, - Action: action, - Username: username, - } +func createToken(secret, username, action string, expiresAt time.Time) (data string, verification models.IdentityVerification) { + verification = models.NewIdentityVerification(username, action) + + verification.ExpiresAt = expiresAt + + claims := verification.ToIdentityVerificationClaim() + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) ss, _ := token.SignedString([]byte(secret)) - return ss + return ss, verification } func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissing() { - token := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", ActionU2FRegistration, + token, v := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", ActionU2FRegistration, time.Now().Add(1*time.Minute)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.StorageProviderMock.EXPECT(). - FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). + FindIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())). Return(true, nil) s.mock.StorageProviderMock.EXPECT(). - RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(token)). + RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())). Return(nil) SecondFactorU2FIdentityFinish(s.mock.Ctx) @@ -72,16 +68,16 @@ func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissi func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() { s.mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http") - token := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", ActionU2FRegistration, + token, v := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", ActionU2FRegistration, time.Now().Add(1*time.Minute)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.StorageProviderMock.EXPECT(). - FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). + FindIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())). Return(true, nil) s.mock.StorageProviderMock.EXPECT(). - RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(token)). + RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())). Return(nil) SecondFactorU2FIdentityFinish(s.mock.Ctx) diff --git a/internal/middlewares/const.go b/internal/middlewares/const.go index 27c157eb..23de4427 100644 --- a/internal/middlewares/const.go +++ b/internal/middlewares/const.go @@ -1,7 +1,5 @@ package middlewares -const jwtIssuer = "Authelia" - const ( headerXForwardedProto = "X-Forwarded-Proto" headerXForwardedMethod = "X-Forwarded-Method" diff --git a/internal/middlewares/identity_verification.go b/internal/middlewares/identity_verification.go index d4f33571..04a5bf24 100644 --- a/internal/middlewares/identity_verification.go +++ b/internal/middlewares/identity_verification.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "time" "github.com/golang-jwt/jwt/v4" @@ -20,7 +19,6 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle return func(ctx *AutheliaCtx) { identity, err := args.IdentityRetrieverFunc(ctx) - if err != nil { // In that case we reply ok to avoid user enumeration. ctx.Logger.Error(err) @@ -29,17 +27,11 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle return } + verification := models.NewIdentityVerification(identity.Username, args.ActionClaim) + // Create the claim with the action to sign it. - claims := &IdentityVerificationClaim{ - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: &jwt.NumericDate{ - Time: time.Now().Add(5 * time.Minute), - }, - Issuer: jwtIssuer, - }, - Action: args.ActionClaim, - Username: identity.Username, - } + claims := verification.ToIdentityVerificationClaim() + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) ss, err := token.SignedString([]byte(ctx.Configuration.JWTSecret)) @@ -48,9 +40,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle return } - err = ctx.Providers.StorageProvider.SaveIdentityVerification(ctx, models.IdentityVerification{ - Token: ss, - }) + err = ctx.Providers.StorageProvider.SaveIdentityVerification(ctx, verification) if err != nil { ctx.Error(err, messageOperationFailed) return @@ -131,20 +121,7 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c return } - found, err := ctx.Providers.StorageProvider.FindIdentityVerification(ctx, finishBody.Token) - - if err != nil { - ctx.Error(err, messageOperationFailed) - return - } - - if !found { - ctx.Error(fmt.Errorf("Token is not in DB, it might have already been used"), - messageIdentityVerificationTokenAlreadyUsed) - return - } - - token, err := jwt.ParseWithClaims(finishBody.Token, &IdentityVerificationClaim{}, + token, err := jwt.ParseWithClaims(finishBody.Token, &models.IdentityVerificationClaim{}, func(token *jwt.Token) (interface{}, error) { return []byte(ctx.Configuration.JWTSecret), nil }) @@ -170,12 +147,31 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c return } - claims, ok := token.Claims.(*IdentityVerificationClaim) + claims, ok := token.Claims.(*models.IdentityVerificationClaim) if !ok { ctx.Error(fmt.Errorf("Wrong type of claims (%T != *middlewares.IdentityVerificationClaim)", claims), messageOperationFailed) return } + verification, err := claims.ToIdentityVerification() + if err != nil { + ctx.Error(fmt.Errorf("Token seems to be invalid: %w", err), + messageOperationFailed) + return + } + + found, err := ctx.Providers.StorageProvider.FindIdentityVerification(ctx, verification.JTI.String()) + if err != nil { + ctx.Error(err, messageOperationFailed) + return + } + + if !found { + ctx.Error(fmt.Errorf("Token is not in DB, it might have already been used"), + messageIdentityVerificationTokenAlreadyUsed) + return + } + // Verify that the action claim in the token is the one expected for the given endpoint. if claims.Action != args.ActionClaim { ctx.Error(fmt.Errorf("This token has not been generated for this kind of action"), messageOperationFailed) @@ -187,8 +183,7 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c return } - // TODO(c.michaud): find a way to garbage collect unused tokens. - err = ctx.Providers.StorageProvider.RemoveIdentityVerification(ctx, finishBody.Token) + err = ctx.Providers.StorageProvider.RemoveIdentityVerification(ctx, claims.ID) if err != nil { ctx.Error(err, messageOperationFailed) return diff --git a/internal/middlewares/identity_verification_test.go b/internal/middlewares/identity_verification_test.go index 29423ae6..82af45d2 100644 --- a/internal/middlewares/identity_verification_test.go +++ b/internal/middlewares/identity_verification_test.go @@ -12,6 +12,7 @@ import ( "github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/mocks" + "github.com/authelia/authelia/v4/internal/models" "github.com/authelia/authelia/v4/internal/session" ) @@ -164,21 +165,17 @@ func (s *IdentityVerificationFinishProcess) TearDownTest() { s.mock.Close() } -func createToken(secret string, username string, action string, expiresAt time.Time) string { - claims := &middlewares.IdentityVerificationClaim{ - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: &jwt.NumericDate{ - Time: expiresAt, - }, - Issuer: "Authelia", - }, - Action: action, - Username: username, - } +func createToken(secret, username, action string, expiresAt time.Time) (data string, verification models.IdentityVerification) { + verification = models.NewIdentityVerification(username, action) + + verification.ExpiresAt = expiresAt + + claims := verification.ToIdentityVerificationClaim() + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) ss, _ := token.SignedString([]byte(secret)) - return ss + return ss, verification } func next(ctx *middlewares.AutheliaCtx, username string) {} @@ -206,10 +203,13 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsNotProvided() } func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsNotFoundInDB() { - s.mock.Ctx.Request.SetBodyString("{\"token\":\"abc\"}") + token, verification := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", "Login", + time.Now().Add(1*time.Minute)) + + s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.StorageProviderMock.EXPECT(). - FindIdentityVerification(s.mock.Ctx, gomock.Eq("abc")). + FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). Return(false, nil) middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx) @@ -221,10 +221,6 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsNotFoundInDB( func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsInvalid() { s.mock.Ctx.Request.SetBodyString("{\"token\":\"abc\"}") - s.mock.StorageProviderMock.EXPECT(). - FindIdentityVerification(s.mock.Ctx, gomock.Eq("abc")). - Return(true, nil) - middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx) s.mock.Assert200KO(s.T(), "Operation failed") @@ -233,14 +229,10 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsInvalid() { func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenExpired() { args := newArgs(defaultRetriever) - token := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", args.ActionClaim, + token, _ := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", args.ActionClaim, time.Now().Add(-1*time.Minute)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) - s.mock.StorageProviderMock.EXPECT(). - FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). - Return(true, nil) - middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx) s.mock.Assert200KO(s.T(), "The identity verification token has expired") @@ -248,12 +240,12 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenExpired() { } func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongAction() { - token := createToken(s.mock.Ctx.Configuration.JWTSecret, "", "", + token, verification := createToken(s.mock.Ctx.Configuration.JWTSecret, "", "", time.Now().Add(1*time.Minute)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.StorageProviderMock.EXPECT(). - FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). + FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). Return(true, nil) middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx) @@ -263,12 +255,12 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongAction() { } func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongUser() { - token := createToken(s.mock.Ctx.Configuration.JWTSecret, "harry", "EXP_ACTION", + token, verification := createToken(s.mock.Ctx.Configuration.JWTSecret, "harry", "EXP_ACTION", time.Now().Add(1*time.Minute)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.StorageProviderMock.EXPECT(). - FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). + FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). Return(true, nil) args := newFinishArgs() @@ -280,16 +272,16 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongUser() { } func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenCannotBeRemovedFromDB() { - token := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", "EXP_ACTION", + token, verification := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", "EXP_ACTION", time.Now().Add(1*time.Minute)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.StorageProviderMock.EXPECT(). - FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). + FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). Return(true, nil) s.mock.StorageProviderMock.EXPECT(). - RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(token)). + RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). Return(fmt.Errorf("cannot remove")) middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx) @@ -299,16 +291,16 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenCannotBeRemoved } func (s *IdentityVerificationFinishProcess) TestShouldReturn200OnFinishComplete() { - token := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", "EXP_ACTION", + token, verification := createToken(s.mock.Ctx.Configuration.JWTSecret, "john", "EXP_ACTION", time.Now().Add(1*time.Minute)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.StorageProviderMock.EXPECT(). - FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). + FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). Return(true, nil) s.mock.StorageProviderMock.EXPECT(). - RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(token)). + RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())). Return(nil) middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx) diff --git a/internal/middlewares/types.go b/internal/middlewares/types.go index 82e3e85c..498e62fd 100644 --- a/internal/middlewares/types.go +++ b/internal/middlewares/types.go @@ -1,7 +1,6 @@ package middlewares import ( - "github.com/golang-jwt/jwt/v4" "github.com/sirupsen/logrus" "github.com/valyala/fasthttp" @@ -80,17 +79,6 @@ type IdentityVerificationFinishArgs struct { IsTokenUserValidFunc func(ctx *AutheliaCtx, username string) bool } -// IdentityVerificationClaim custom claim for specifying the action claim. -// The action can be to register a TOTP device, a U2F device or reset one's password. -type IdentityVerificationClaim struct { - jwt.RegisteredClaims - - // The action this token has been crafted for. - Action string `json:"action"` - // The user this token has been crafted for. - Username string `json:"username"` -} - // IdentityVerificationFinishBody type of the body received by the finish endpoint. type IdentityVerificationFinishBody struct { Token string `json:"token"` diff --git a/internal/models/model_identity_verification.go b/internal/models/model_identity_verification.go index 873ba92a..e5e180c1 100644 --- a/internal/models/model_identity_verification.go +++ b/internal/models/model_identity_verification.go @@ -2,11 +2,69 @@ package models import ( "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" ) +// NewIdentityVerification creates a new IdentityVerification from a given username and action. +func NewIdentityVerification(username, action string) (verification IdentityVerification) { + return IdentityVerification{ + JTI: uuid.New(), + IssuedAt: time.Now(), + ExpiresAt: time.Now().Add(5 * time.Minute), + Action: action, + Username: username, + } +} + // IdentityVerification represents an identity verification row in the database. type IdentityVerification struct { - ID int `db:"id"` - Created time.Time `db:"created"` - Token string `db:"token"` + ID int `db:"id"` + JTI uuid.UUID `db:"jti"` + IssuedAt time.Time `db:"iat"` + ExpiresAt time.Time `db:"exp"` + Used *time.Time `db:"used"` + Action string `db:"action"` + Username string `db:"username"` +} + +// ToIdentityVerificationClaim converts the IdentityVerification into a IdentityVerificationClaim. +func (v IdentityVerification) ToIdentityVerificationClaim() (claim *IdentityVerificationClaim) { + return &IdentityVerificationClaim{ + RegisteredClaims: jwt.RegisteredClaims{ + ID: v.JTI.String(), + Issuer: "Authelia", + IssuedAt: jwt.NewNumericDate(v.IssuedAt), + ExpiresAt: jwt.NewNumericDate(v.ExpiresAt), + }, + Action: v.Action, + Username: v.Username, + } +} + +// IdentityVerificationClaim custom claim for specifying the action claim. +// The action can be to register a TOTP device, a U2F device or reset one's password. +type IdentityVerificationClaim struct { + jwt.RegisteredClaims + + // The action this token has been crafted for. + Action string `json:"action"` + // The user this token has been crafted for. + Username string `json:"username"` +} + +// ToIdentityVerification converts the IdentityVerificationClaim into a IdentityVerification. +func (v IdentityVerificationClaim) ToIdentityVerification() (verification *IdentityVerification, err error) { + jti, err := uuid.Parse(v.ID) + if err != nil { + return nil, err + } + + return &IdentityVerification{ + JTI: jti, + Username: v.Username, + Action: v.Action, + ExpiresAt: v.ExpiresAt.Time, + }, nil } diff --git a/internal/storage/const.go b/internal/storage/const.go index ad2e2ed2..5472b173 100644 --- a/internal/storage/const.go +++ b/internal/storage/const.go @@ -6,7 +6,7 @@ import ( const ( tableUserPreferences = "user_preferences" - tableIdentityVerification = "identity_verification_tokens" + tableIdentityVerification = "identity_verification" tableTOTPConfigurations = "totp_configurations" tableU2FDevices = "u2f_devices" tableDUODevices = "duo_devices" diff --git a/internal/storage/migrations/V0001.Initial_Schema.all.down.sql b/internal/storage/migrations/V0001.Initial_Schema.all.down.sql index fdee524c..0209622f 100644 --- a/internal/storage/migrations/V0001.Initial_Schema.all.down.sql +++ b/internal/storage/migrations/V0001.Initial_Schema.all.down.sql @@ -1,5 +1,5 @@ DROP TABLE IF EXISTS authentication_logs; -DROP TABLE IF EXISTS identity_verification_tokens; +DROP TABLE IF EXISTS identity_verification; DROP TABLE IF EXISTS totp_configurations; DROP TABLE IF EXISTS u2f_devices; DROP TABLE IF EXISTS user_preferences; diff --git a/internal/storage/migrations/V0001.Initial_Schema.mysql.up.sql b/internal/storage/migrations/V0001.Initial_Schema.mysql.up.sql index 98cb08c5..e36a12b1 100644 --- a/internal/storage/migrations/V0001.Initial_Schema.mysql.up.sql +++ b/internal/storage/migrations/V0001.Initial_Schema.mysql.up.sql @@ -14,12 +14,16 @@ CREATE TABLE IF NOT EXISTS authentication_logs ( CREATE INDEX authentication_logs_username_idx ON authentication_logs (time, username, auth_type); CREATE INDEX authentication_logs_remote_ip_idx ON authentication_logs (time, remote_ip, auth_type); -CREATE TABLE IF NOT EXISTS identity_verification_tokens ( +CREATE TABLE IF NOT EXISTS identity_verification ( id INTEGER AUTO_INCREMENT, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - token VARCHAR(512), + jti CHAR(36), + iat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + exp TIMESTAMP NOT NULL, + used TIMESTAMP NULL DEFAULT NULL, + username VARCHAR(100) NOT NULL, + action VARCHAR(50) NOT NULL, PRIMARY KEY (id), - UNIQUE KEY (token) + UNIQUE KEY (jti) ); CREATE TABLE IF NOT EXISTS totp_configurations ( diff --git a/internal/storage/migrations/V0001.Initial_Schema.postgres.up.sql b/internal/storage/migrations/V0001.Initial_Schema.postgres.up.sql index bef73a3f..ec7f225d 100644 --- a/internal/storage/migrations/V0001.Initial_Schema.postgres.up.sql +++ b/internal/storage/migrations/V0001.Initial_Schema.postgres.up.sql @@ -14,12 +14,16 @@ CREATE TABLE IF NOT EXISTS authentication_logs ( CREATE INDEX authentication_logs_username_idx ON authentication_logs (time, username, auth_type); CREATE INDEX authentication_logs_remote_ip_idx ON authentication_logs (time, remote_ip, auth_type); -CREATE TABLE IF NOT EXISTS identity_verification_tokens ( +CREATE TABLE IF NOT EXISTS identity_verification ( id SERIAL, - created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - token VARCHAR(512), + jti CHAR(36), + iat TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + exp TIMESTAMP WITH TIME ZONE NOT NULL, + used TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL, + username VARCHAR(100) NOT NULL, + action VARCHAR(50) NOT NULL, PRIMARY KEY (id), - UNIQUE (token) + UNIQUE (jti) ); CREATE TABLE IF NOT EXISTS totp_configurations ( diff --git a/internal/storage/migrations/V0001.Initial_Schema.sqlite.up.sql b/internal/storage/migrations/V0001.Initial_Schema.sqlite.up.sql index 06b3303e..2e91d6f9 100644 --- a/internal/storage/migrations/V0001.Initial_Schema.sqlite.up.sql +++ b/internal/storage/migrations/V0001.Initial_Schema.sqlite.up.sql @@ -14,12 +14,16 @@ CREATE TABLE IF NOT EXISTS authentication_logs ( CREATE INDEX authentication_logs_username_idx ON authentication_logs (time, username, auth_type); CREATE INDEX authentication_logs_remote_ip_idx ON authentication_logs (time, remote_ip, auth_type); -CREATE TABLE IF NOT EXISTS identity_verification_tokens ( +CREATE TABLE IF NOT EXISTS identity_verification ( id INTEGER, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - token VARCHAR(512), + jti VARCHAR(36), + iat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + exp TIMESTAMP NOT NULL, + used TIMESTAMP NULL DEFAULT NULL, + username VARCHAR(100) NOT NULL, + action VARCHAR(50) NOT NULL, PRIMARY KEY (id), - UNIQUE (token) + UNIQUE (jti) ); CREATE TABLE IF NOT EXISTS totp_configurations ( diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go index 5b876f4c..dfa23e66 100644 --- a/internal/storage/sql_provider.go +++ b/internal/storage/sql_provider.go @@ -81,7 +81,7 @@ type SQLProvider struct { sqlInsertAuthenticationAttempt string sqlSelectAuthenticationAttemptsByUsername string - // Table: identity_verification_tokens. + // Table: identity_verification. sqlInsertIdentityVerification string sqlDeleteIdentityVerification string sqlSelectExistsIdentityVerification string @@ -208,7 +208,9 @@ func (p *SQLProvider) LoadUserInfo(ctx context.Context, username string) (info m // SaveIdentityVerification save an identity verification record to the database. func (p *SQLProvider) SaveIdentityVerification(ctx context.Context, verification models.IdentityVerification) (err error) { - if _, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification, verification.Token); err != nil { + if _, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification, + verification.JTI, verification.IssuedAt, verification.ExpiresAt, + verification.Username, verification.Action); err != nil { return fmt.Errorf("error inserting identity verification: %w", err) } @@ -216,8 +218,8 @@ func (p *SQLProvider) SaveIdentityVerification(ctx context.Context, verification } // RemoveIdentityVerification remove an identity verification record from the database. -func (p *SQLProvider) RemoveIdentityVerification(ctx context.Context, token string) (err error) { - if _, err = p.db.ExecContext(ctx, p.sqlDeleteIdentityVerification, token); err != nil { +func (p *SQLProvider) RemoveIdentityVerification(ctx context.Context, jti string) (err error) { + if _, err = p.db.ExecContext(ctx, p.sqlDeleteIdentityVerification, jti); err != nil { return fmt.Errorf("error updating identity verification: %w", err) } @@ -225,8 +227,8 @@ func (p *SQLProvider) RemoveIdentityVerification(ctx context.Context, token stri } // FindIdentityVerification checks if an identity verification record is in the database and active. -func (p *SQLProvider) FindIdentityVerification(ctx context.Context, token string) (found bool, err error) { - if err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, token); err != nil { +func (p *SQLProvider) FindIdentityVerification(ctx context.Context, jti string) (found bool, err error) { + if err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, jti); err != nil { return false, fmt.Errorf("error selecting identity verification exists: %w", err) } diff --git a/internal/storage/sql_provider_queries.go b/internal/storage/sql_provider_queries.go index a4cb5a1c..8f59c0c0 100644 --- a/internal/storage/sql_provider_queries.go +++ b/internal/storage/sql_provider_queries.go @@ -60,16 +60,17 @@ const ( SELECT EXISTS ( SELECT id FROM %s - WHERE token = ? + WHERE jti = ? AND exp > CURRENT_TIMESTAMP AND used IS NULL );` queryFmtInsertIdentityVerification = ` - INSERT INTO %s (token) - VALUES (?);` + INSERT INTO %s (jti, iat, exp, username, action) + VALUES (?, ?, ?, ?, ?);` queryFmtDeleteIdentityVerification = ` - DELETE FROM %s - WHERE token = ?;` + UPDATE %s + SET used = CURRENT_TIMESTAMP + WHERE jti = ?;` ) const ( diff --git a/internal/suites/suite_cli_test.go b/internal/suites/suite_cli_test.go index fae91cda..7cd7884f 100644 --- a/internal/suites/suite_cli_test.go +++ b/internal/suites/suite_cli_test.go @@ -261,7 +261,7 @@ func (s *CLISuite) TestStorage02ShouldShowSchemaInfo() { 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_tokens, totp_configurations, u2f_devices, user_preferences, migrations, encryption\nSchema Encryption Key: valid`) + pattern := regexp.MustCompile(`^Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification, totp_configurations, u2f_devices, user_preferences, migrations, encryption\nSchema Encryption Key: valid`) s.Assert().Regexp(pattern, output) } @@ -336,7 +336,7 @@ func (s *CLISuite) TestStorage04ShouldChangeEncryptionKey() { 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_tokens, totp_configurations, u2f_devices, user_preferences, migrations, encryption\nSchema Encryption Key: invalid`) + pattern := regexp.MustCompile(`Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification, totp_configurations, u2f_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"})