Skip to content

Commit

Permalink
feat: customizable token prefix (#27)
Browse files Browse the repository at this point in the history
This allows customizing a token prefix. No token prefix is configured by default.
  • Loading branch information
james-d-elliott committed Dec 22, 2023
1 parent 4943345 commit 4f55dab
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 18 deletions.
11 changes: 6 additions & 5 deletions README.md
Expand Up @@ -55,17 +55,18 @@ following list of differences:
<sup>[commit](https://github.com/authelia/oauth2-provider/commit/a87d91df762a8fe26282145ba9dace0461f31b4d)</sup>
- Features:
- [ ] Customizable Token Prefix
- [ ] JWE support for Client Authentication and Issuance
- [ ] UserInfo support
- Response Mode Rework:
- [ ] [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM)](https://openid.net/specs/oauth-v2-jarm.html)
support
- [ ] [RFC9207](https://datatracker.ietf.org/doc/html/rfc9207) OAuth 2.0
Authorization Server Issuer Identification support
- [ ] [RFC9207: OAuth 2.0 Authorization Server Issuer Identification](https://datatracker.ietf.org/doc/html/rfc9207)
support
- [ ] Response Type None
- [x] Revocation Flow per policy can decide to revoke Refresh Tokens on
request <sup>[commit](e3ffc451f1c7056494f9dc3e51d47e84f12357de)</sup>
- [ ] Response Type None
- [ ] Client Secret Validation Interface
- Client Authentication Rework:
- [ ] Client Secret Validation Interface
- [ ] JWE support for Client Authentication and Issuance
- [ ] Clock Drift Support
- [ ] Injectable Clock Configurator
- [x] Support `s_hash`
Expand Down
62 changes: 49 additions & 13 deletions handler/oauth2/strategy_hmacsha.go
Expand Up @@ -14,14 +14,37 @@ import (
"authelia.com/provider/oauth2/token/hmac"
)

// NewHMACSHAStrategy creates a new HMACSHAStrategy with the potential to include the prefix format. The prefix must
// include a single '%s' for the purpose of adding the token part (ac, at, and rt; for the Authorize Code, Access
// Token, and Refresh Token; respectively.
func NewHMACSHAStrategy(config HMACSHAStrategyConfigurator, prefix string) (strategy *HMACSHAStrategy, err error) {
if len(prefix) == 0 {
return &HMACSHAStrategy{
Enigma: &hmac.HMACStrategy{Config: config},
Config: config,
}, nil
}

if n := strings.Count(prefix, "%s"); n != 1 {
return nil, fmt.Errorf("the prefix must contain a single '%%s' but contains %d", n)
}

return &HMACSHAStrategy{
Enigma: &hmac.HMACStrategy{Config: config},
Config: config,
prefix: prefix,
}, nil
}

type HMACSHAStrategy struct {
Enigma *hmac.HMACStrategy
Config interface {
oauth2.AccessTokenLifespanProvider
oauth2.RefreshTokenLifespanProvider
oauth2.AuthorizeCodeLifespanProvider
}
prefix *string

prefix string
}

func (h *HMACSHAStrategy) AccessTokenSignature(ctx context.Context, token string) string {
Expand All @@ -37,14 +60,11 @@ func (h *HMACSHAStrategy) AuthorizeCodeSignature(ctx context.Context, token stri
}

func (h *HMACSHAStrategy) getPrefix(part string) string {
if h.prefix == nil {
prefix := "authelia_%s_"
h.prefix = &prefix
} else if len(*h.prefix) == 0 {
if len(h.prefix) == 0 {
return ""
}

return fmt.Sprintf(*h.prefix, part)
return fmt.Sprintf(h.prefix, part)
}

func (h *HMACSHAStrategy) trimPrefix(token, part string) string {
Expand All @@ -61,7 +81,7 @@ func (h *HMACSHAStrategy) GenerateAccessToken(ctx context.Context, _ oauth2.Requ
return "", "", err
}

return h.setPrefix(token, "at"), sig, nil
return h.setPrefix(token, tokenPartAccessToken), sig, nil
}

func (h *HMACSHAStrategy) ValidateAccessToken(ctx context.Context, r oauth2.Requester, token string) (err error) {
Expand All @@ -74,7 +94,7 @@ func (h *HMACSHAStrategy) ValidateAccessToken(ctx context.Context, r oauth2.Requ
return errorsx.WithStack(oauth2.ErrTokenExpired.WithHintf("Access token expired at '%s'.", exp))
}

return h.Enigma.Validate(ctx, h.trimPrefix(token, "at"))
return h.Enigma.Validate(ctx, h.trimPrefix(token, tokenPartAccessToken))
}

func (h *HMACSHAStrategy) GenerateRefreshToken(ctx context.Context, _ oauth2.Requester) (token string, signature string, err error) {
Expand All @@ -83,21 +103,21 @@ func (h *HMACSHAStrategy) GenerateRefreshToken(ctx context.Context, _ oauth2.Req
return "", "", err
}

return h.setPrefix(token, "rt"), sig, nil
return h.setPrefix(token, tokenPartRefreshToken), sig, nil
}

func (h *HMACSHAStrategy) ValidateRefreshToken(ctx context.Context, r oauth2.Requester, token string) (err error) {
var exp = r.GetSession().GetExpiresAt(oauth2.RefreshToken)
if exp.IsZero() {
// Unlimited lifetime
return h.Enigma.Validate(ctx, h.trimPrefix(token, "rt"))
return h.Enigma.Validate(ctx, h.trimPrefix(token, tokenPartRefreshToken))
}

if !exp.IsZero() && exp.Before(time.Now().UTC()) {
return errorsx.WithStack(oauth2.ErrTokenExpired.WithHintf("Refresh token expired at '%s'.", exp))
}

return h.Enigma.Validate(ctx, h.trimPrefix(token, "rt"))
return h.Enigma.Validate(ctx, h.trimPrefix(token, tokenPartRefreshToken))
}

func (h *HMACSHAStrategy) GenerateAuthorizeCode(ctx context.Context, _ oauth2.Requester) (token string, signature string, err error) {
Expand All @@ -106,7 +126,7 @@ func (h *HMACSHAStrategy) GenerateAuthorizeCode(ctx context.Context, _ oauth2.Re
return "", "", err
}

return h.setPrefix(token, "ac"), sig, nil
return h.setPrefix(token, tokenPartAuthorizeCode), sig, nil
}

func (h *HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r oauth2.Requester, token string) (err error) {
Expand All @@ -119,5 +139,21 @@ func (h *HMACSHAStrategy) ValidateAuthorizeCode(ctx context.Context, r oauth2.Re
return errorsx.WithStack(oauth2.ErrTokenExpired.WithHintf("Authorize code expired at '%s'.", exp))
}

return h.Enigma.Validate(ctx, h.trimPrefix(token, "ac"))
return h.Enigma.Validate(ctx, h.trimPrefix(token, tokenPartAuthorizeCode))
}

const (
tokenPartAuthorizeCode = "ac"
tokenPartAccessToken = "at"
tokenPartRefreshToken = "rt"
)

type HMACSHAStrategyConfigurator interface {
oauth2.AccessTokenLifespanProvider
oauth2.RefreshTokenLifespanProvider
oauth2.AuthorizeCodeLifespanProvider
oauth2.TokenEntropyProvider
oauth2.GlobalSecretProvider
oauth2.RotatedGlobalSecretsProvider
oauth2.HMACHashingProvider
}
55 changes: 55 additions & 0 deletions handler/oauth2/strategy_hmacsha_test.go
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"authelia.com/provider/oauth2"
"authelia.com/provider/oauth2/token/hmac"
Expand All @@ -21,6 +22,7 @@ var hmacshaStrategy = HMACSHAStrategy{
AccessTokenLifespan: time.Hour * 24,
AuthorizeCodeLifespan: time.Hour * 24,
},
prefix: "authelia_%s_",
}

var hmacExpiredCase = oauth2.Request{
Expand Down Expand Up @@ -62,6 +64,59 @@ var hmacValidZeroTimeRefreshCase = oauth2.Request{
},
}

func TestNewHMACSHAStrategy(t *testing.T) {
testCases := []struct {
name string
have string
expectedAT string
expectedRT string
expectedAC string
expected string
}{
{
"ShouldHandleCustom",
"example_%s_",
"example_at_",
"example_rt_",
"example_ac_",
"",
},
{
"ShouldHandleDefault",
"",
"",
"",
"",
"",
},
{
"ShouldHandleInvalidPrefix",
"example_%s_%s_",
"",
"",
"",
"the prefix must contain a single '%s' but contains 2",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual, err := NewHMACSHAStrategy(nil, tc.have)

if len(tc.expected) == 0 {
assert.NoError(t, err)
require.NotNil(t, actual)
assert.Equal(t, tc.expectedAT, actual.getPrefix(tokenPartAccessToken))
assert.Equal(t, tc.expectedRT, actual.getPrefix(tokenPartRefreshToken))
assert.Equal(t, tc.expectedAC, actual.getPrefix(tokenPartAuthorizeCode))
} else {
assert.Nil(t, actual)
assert.EqualError(t, err, tc.expected)
}
})
}
}

func TestHMACAccessToken(t *testing.T) {
for k, c := range []struct {
r oauth2.Request
Expand Down

0 comments on commit 4f55dab

Please sign in to comment.