Skip to content

Commit

Permalink
Session management changes
Browse files Browse the repository at this point in the history
- Only refresh session on API calls to blaise, not static asset loading
- Don't check session expiry if the button being clicked is save and
  sign out
- Use redis backed session management for user session
- Use cookie backed session for CSRF
- 30 day expiry for CSRF
- 1 day expiry for all redis backed session data
- Add seperate session validation session to ensure refreshed tokens are
  not valid after logout - fixes race condition where the JWT is both
  cleared and written and the same time (within the same ms)
  • Loading branch information
srbry committed Dec 21, 2021
1 parent 1259506 commit 30857d4
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 275 deletions.
16 changes: 10 additions & 6 deletions assets/js/check-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ window.addEventListener('click', function(event) {
target.parentElement.getAttribute("role") == "button" ||
target.parentElement.tagName == "button"
) {
var xmlHttp = new XMLHttpRequest
xmlHttp.open("GET", "/auth/logged-in", false);
xmlHttp.send(null);
if (xmlHttp.status !== 200) {
this.window.location.replace("/auth/timed-out");
if (
target.innerText.toLowerCase() !== "save and sign out"
) {
var xmlHttp = new XMLHttpRequest
xmlHttp.open("GET", "/auth/logged-in", false);
xmlHttp.send(null);
if (xmlHttp.status !== 200) {
this.window.location.replace("/auth/timed-out");
};
};
}
};
}, false);
49 changes: 38 additions & 11 deletions authenticate/authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import (
"github.com/ONSdigital/blaise-cawi-portal/utils"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
csrf "github.com/utrack/gin-csrf"
csrf "github.com/srbry/gin-csrf"
"go.uber.org/zap"
)

