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.
pull/2646/head
James Elliott 2021-11-30 17:58:21 +11:00 committed by GitHub
parent b1d37d2069
commit 9ceee6c660
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 170 additions and 128 deletions

View File

@ -11,8 +11,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "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/mocks"
"github.com/authelia/authelia/v4/internal/models"
) )
type HandlerRegisterU2FStep1Suite struct { type HandlerRegisterU2FStep1Suite struct {
@ -34,34 +34,30 @@ func (s *HandlerRegisterU2FStep1Suite) TearDownTest() {
s.mock.Close() s.mock.Close()
} }
func createToken(secret string, username string, action string, expiresAt time.Time) string { func createToken(secret, username, action string, expiresAt time.Time) (data string, verification models.IdentityVerification) {
claims := &middlewares.IdentityVerificationClaim{ verification = models.NewIdentityVerification(username, action)
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: &jwt.NumericDate{ verification.ExpiresAt = expiresAt
Time: expiresAt,
}, claims := verification.ToIdentityVerificationClaim()
Issuer: "Authelia",
},
Action: action,
Username: username,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, _ := token.SignedString([]byte(secret)) ss, _ := token.SignedString([]byte(secret))
return ss return ss, verification
} }
func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissing() { 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)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageProviderMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). FindIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())).
Return(true, nil) Return(true, nil)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageProviderMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(token)). RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())).
Return(nil) Return(nil)
SecondFactorU2FIdentityFinish(s.mock.Ctx) SecondFactorU2FIdentityFinish(s.mock.Ctx)
@ -72,16 +68,16 @@ func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedProtoIsMissi
func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() { func (s *HandlerRegisterU2FStep1Suite) TestShouldRaiseWhenXForwardedHostIsMissing() {
s.mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http") 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)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageProviderMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). FindIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())).
Return(true, nil) Return(true, nil)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageProviderMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(token)). RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(v.JTI.String())).
Return(nil) Return(nil)
SecondFactorU2FIdentityFinish(s.mock.Ctx) SecondFactorU2FIdentityFinish(s.mock.Ctx)

View File

