Skip to content

Commit

Permalink
feat: add time-boxed sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
hf committed Oct 31, 2023
1 parent 2385212 commit 3298ffc
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 3 deletions.
10 changes: 10 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ type API struct {
db *storage.Connection
config *conf.GlobalConfiguration
version string

overrideTime func() time.Time
}

func (a *API) Now() time.Time {
if a.overrideTime != nil {
return a.overrideTime()
}

return time.Now()
}

// NewAPI instantiates a new REST API
Expand Down
14 changes: 11 additions & 3 deletions internal/api/token_refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
// Instead of waiting at the database level, they're waiting at the API
// level instead and retry to refresh the locked row every 10-30
// milliseconds.
retryStart := time.Now()
retryStart := a.Now()
retry := true

for retry && time.Since(retryStart).Seconds() < retryLoopDuration {
Expand All @@ -69,7 +69,15 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
notAfter = *session.NotAfter
}

if !notAfter.IsZero() && time.Now().UTC().After(notAfter) {
if config.Sessions.Timebox != nil {
sessionEndsAt := session.CreatedAt.Add((*config.Sessions.Timebox).Abs())

if notAfter.IsZero() || notAfter.After(sessionEndsAt) {
notAfter = sessionEndsAt
}
}

if !notAfter.IsZero() && a.Now().After(notAfter) {
return oauthError("invalid_grant", "Invalid Refresh Token: Session Expired")
}
}
Expand Down Expand Up @@ -130,7 +138,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
reuseUntil := token.UpdatedAt.Add(
time.Second * time.Duration(config.Security.RefreshTokenReuseInterval))

if time.Now().After(reuseUntil) {
if a.Now().After(reuseUntil) {
a.clearCookieTokens(config, w)
// not OK to reuse this token

Expand Down
34 changes: 34 additions & 0 deletions internal/api/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,40 @@ func (ts *TokenTestSuite) SetupTest() {
require.NoError(ts.T(), err, "Error creating refresh token")
}

func (ts *TokenTestSuite) TestSessionTimebox() {
timebox := 10 * time.Second

ts.API.config.Sessions.Timebox = &timebox
ts.API.overrideTime = func() time.Time {
return time.Now().Add(timebox).Add(time.Second)
}

defer func() {
ts.API.overrideTime = nil
}()

var buffer bytes.Buffer
require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
"refresh_token": ts.RefreshToken.Token,
}))

req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &buffer)
req.Header.Set("Content-Type", "application/json")

w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
assert.Equal(ts.T(), http.StatusBadRequest, w.Code)

var firstResult struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}

assert.NoError(ts.T(), json.NewDecoder(w.Result().Body).Decode(&firstResult))
assert.Equal(ts.T(), "invalid_grant", firstResult.Error)
assert.Equal(ts.T(), "Invalid Refresh Token: Session Expired", firstResult.ErrorDescription)
}

func (ts *TokenTestSuite) TestFailedToSaveRefreshTokenResultCase() {
var buffer bytes.Buffer

Expand Down
5 changes: 5 additions & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ func (a *APIConfiguration) Validate() error {
return nil
}

type SessionsConfiguration struct {
Timebox *time.Duration `json:"timebox"`
}

// GlobalConfiguration holds all the configuration that applies to all instances.
type GlobalConfiguration struct {
API APIConfiguration
Expand Down Expand Up @@ -139,6 +143,7 @@ type GlobalConfiguration struct {
DisableSignup bool `json:"disable_signup" split_words:"true"`
Webhook WebhookConfig `json:"webhook" split_words:"true"`
Security SecurityConfiguration `json:"security"`
Sessions SessionsConfiguration `json:"sessions"`
MFA MFAConfiguration `json:"MFA"`
Cookie struct {
Key string `json:"key"`
Expand Down

0 comments on commit 3298ffc

Please sign in to comment.