const (
SESSION_TIMEOUT_KEY = "session_timeout"
JWT_TOKEN_KEY = "jwt_token"
SESSION_VALID_KEY = "session_valid"
NO_ACCESS_CODE_ERR = "Enter an access code"
INVALID_LENGTH_ERR = "Enter a %s access code"
NOT_RECOGNISED_ERR = "Access code not recognised. Enter the code again"
Expand All @@ -41,15 +42,15 @@ type Auth struct {
JWTCrypto JWTCryptoInterface
BlaiseRestApi blaiserestapi.BlaiseRestApiInterface
Logger *zap.Logger
CSRFSecret string
UacKind string
CSRFManager csrf.CSRFManager
}

func (auth *Auth) AuthenticatedWithUac(context *gin.Context) {
session := sessions.Default(context)
session := sessions.DefaultMany(context, "user_session")
jwtToken := session.Get(JWT_TOKEN_KEY)

if jwtToken == nil {
if jwtToken == nil || !auth.SessionValid(context) {
auth.notAuth(context)
return
}
Expand All @@ -64,7 +65,7 @@ func (auth *Auth) AuthenticatedWithUac(context *gin.Context) {
}

func (auth *Auth) HasSession(context *gin.Context) (bool, *UACClaims) {
session := sessions.Default(context)
session := sessions.DefaultMany(context, "user_session")
jwtToken := session.Get(JWT_TOKEN_KEY)

if jwtToken == nil {
Expand Down Expand Up @@ -144,6 +145,14 @@ func (auth *Auth) Login(context *gin.Context, session sessions.Session) {
return
}

validationSession := sessions.DefaultMany(context, "session_validation")
validationSession.Set(SESSION_VALID_KEY, true)
if err := validationSession.Save(); err != nil {
auth.Logger.Error("Failed to save validationSession", zap.Error(err))
auth.NotAuthWithError(context, INTERNAL_SERVER_ERR)
return
}

context.Redirect(http.StatusFound, fmt.Sprintf("/%s/", uacInfo.InstrumentName))
context.Abort()
}
Expand All @@ -153,39 +162,40 @@ func (auth *Auth) Logout(context *gin.Context, session sessions.Session) {
session.Clear()
session.Options(sessions.Options{MaxAge: -1})
err := session.Save()
if err != nil {
if err != nil || auth.clearSessionValidation(context) != nil {
auth.notAuth(context)
return
}
context.HTML(http.StatusOK, "logout.tmpl", gin.H{})
}

func (auth *Auth) notAuth(context *gin.Context) {
context.Set("csrfSecret", auth.CSRFSecret)
context.HTML(http.StatusUnauthorized, "login.tmpl", gin.H{
"uac16": auth.isUac16(),
"csrf_token": csrf.GetToken(context)})
"csrf_token": auth.CSRFManager.GetToken(context)})
context.Abort()
}

func (auth *Auth) NotAuthWithError(context *gin.Context, errorMessage string) {
context.Set("csrfSecret", auth.CSRFSecret)
context.HTML(http.StatusUnauthorized, "login.tmpl", gin.H{
"error": errorMessage,
"uac16": auth.isUac16(),
"csrf_token": csrf.GetToken(context)})
"csrf_token": auth.CSRFManager.GetToken(context)})
context.Abort()
}

func (auth *Auth) RefreshToken(context *gin.Context, session sessions.Session, claim *UACClaims) {
if session.Get(JWT_TOKEN_KEY) == nil || session.Get(JWT_TOKEN_KEY).(string) == "" {
jwtToken := session.Get(JWT_TOKEN_KEY)
if jwtToken == nil || jwtToken.(string) == "" ||
!auth.SessionValid(context) {
auth.Logger.Info("Not refreshing JWT as it looks like the user has logged out",
append(utils.GetRequestSource(context),
zap.String("InstrumentName", claim.UacInfo.InstrumentName),
zap.String("CaseID", claim.UacInfo.InstrumentName),
)...)
return
}

signedToken, err := auth.JWTCrypto.EncryptJWT(claim.UAC, &claim.UacInfo, claim.AuthTimeout)
if err != nil {
auth.Logger.Error("Failed to Encrypt JWT", zap.Error(err))
Expand All @@ -200,6 +210,23 @@ func (auth *Auth) RefreshToken(context *gin.Context, session sessions.Session, c
return
}

func (auth *Auth) SessionValid(context *gin.Context) bool {
validationSession := sessions.DefaultMany(context, "session_validation")
sessionValid := validationSession.Get(SESSION_VALID_KEY)
if sessionValid == nil {
return false
}
return sessionValid.(bool)
}

func (auth *Auth) clearSessionValidation(context *gin.Context) error {
validationSession := sessions.DefaultMany(context, "session_validation")
validationSession.Set(SESSION_VALID_KEY, false)
validationSession.Clear()
validationSession.Options(sessions.Options{MaxAge: -1})
return validationSession.Save()
}

func (auth *Auth) isUac16() bool {
return auth.UacKind == "uac16"
}
Expand Down
72 changes: 55 additions & 17 deletions authenticate/authenticate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
csrf "github.com/srbry/gin-csrf"
"github.com/stretchr/testify/mock"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
Expand Down Expand Up @@ -43,6 +44,10 @@ var _ = Describe("Login", func() {
session sessions.Session
observedLogs *observer.ObservedLogs
observedZapCore zapcore.Core
csrfManager = &csrf.DefaultCSRFManager{
Secret: "fwibble",
SessionName: "session",
}
)

BeforeEach(func() {
Expand All @@ -53,13 +58,14 @@ var _ = Describe("Login", func() {
JWTCrypto: jwtCrypto,
BlaiseRestApi: mockRestApi,
Logger: observedLogger,
CSRFManager: csrfManager,
}
httpRouter = gin.Default()
httpRouter.LoadHTMLGlob("../templates/*")
store := cookie.NewStore([]byte("secret"))
httpRouter.Use(sessions.Sessions("mysession", store))
httpRouter.Use(sessions.SessionsMany([]string{"session", "user_session", "session_validation"}, store))
httpRouter.POST("/login", func(context *gin.Context) {
session = sessions.Default(context)
session = sessions.DefaultMany(context, "user_session")
auth.Login(context, session)
})
})
Expand Down Expand Up @@ -374,16 +380,20 @@ var _ = Describe("Logout", func() {
httpRouter *gin.Engine
httpRecorder *httptest.ResponseRecorder
session sessions.Session
auth = &authenticate.Auth{}
csrfManager = &csrf.DefaultCSRFManager{
Secret: "fwibble",
SessionName: "session",
}
auth = &authenticate.Auth{CSRFManager: csrfManager}
)

BeforeEach(func() {
httpRouter = gin.Default()
httpRouter.LoadHTMLGlob("../templates/*")
store := cookie.NewStore([]byte("secret"))
httpRouter.Use(sessions.Sessions("mysession", store))
httpRouter.Use(sessions.SessionsMany([]string{"session", "user_session", "session_validation"}, store))
httpRouter.GET("/logout", func(context *gin.Context) {
session = sessions.Default(context)
session = sessions.DefaultMany(context, "user_session")
session.Set("foobar", "fizzbuzz")
session.Save()
Expect(session.Get("foobar")).ToNot(BeNil())
Expand Down Expand Up @@ -412,18 +422,24 @@ var _ = Describe("AuthenticatedWithUac", func() {
session sessions.Session

mockJwtCrypto = &mockauth.JWTCryptoInterface{}
auth = &authenticate.Auth{
JWTCrypto: mockJwtCrypto,
csrfManager = &csrf.DefaultCSRFManager{
Secret: "fwibble",
SessionName: "session",
}
auth = &authenticate.Auth{
JWTCrypto: mockJwtCrypto,
CSRFManager: csrfManager,
}
httpRecorder *httptest.ResponseRecorder
httpRouter *gin.Engine
sessionValid = false
)

BeforeEach(func() {
httpRouter = gin.Default()
httpRouter.LoadHTMLGlob("../templates/*")
store := cookie.NewStore([]byte("secret"))
httpRouter.Use(sessions.Sessions("mysession", store))
httpRouter.Use(sessions.SessionsMany([]string{"session", "user_session", "session_validation"}, store))
})

AfterEach(func() {
Expand All @@ -441,9 +457,13 @@ var _ = Describe("AuthenticatedWithUac", func() {
Context("when there is a token", func() {
BeforeEach(func() {
httpRouter.Use(func(context *gin.Context) {
session = sessions.Default(context)
session = sessions.DefaultMany(context, "user_session")
session.Set(authenticate.JWT_TOKEN_KEY, "foobar")
session.Save()

sessionValidation := sessions.DefaultMany(context, "session_validation")
sessionValidation.Set(authenticate.SESSION_VALID_KEY, sessionValid)
sessionValidation.Save()
context.Next()
})

Expand All @@ -458,10 +478,28 @@ var _ = Describe("AuthenticatedWithUac", func() {
mockJwtCrypto.On("DecryptJWT", mock.Anything).Return(nil, nil)
})

It("Allows the context to continue", func() {
Expect(httpRecorder.Code).To(Equal(http.StatusOK))
body := httpRecorder.Body.Bytes()
Expect(string(body)).To(Equal("true"))
Context("and the session is valid", func() {
BeforeEach(func() {
sessionValid = true
})

It("Allows the context to continue", func() {
Expect(httpRecorder.Code).To(Equal(http.StatusOK))
body := httpRecorder.Body.Bytes()
Expect(string(body)).To(Equal("true"))
})
})

Context("and the session is invalid", func() {
BeforeEach(func() {
sessionValid = false
})

It("returns unauthorized", func() {
Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized))
body := httpRecorder.Body.Bytes()
Expect(strings.Contains(string(body), `<span class="btn__inner">Access study`)).To(BeTrue())
})
})
})

Expand All @@ -470,7 +508,7 @@ var _ = Describe("AuthenticatedWithUac", func() {
mockJwtCrypto.On("DecryptJWT", mock.Anything).Return(nil, fmt.Errorf("Explosions"))
})

It("return unauthorized", func() {
It("returns unauthorized", func() {
Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized))
body := httpRecorder.Body.Bytes()
Expect(strings.Contains(string(body), `<span class="btn__inner">Access study`)).To(BeTrue())
Expand All @@ -486,7 +524,7 @@ var _ = Describe("AuthenticatedWithUac", func() {
})
})

It("return unauthorized", func() {
It("returns unauthorized", func() {
Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized))
body := httpRecorder.Body.Bytes()
Expect(strings.Contains(string(body), `<span class="btn__inner">Access study`)).To(BeTrue())
Expand All @@ -512,10 +550,10 @@ var _ = Describe("Has Session", func() {
httpRouter = gin.Default()
httpRouter.LoadHTMLGlob("../templates/*")
store := cookie.NewStore([]byte("secret"))
httpRouter.Use(sessions.Sessions("mysession", store))
httpRouter.Use(sessions.SessionsMany([]string{"session", "user_session", "session_validation"}, store))

httpRouter.Use(func(context *gin.Context) {
session = sessions.Default(context)
session = sessions.DefaultMany(context, "user_session")
session.Set(authenticate.JWT_TOKEN_KEY, "foobar")
session.Save()
context.Next()
Expand Down
14 changes: 8 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@ go 1.15

require (
github.com/blendle/zapdriver v1.3.1
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 // indirect
github.com/gin-contrib/secure v0.0.1
github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.7.2
github.com/gin-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.7.7
github.com/golang-jwt/jwt v3.2.1+incompatible
github.com/gorilla/sessions v1.2.1 // indirect
github.com/jarcoal/httpmock v1.0.8
github.com/kelseyhightower/envconfig v1.4.0
github.com/onsi/ginkgo v1.16.4
github.com/onsi/gomega v1.10.1
github.com/srbry/gin-csrf v0.0.0-20211221152635-387e490c81de
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.7.0
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
github.com/vektra/mockery/v2 v2.9.4 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.19.1
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
google.golang.org/api v0.50.0
)

replace github.com/gin-contrib/sessions v0.0.4 => github.com/srbry/sessions v0.0.5
Loading

0 comments on commit 30857d4

Please sign in to comment.