@ -1,7 +1,5 @@
package middlewares package middlewares
const jwtIssuer = "Authelia"
const ( const (
headerXForwardedProto = "X-Forwarded-Proto" headerXForwardedProto = "X-Forwarded-Proto"
headerXForwardedMethod = "X-Forwarded-Method" headerXForwardedMethod = "X-Forwarded-Method"

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
@ -20,7 +19,6 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle
return func(ctx *AutheliaCtx) { return func(ctx *AutheliaCtx) {
identity, err := args.IdentityRetrieverFunc(ctx) identity, err := args.IdentityRetrieverFunc(ctx)
if err != nil { if err != nil {
// In that case we reply ok to avoid user enumeration. // In that case we reply ok to avoid user enumeration.
ctx.Logger.Error(err) ctx.Logger.Error(err)
@ -29,17 +27,11 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle
return return
} }
verification := models.NewIdentityVerification(identity.Username, args.ActionClaim)
// Create the claim with the action to sign it. // Create the claim with the action to sign it.
claims := &IdentityVerificationClaim{ claims := verification.ToIdentityVerificationClaim()
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: &jwt.NumericDate{
Time: time.Now().Add(5 * time.Minute),
},
Issuer: jwtIssuer,
},
Action: args.ActionClaim,
Username: identity.Username,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString([]byte(ctx.Configuration.JWTSecret)) ss, err := token.SignedString([]byte(ctx.Configuration.JWTSecret))
@ -48,9 +40,7 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs) RequestHandle
return return
} }
err = ctx.Providers.StorageProvider.SaveIdentityVerification(ctx, models.IdentityVerification{ err = ctx.Providers.StorageProvider.SaveIdentityVerification(ctx, verification)
Token: ss,
})
if err != nil { if err != nil {
ctx.Error(err, messageOperationFailed) ctx.Error(err, messageOperationFailed)
return return
@ -131,20 +121,7 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c
return return
} }
found, err := ctx.Providers.StorageProvider.FindIdentityVerification(ctx, finishBody.Token) token, err := jwt.ParseWithClaims(finishBody.Token, &models.IdentityVerificationClaim{},
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{},
func(token *jwt.Token) (interface{}, error) { func(token *jwt.Token) (interface{}, error) {
return []byte(ctx.Configuration.JWTSecret), nil return []byte(ctx.Configuration.JWTSecret), nil
}) })
@ -170,12 +147,31 @@ func IdentityVerificationFinish(args IdentityVerificationFinishArgs, next func(c
return return
} }
claims, ok := token.Claims.(*IdentityVerificationClaim) claims, ok := token.Claims.(*models.IdentityVerificationClaim)
if !ok { if !ok {
ctx.Error(fmt.Errorf("Wrong type of claims (%T != *middlewares.IdentityVerificationClaim)", claims), messageOperationFailed) ctx.Error(fmt.Errorf("Wrong type of claims (%T != *middlewares.IdentityVerificationClaim)", claims), messageOperationFailed)
return 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. // Verify that the action claim in the token is the one expected for the given endpoint.
if claims.Action != args.ActionClaim { if claims.Action != args.ActionClaim {
ctx.Error(fmt.Errorf("This token has not been generated for this kind of action"), messageOperationFailed) 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 return
} }
// TODO(c.michaud): find a way to garbage collect unused tokens. err = ctx.Providers.StorageProvider.RemoveIdentityVerification(ctx, claims.ID)
err = ctx.Providers.StorageProvider.RemoveIdentityVerification(ctx, finishBody.Token)
if err != nil { if err != nil {
ctx.Error(err, messageOperationFailed) ctx.Error(err, messageOperationFailed)
return return

View File

@ -12,6 +12,7 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/models"
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
) )
@ -164,21 +165,17 @@ func (s *IdentityVerificationFinishProcess) TearDownTest() {
s.mock.Close() s.mock.Close()
} }
func createToken(secret string, username string, action string, expiresAt time.Time) string { func createToken(secret, username, action string, expiresAt time.Time) (data string, verification models.IdentityVerification) {
claims := &middlewares.IdentityVerificationClaim{ verification = models.NewIdentityVerification(username, action)
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: &jwt.NumericDate{ verification.ExpiresAt = expiresAt
Time: expiresAt,
}, claims := verification.ToIdentityVerificationClaim()
Issuer: "Authelia",
},
Action: action,
Username: username,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, _ := token.SignedString([]byte(secret)) ss, _ := token.SignedString([]byte(secret))
return ss return ss, verification
} }
func next(ctx *middlewares.AutheliaCtx, username string) {} func next(ctx *middlewares.AutheliaCtx, username string) {}
@ -206,10 +203,13 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsNotProvided()
} }
func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsNotFoundInDB() { 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(). s.mock.StorageProviderMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq("abc")). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(false, nil) Return(false, nil)
middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx) middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx)
@ -221,10 +221,6 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsNotFoundInDB(
func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsInvalid() { func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsInvalid() {
s.mock.Ctx.Request.SetBodyString("{\"token\":\"abc\"}") 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) middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx)
s.mock.Assert200KO(s.T(), "Operation failed") s.mock.Assert200KO(s.T(), "Operation failed")
@ -233,14 +229,10 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenIsInvalid() {
func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenExpired() { func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenExpired() {
args := newArgs(defaultRetriever) 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)) time.Now().Add(-1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) 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) middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx)
s.mock.Assert200KO(s.T(), "The identity verification token has expired") s.mock.Assert200KO(s.T(), "The identity verification token has expired")
@ -248,12 +240,12 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenExpired() {
} }
func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongAction() { 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)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageProviderMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil) Return(true, nil)
middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx) middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx)
@ -263,12 +255,12 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongAction() {
} }
func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongUser() { 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)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageProviderMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil) Return(true, nil)
args := newFinishArgs() args := newFinishArgs()
@ -280,16 +272,16 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailForWrongUser() {
} }
func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenCannotBeRemovedFromDB() { 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)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageProviderMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil) Return(true, nil)
s.mock.StorageProviderMock.EXPECT(). 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")) Return(fmt.Errorf("cannot remove"))
middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx) middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx)
@ -299,16 +291,16 @@ func (s *IdentityVerificationFinishProcess) TestShouldFailIfTokenCannotBeRemoved
} }
func (s *IdentityVerificationFinishProcess) TestShouldReturn200OnFinishComplete() { 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)) time.Now().Add(1*time.Minute))
s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token)) s.mock.Ctx.Request.SetBodyString(fmt.Sprintf("{\"token\":\"%s\"}", token))
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageProviderMock.EXPECT().
FindIdentityVerification(s.mock.Ctx, gomock.Eq(token)). FindIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(true, nil) Return(true, nil)
s.mock.StorageProviderMock.EXPECT(). s.mock.StorageProviderMock.EXPECT().
RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(token)). RemoveIdentityVerification(s.mock.Ctx, gomock.Eq(verification.JTI.String())).
Return(nil) Return(nil)
middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx) middlewares.IdentityVerificationFinish(newFinishArgs(), next)(s.mock.Ctx)

View File

