Skip to content

Commit

Permalink
Merge 7959fce into 25cc6c4
Browse files Browse the repository at this point in the history
  • Loading branch information
aeneasr committed Nov 16, 2018
2 parents 25cc6c4 + 7959fce commit bf4826f
Show file tree
Hide file tree
Showing 21 changed files with 251 additions and 39 deletions.
7 changes: 7 additions & 0 deletions HISTORY.md
Expand Up @@ -63,6 +63,13 @@ bumps (`0.1.0` -> `0.2.0`).

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## 0.28.0

This version (re-)introduces refresh token lifespans. Per default, this feature is enabled and set to 30 days.
If a refresh token has not been used within 30 days, it will expire.

To disable refresh token lifespans (previous behaviour), set `compose.Config.RefreshTokenLifespan = -1`.

## 0.27.0

This PR adds the ability to specify a target audience for OAuth 2.0 Access Tokens.
Expand Down
9 changes: 6 additions & 3 deletions compose/compose_oauth2.go
Expand Up @@ -34,6 +34,7 @@ func OAuth2AuthorizeExplicitFactory(config *Config, storage interface{}, strateg
AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy),
CoreStorage: storage.(oauth2.CoreStorage),
AuthCodeLifespan: config.GetAuthorizeCodeLifespan(),
RefreshTokenLifespan: config.GetRefreshTokenLifespan(),
AccessTokenLifespan: config.GetAccessTokenLifespan(),
ScopeStrategy: config.GetScopeStrategy(),
AudienceMatchingStrategy: config.GetAudienceStrategy(),
Expand Down Expand Up @@ -63,6 +64,7 @@ func OAuth2RefreshTokenGrantFactory(config *Config, storage interface{}, strateg
RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy),
TokenRevocationStorage: storage.(oauth2.TokenRevocationStorage),
AccessTokenLifespan: config.GetAccessTokenLifespan(),
RefreshTokenLifespan: config.GetRefreshTokenLifespan(),
ScopeStrategy: config.GetScopeStrategy(),
AudienceMatchingStrategy: config.GetAudienceStrategy(),
}
Expand All @@ -86,9 +88,10 @@ func OAuth2ResourceOwnerPasswordCredentialsFactory(config *Config, storage inter
return &oauth2.ResourceOwnerPasswordCredentialsGrantHandler{
ResourceOwnerPasswordCredentialsGrantStorage: storage.(oauth2.ResourceOwnerPasswordCredentialsGrantStorage),
HandleHelper: &oauth2.HandleHelper{
AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy),
AccessTokenStorage: storage.(oauth2.AccessTokenStorage),
AccessTokenLifespan: config.GetAccessTokenLifespan(),
AccessTokenStrategy: strategy.(oauth2.AccessTokenStrategy),
AccessTokenStorage: storage.(oauth2.AccessTokenStorage),
AccessTokenLifespan: config.GetAccessTokenLifespan(),
RefreshTokenLifespan: config.GetRefreshTokenLifespan(),
},
RefreshTokenStrategy: strategy.(oauth2.RefreshTokenStrategy),
ScopeStrategy: config.GetScopeStrategy(),
Expand Down
1 change: 1 addition & 0 deletions compose/compose_openid.go
Expand Up @@ -81,6 +81,7 @@ func OpenIDConnectHybridFactory(config *Config, storage interface{}, strategy in
CoreStorage: storage.(oauth2.CoreStorage),
AuthCodeLifespan: config.GetAuthorizeCodeLifespan(),
AccessTokenLifespan: config.GetAccessTokenLifespan(),
RefreshTokenLifespan: config.GetRefreshTokenLifespan(),
},
ScopeStrategy: config.GetScopeStrategy(),
AuthorizeImplicitGrantTypeHandler: &oauth2.AuthorizeImplicitGrantTypeHandler{
Expand Down
1 change: 1 addition & 0 deletions compose/compose_strategy.go
Expand Up @@ -44,6 +44,7 @@ func NewOAuth2HMACStrategy(config *Config, secret []byte, rotatedSecrets [][]byt
},
AccessTokenLifespan: config.GetAccessTokenLifespan(),
AuthorizeCodeLifespan: config.GetAuthorizeCodeLifespan(),
RefreshTokenLifespan: config.GetRefreshTokenLifespan(),
}
}

