[FEATURE] Regenerate session IDs after 2FA authentication. (#670)

Session fixation attacks were prevented because a session ID was
regenerated at each first factor authentication but this commit
generalize session regeneration from first to second factor too.

Fixes #180
pull/667/head^2
Clément Michaud 2020-03-01 00:13:33 +01:00 committed by GitHub
parent b007953580
commit 3816aa4df2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 101 additions and 0 deletions

View File

@ -43,6 +43,13 @@ func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
return return
} }
err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
if err != nil {
ctx.Error(fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), authenticationFailedMessage)
return
}
userSession.AuthenticationLevel = authentication.TwoFactor userSession.AuthenticationLevel = authentication.TwoFactor
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url" "net/url"
"regexp"
"testing" "testing"
"github.com/authelia/authelia/internal/duo" "github.com/authelia/authelia/internal/duo"
@ -167,6 +168,31 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() {
s.mock.Assert200OK(s.T(), nil) s.mock.Assert200OK(s.T(), nil)
} }
func (s *SecondFactorDuoPostSuite) TestShouldRegenerateSessionForPreventingSessionFixation() {
duoMock := mocks.NewMockAPI(s.mock.Ctrl)
response := duo.Response{}
response.Response.Result = "allow"
duoMock.EXPECT().Call(gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(signDuoRequestBody{
TargetURL: "http://mydomain.local",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
SecondFactorDuoPost(duoMock)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil)
s.Assert().NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
}
func TestRunSecondFactorDuoPostSuite(t *testing.T) { func TestRunSecondFactorDuoPostSuite(t *testing.T) {
s := new(SecondFactorDuoPostSuite) s := new(SecondFactorDuoPostSuite)
suite.Run(t, s) suite.Run(t, s)

View File

@ -32,6 +32,13 @@ func SecondFactorTOTPPost(totpVerifier TOTPVerifier) middlewares.RequestHandler
return return
} }
err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
if err != nil {
ctx.Error(fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), authenticationFailedMessage)
return
}
userSession.AuthenticationLevel = authentication.TwoFactor userSession.AuthenticationLevel = authentication.TwoFactor
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"regexp"
"testing" "testing"
"github.com/authelia/authelia/internal/mocks" "github.com/authelia/authelia/internal/mocks"
@ -122,6 +123,34 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
s.mock.Assert200OK(s.T(), nil) s.mock.Assert200OK(s.T(), nil)
} }
func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFixation() {
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
s.mock.StorageProviderMock.EXPECT().
LoadTOTPSecret(gomock.Any()).
Return("secret", nil)
verifier.EXPECT().
Verify(gomock.Eq("abc"), gomock.Eq("secret")).
Return(true)
bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc",
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
SecondFactorTOTPPost(verifier)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil)
s.Assert().NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
}
func TestRunHandlerSignTOTPSuite(t *testing.T) { func TestRunHandlerSignTOTPSuite(t *testing.T) {
suite.Run(t, new(HandlerSignTOTPSuite)) suite.Run(t, new(HandlerSignTOTPSuite))
} }

View File

@ -40,6 +40,13 @@ func SecondFactorU2FSignPost(u2fVerifier U2FVerifier) middlewares.RequestHandler
return return
} }
err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
if err != nil {
ctx.Error(fmt.Errorf("Unable to regenerate session for user %s: %s", userSession.Username, err), authenticationFailedMessage)
return
}
userSession.AuthenticationLevel = authentication.TwoFactor userSession.AuthenticationLevel = authentication.TwoFactor
err = ctx.SaveSession(userSession) err = ctx.SaveSession(userSession)

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"regexp"
"testing" "testing"
"github.com/authelia/authelia/internal/mocks" "github.com/authelia/authelia/internal/mocks"
@ -106,6 +107,30 @@ func (s *HandlerSignU2FStep2Suite) TestShouldNotRedirectToUnsafeURL() {
s.mock.Assert200OK(s.T(), nil) s.mock.Assert200OK(s.T(), nil)
} }
func (s *HandlerSignU2FStep2Suite) TestShouldRegenerateSessionForPreventingSessionFixation() {
u2fVerifier := NewMockU2FVerifier(s.mock.Ctrl)
u2fVerifier.EXPECT().
Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
bodyBytes, err := json.Marshal(signU2FRequestBody{
SignResponse: u2f.SignResponse{},
})
s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes)
r := regexp.MustCompile("^authelia_session=(.*); path=")
res := r.FindAllStringSubmatch(string(s.mock.Ctx.Response.Header.PeekCookie("authelia_session")), -1)
SecondFactorU2FSignPost(u2fVerifier)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), nil)
s.Assert().NotEqual(
res[0][1],
string(s.mock.Ctx.Request.Header.Cookie("authelia_session")))
}
func TestRunHandlerSignU2FStep2Suite(t *testing.T) { func TestRunHandlerSignU2FStep2Suite(t *testing.T) {
suite.Run(t, new(HandlerSignU2FStep2Suite)) suite.Run(t, new(HandlerSignU2FStep2Suite))
} }