@ -1,7 +1,6 @@
package middlewares package middlewares
import ( import (
"github.com/golang-jwt/jwt/v4"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@ -80,17 +79,6 @@ type IdentityVerificationFinishArgs struct {
IsTokenUserValidFunc func(ctx *AutheliaCtx, username string) bool 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. // IdentityVerificationFinishBody type of the body received by the finish endpoint.
type IdentityVerificationFinishBody struct { type IdentityVerificationFinishBody struct {
Token string `json:"token"` Token string `json:"token"`

View File

@ -2,11 +2,69 @@ package models
import ( import (
"time" "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. // IdentityVerification represents an identity verification row in the database.
type IdentityVerification struct { type IdentityVerification struct {
ID int `db:"id"` ID int `db:"id"`
Created time.Time `db:"created"` JTI uuid.UUID `db:"jti"`
Token string `db:"token"` 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
} }

View File

@ -6,7 +6,7 @@ import (
const ( const (
tableUserPreferences = "user_preferences" tableUserPreferences = "user_preferences"
tableIdentityVerification = "identity_verification_tokens" tableIdentityVerification = "identity_verification"
tableTOTPConfigurations = "totp_configurations" tableTOTPConfigurations = "totp_configurations"
tableU2FDevices = "u2f_devices" tableU2FDevices = "u2f_devices"
tableDUODevices = "duo_devices" tableDUODevices = "duo_devices"

View File

@ -1,5 +1,5 @@
DROP TABLE IF EXISTS authentication_logs; 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 totp_configurations;
DROP TABLE IF EXISTS u2f_devices; DROP TABLE IF EXISTS u2f_devices;
DROP TABLE IF EXISTS user_preferences; DROP TABLE IF EXISTS user_preferences;

View File

@ -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_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 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, id INTEGER AUTO_INCREMENT,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, jti CHAR(36),
token VARCHAR(512), 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), PRIMARY KEY (id),
UNIQUE KEY (token) UNIQUE KEY (jti)
); );
CREATE TABLE IF NOT EXISTS totp_configurations ( CREATE TABLE IF NOT EXISTS totp_configurations (

View File

@ -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_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 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, id SERIAL,
created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, jti CHAR(36),
token VARCHAR(512), 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), PRIMARY KEY (id),
UNIQUE (token) UNIQUE (jti)
); );
CREATE TABLE IF NOT EXISTS totp_configurations ( CREATE TABLE IF NOT EXISTS totp_configurations (

View File

@ -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_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 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, id INTEGER,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, jti VARCHAR(36),
token VARCHAR(512), 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), PRIMARY KEY (id),
UNIQUE (token) UNIQUE (jti)
); );
CREATE TABLE IF NOT EXISTS totp_configurations ( CREATE TABLE IF NOT EXISTS totp_configurations (

View File

@ -81,7 +81,7 @@ type SQLProvider struct {
sqlInsertAuthenticationAttempt string sqlInsertAuthenticationAttempt string
sqlSelectAuthenticationAttemptsByUsername string sqlSelectAuthenticationAttemptsByUsername string
// Table: identity_verification_tokens. // Table: identity_verification.
sqlInsertIdentityVerification string sqlInsertIdentityVerification string
sqlDeleteIdentityVerification string sqlDeleteIdentityVerification string
sqlSelectExistsIdentityVerification 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. // SaveIdentityVerification save an identity verification record to the database.
func (p *SQLProvider) SaveIdentityVerification(ctx context.Context, verification models.IdentityVerification) (err error) { 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) 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. // RemoveIdentityVerification remove an identity verification record from the database.
func (p *SQLProvider) RemoveIdentityVerification(ctx context.Context, token string) (err error) { func (p *SQLProvider) RemoveIdentityVerification(ctx context.Context, jti string) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlDeleteIdentityVerification, token); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlDeleteIdentityVerification, jti); err != nil {
return fmt.Errorf("error updating identity verification: %w", err) 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. // 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) { func (p *SQLProvider) FindIdentityVerification(ctx context.Context, jti string) (found bool, err error) {
if err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, token); err != nil { if err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, jti); err != nil {
return false, fmt.Errorf("error selecting identity verification exists: %w", err) return false, fmt.Errorf("error selecting identity verification exists: %w", err)
} }

View File

@ -60,16 +60,17 @@ const (
SELECT EXISTS ( SELECT EXISTS (
SELECT id SELECT id
FROM %s FROM %s
WHERE token = ? WHERE jti = ? AND exp > CURRENT_TIMESTAMP AND used IS NULL
);` );`
queryFmtInsertIdentityVerification = ` queryFmtInsertIdentityVerification = `
INSERT INTO %s (token) INSERT INTO %s (jti, iat, exp, username, action)
VALUES (?);` VALUES (?, ?, ?, ?, ?);`
queryFmtDeleteIdentityVerification = ` queryFmtDeleteIdentityVerification = `
DELETE FROM %s UPDATE %s
WHERE token = ?;` SET used = CURRENT_TIMESTAMP
WHERE jti = ?;`
) )
const ( const (

View File

@ -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"}) output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err) 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) 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"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
s.Assert().NoError(err) 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) 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"})