Expand Down
15 changes: 14 additions & 1 deletion compose/config.go
Expand Up @@ -31,6 +31,10 @@ type Config struct {
// AccessTokenLifespan sets how long an access token is going to be valid. Defaults to one hour.
AccessTokenLifespan time.Duration

// RefreshTokenLifespan sets how long a refresh token is going to be valid. Defaults to 30 days. Set to -1 for
// refresh tokens that never expire.
RefreshTokenLifespan time.Duration

// AuthorizeCodeLifespan sets how long an authorize code is going to be valid. Defaults to fifteen minutes.
AuthorizeCodeLifespan time.Duration

Expand Down Expand Up @@ -108,14 +112,23 @@ func (c *Config) GetIDTokenLifespan() time.Duration {
return c.IDTokenLifespan
}

// GetAccessTokenLifespan returns how long a refresh token should be valid. Defaults to one hour.
// GetAccessTokenLifespan returns how long an access token should be valid. Defaults to one hour.
func (c *Config) GetAccessTokenLifespan() time.Duration {
if c.AccessTokenLifespan == 0 {
return time.Hour
}
return c.AccessTokenLifespan
}

// GetRefreshTokenLifespan sets how long a refresh token is going to be valid. Defaults to 30 days. Set to -1 for
// refresh tokens that never expire.
func (c *Config) GetRefreshTokenLifespan() time.Duration {
if c.RefreshTokenLifespan == 0 {
return time.Hour * 24 * 30
}
return c.RefreshTokenLifespan
}

// GetHashCost returns the bcrypt cost factor. Defaults to 12.
func (c *Config) GetHashCost() int {
if c.HashCost == 0 {
Expand Down
3 changes: 3 additions & 0 deletions handler/oauth2/flow_authorize_code_auth.go
Expand Up @@ -46,6 +46,9 @@ type AuthorizeExplicitGrantHandler struct {
// AccessTokenLifespan defines the lifetime of an access token.
AccessTokenLifespan time.Duration

// RefreshTokenLifespan defines the lifetime of a refresh token. Leave to 0 for unlimited lifetime.
RefreshTokenLifespan time.Duration

ScopeStrategy fosite.ScopeStrategy
AudienceMatchingStrategy fosite.AudienceMatchingStrategy

Expand Down
7 changes: 6 additions & 1 deletion handler/oauth2/flow_authorize_code_token.go
Expand Up @@ -106,8 +106,13 @@ func (c *AuthorizeExplicitGrantHandler) HandleTokenEndpointRequest(ctx context.C
// client MUST authenticate with the authorization server as described
// in Section 3.2.1.
request.SetSession(authorizeRequest.GetSession())
request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan))
request.SetID(authorizeRequest.GetID())

request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan).Round(time.Second))
if c.RefreshTokenLifespan > -1 {
request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(c.RefreshTokenLifespan).Round(time.Second))
}

return nil
}

