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
parent
b1d37d2069
commit
9ceee6c660
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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"})
|
||||||
|
|
Loading…
Reference in New Issue