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"})