Expand Down
10 changes: 9 additions & 1 deletion handler/oauth2/flow_authorize_code_token_test.go
Expand Up @@ -26,6 +26,7 @@ import (
"fmt"
"net/url"
"testing" //"time"

//"github.com/golang/mock/gomock"
"time"

Expand Down Expand Up @@ -130,7 +131,6 @@ func TestAuthorizeCode_PopulateTokenEndpointResponse(t *testing.T) {
assert.NotEmpty(t, aresp.AccessToken)
assert.Equal(t, "bearer", aresp.TokenType)
assert.NotEmpty(t, aresp.GetExtra("refresh_token"))
assert.NotEmpty(t, aresp.GetExtra("refresh_token"))
assert.NotEmpty(t, aresp.GetExtra("expires_in"))
assert.Equal(t, "foo offline", aresp.GetExtra("scope"))
},
Expand Down Expand Up @@ -179,6 +179,7 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) {
authreq *fosite.AuthorizeRequest
description string
setup func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.AuthorizeRequest)
check func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.AuthorizeRequest)
expectErr error
}{
{
Expand Down Expand Up @@ -321,6 +322,10 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) {
RequestedAt: time.Now().UTC(),
},
},
check: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.AuthorizeRequest) {
assert.Equal(t, time.Now().Add(time.Minute).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.AccessToken))
assert.Equal(t, time.Now().Add(time.Minute).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.RefreshToken))
},
setup: func(t *testing.T, areq *fosite.AccessRequest, authreq *fosite.AuthorizeRequest) {
code, sig, err := strategy.GenerateAuthorizeCode(nil, nil)
require.NoError(t, err)
Expand All @@ -345,6 +350,9 @@ func TestAuthorizeCode_HandleTokenEndpointRequest(t *testing.T) {
require.EqualError(t, errors.Cause(err), c.expectErr.Error(), "%+v", err)
} else {
require.NoError(t, err, "%+v", err)
if c.check != nil {
c.check(t, c.areq, c.authreq)
}
}
})
}
Expand Down
5 changes: 4 additions & 1 deletion handler/oauth2/flow_authorize_implicit.go
Expand Up @@ -80,7 +80,10 @@ func (c *AuthorizeImplicitGrantTypeHandler) HandleAuthorizeEndpointRequest(ctx c
}

func (c *AuthorizeImplicitGrantTypeHandler) IssueImplicitAccessToken(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error {
ar.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan))
// Only override expiry if none is set.
if ar.GetSession().GetExpiresAt(fosite.AccessToken).IsZero() {
ar.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan).Round(time.Second))
}

// Generate the code
token, signature, err := c.AccessTokenStrategy.GenerateAccessToken(ctx, ar)
Expand Down
12 changes: 9 additions & 3 deletions handler/oauth2/flow_refresh.go
Expand Up @@ -38,6 +38,9 @@ type RefreshTokenGrantHandler struct {
// AccessTokenLifespan defines the lifetime of an access token.
AccessTokenLifespan time.Duration

// RefreshTokenLifespan defines the lifetime of a refresh token.
RefreshTokenLifespan time.Duration

ScopeStrategy fosite.ScopeStrategy
AudienceMatchingStrategy fosite.AudienceMatchingStrategy
}
Expand All @@ -55,14 +58,13 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex
}

refresh := request.GetRequestForm().Get("refresh_token")

signature := c.RefreshTokenStrategy.RefreshTokenSignature(refresh)
originalRequest, err := c.TokenRevocationStorage.GetRefreshTokenSession(ctx, signature, request.GetSession())
if errors.Cause(err) == fosite.ErrNotFound {
return errors.WithStack(fosite.ErrInvalidRequest.WithDebug(err.Error()))
} else if err != nil {
return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error()))
} else if err := c.RefreshTokenStrategy.ValidateRefreshToken(ctx, request, refresh); err != nil {
} else if err := c.RefreshTokenStrategy.ValidateRefreshToken(ctx, originalRequest, refresh); err != nil {
// The authorization server MUST ... validate the refresh token.
// This needs to happen after store retrieval for the session to be hydrated properly
return errors.WithStack(fosite.ErrInvalidRequest.WithDebug(err.Error()))
Expand Down Expand Up @@ -97,7 +99,11 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex
request.GrantAudience(audience)
}

request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan))
request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan).Round(time.Second))
if c.RefreshTokenLifespan > -1 {
request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(c.RefreshTokenLifespan).Round(time.Second))
}

return nil
}

Expand Down
4 changes: 4 additions & 0 deletions handler/oauth2/flow_refresh_test.go
Expand Up @@ -48,6 +48,7 @@ func TestRefreshFlow_HandleTokenEndpointRequest(t *testing.T) {
TokenRevocationStorage: store,
RefreshTokenStrategy: strategy,
AccessTokenLifespan: time.Hour,
RefreshTokenLifespan: time.Hour,
ScopeStrategy: fosite.HierarchicScopeStrategy,
AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy,
}
Expand Down Expand Up @@ -103,6 +104,7 @@ func TestRefreshFlow_HandleTokenEndpointRequest(t *testing.T) {
err = store.CreateRefreshTokenSession(nil, sig, &fosite.Request{
Client: &fosite.DefaultClient{ID: ""},
GrantedScope: []string{"offline"},
Session: sess,
})
require.NoError(t, err)
},
Expand Down Expand Up @@ -163,6 +165,8 @@ func TestRefreshFlow_HandleTokenEndpointRequest(t *testing.T) {
assert.Equal(t, fosite.Arguments{"foo", "offline"}, areq.GrantedScope)
assert.Equal(t, fosite.Arguments{"foo", "bar", "offline"}, areq.RequestedScope)
assert.NotEqual(t, url.Values{"foo": []string{"bar"}}, areq.Form)
assert.Equal(t, time.Now().Add(time.Hour).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.AccessToken))
assert.Equal(t, time.Now().Add(time.Hour).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.RefreshToken))
},
},
} {
Expand Down
6 changes: 5 additions & 1 deletion handler/oauth2/flow_resource_owner.go
Expand Up @@ -77,7 +77,11 @@ func (c *ResourceOwnerPasswordCredentialsGrantHandler) HandleTokenEndpointReques
// Credentials must not be passed around, potentially leaking to the database!
delete(request.GetRequestForm(), "password")

request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan))
request.GetSession().SetExpiresAt(fosite.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan).Round(time.Second))
if c.RefreshTokenLifespan > -1 {
request.GetSession().SetExpiresAt(fosite.RefreshToken, time.Now().UTC().Add(c.RefreshTokenLifespan).Round(time.Second))
}

return nil
}

Expand Down
9 changes: 6 additions & 3 deletions handler/oauth2/flow_resource_owner_test.go
Expand Up @@ -47,8 +47,9 @@ func TestResourceOwnerFlow_HandleTokenEndpointRequest(t *testing.T) {
h := ResourceOwnerPasswordCredentialsGrantHandler{
ResourceOwnerPasswordCredentialsGrantStorage: store,
HandleHelper: &HandleHelper{
AccessTokenStorage: store,
AccessTokenLifespan: time.Hour,
AccessTokenStorage: store,
AccessTokenLifespan: time.Hour,
RefreshTokenLifespan: time.Hour,
},
ScopeStrategy: fosite.HierarchicScopeStrategy,
AudienceMatchingStrategy: fosite.DefaultAudienceMatchingStrategy,
Expand Down Expand Up @@ -107,7 +108,9 @@ func TestResourceOwnerFlow_HandleTokenEndpointRequest(t *testing.T) {
store.EXPECT().Authenticate(nil, "peter", "pan").Return(nil)
},
check: func(areq *fosite.AccessRequest) {
assert.NotEmpty(t, areq.GetSession().GetExpiresAt(fosite.AccessToken))
//assert.NotEmpty(t, areq.GetSession().GetExpiresAt(fosite.AccessToken))
assert.Equal(t, time.Now().Add(time.Hour).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.AccessToken))
assert.Equal(t, time.Now().Add(time.Hour).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.RefreshToken))
},
},
} {
Expand Down
7 changes: 4 additions & 3 deletions handler/oauth2/helper.go
Expand Up @@ -29,9 +29,10 @@ import (
)

type HandleHelper struct {
AccessTokenStrategy AccessTokenStrategy
AccessTokenStorage AccessTokenStorage
AccessTokenLifespan time.Duration
AccessTokenStrategy AccessTokenStrategy
AccessTokenStorage AccessTokenStorage
AccessTokenLifespan time.Duration
RefreshTokenLifespan time.Duration
}

func (h *HandleHelper) IssueAccessToken(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error {
Expand Down
11 changes: 10 additions & 1 deletion handler/oauth2/strategy_hmacsha.go
Expand Up @@ -34,6 +34,7 @@ import (
type HMACSHAStrategy struct {
Enigma *enigma.HMACStrategy
AccessTokenLifespan time.Duration
RefreshTokenLifespan time.Duration
AuthorizeCodeLifespan time.Duration
}

Expand Down Expand Up @@ -66,7 +67,15 @@ func (h HMACSHAStrategy) GenerateRefreshToken(_ context.Context, _ fosite.Reques
return h.Enigma.Generate()
}

func (h HMACSHAStrategy) ValidateRefreshToken(_ context.Context, _ fosite.Requester, token string) (err error) {
func (h HMACSHAStrategy) ValidateRefreshToken(_ context.Context, r fosite.Requester, token string) (err error) {
var exp = r.GetSession().GetExpiresAt(fosite.RefreshToken)
if exp.IsZero() {
// Unlimited lifetime
return h.Enigma.Validate(token)
}
if !exp.IsZero() && exp.Before(time.Now().UTC()) {
return errors.WithStack(fosite.ErrTokenExpired.WithHintf("Refresh token expired at \"%s\".", exp))
}
return h.Enigma.Validate(token)
}

Expand Down
49 changes: 42 additions & 7 deletions handler/oauth2/strategy_hmacsha_test.go
Expand Up @@ -47,6 +47,7 @@ var hmacExpiredCase = fosite.Request{
ExpiresAt: map[fosite.TokenType]time.Time{
fosite.AccessToken: time.Now().UTC().Add(-time.Hour),
fosite.AuthorizeCode: time.Now().UTC().Add(-time.Hour),
fosite.RefreshToken: time.Now().UTC().Add(-time.Hour),
},
},
}
Expand All @@ -59,6 +60,20 @@ var hmacValidCase = fosite.Request{
ExpiresAt: map[fosite.TokenType]time.Time{
fosite.AccessToken: time.Now().UTC().Add(time.Hour),
fosite.AuthorizeCode: time.Now().UTC().Add(time.Hour),
fosite.RefreshToken: time.Now().UTC().Add(time.Hour),
},
},
}

var hmacValidZeroTimeRefreshCase = fosite.Request{
Client: &fosite.DefaultClient{
Secret: []byte("foobarfoobarfoobarfoobar"),
},
Session: &fosite.DefaultSession{
ExpiresAt: map[fosite.TokenType]time.Time{
fosite.AccessToken: time.Now().UTC().Add(time.Hour),
fosite.AuthorizeCode: time.Now().UTC().Add(time.Hour),
fosite.RefreshToken: {},
},
},
}
Expand Down Expand Up @@ -95,14 +110,34 @@ func TestHMACAccessToken(t *testing.T) {
}

func TestHMACRefreshToken(t *testing.T) {
token, signature, err := hmacshaStrategy.GenerateRefreshToken(nil, &hmacValidCase)
assert.NoError(t, err)
assert.Equal(t, strings.Split(token, ".")[1], signature)
for k, c := range []struct {
r fosite.Request
pass bool
}{
{
r: hmacValidCase,
pass: true,
},
{
r: hmacExpiredCase,
pass: false,
},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
token, signature, err := hmacshaStrategy.GenerateRefreshToken(nil, &c.r)
assert.NoError(t, err)
assert.Equal(t, strings.Split(token, ".")[1], signature)

validate := hmacshaStrategy.Enigma.Signature(token)
err = hmacshaStrategy.ValidateRefreshToken(nil, &hmacValidCase, token)
assert.NoError(t, err)
assert.Equal(t, signature, validate)
err = hmacshaStrategy.ValidateRefreshToken(nil, &c.r, token)
if c.pass {
assert.NoError(t, err)
validate := hmacshaStrategy.Enigma.Signature(token)
assert.Equal(t, signature, validate)
} else {
assert.Error(t, err)
}
})
}
}

func TestHMACAuthorizeCode(t *testing.T) {
Expand Down

0 comments on commit bf4826f

Please sign in to comment.