Skip to content

Commit

Permalink
refactor: rfc
Browse files Browse the repository at this point in the history
  • Loading branch information
james-d-elliott committed Mar 10, 2024
1 parent 9afec8b commit 963c25f
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 40 deletions.
80 changes: 40 additions & 40 deletions handler/oauth2/introspector_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,46 @@ type StatelessJWTValidator struct {
}
}

func (v *StatelessJWTValidator) IntrospectToken(ctx context.Context, token string, tokenUse oauth2.TokenUse, accessRequest oauth2.AccessRequester, scopes []string) (oauth2.TokenUse, error) {
t, err := validate(ctx, v.Signer, token)
if err != nil {
return "", err
}

if !IsJWTProfileAccessToken(t) {
return "", errorsx.WithStack(oauth2.ErrRequestUnauthorized.WithDebug("The provided token is not a valid RFC9068 JWT Profile Access Token as it is missing the header 'typ' value of 'at+jwt' "))
}

requester := AccessTokenJWTToRequest(t)

if err := matchScopes(v.Config.GetScopeStrategy(ctx), requester.GetGrantedScopes(), scopes); err != nil {
return oauth2.AccessToken, err
}

accessRequest.Merge(requester)

return oauth2.AccessToken, nil
}

// IsJWTProfileAccessToken validates a *jwt.Token is actually a RFC9068 JWT Profile Access Token by checking the
// relevant header as per https://datatracker.ietf.org/doc/html/rfc9068#section-2.1 which explicitly states that
// the header MUST include a typ of 'at+jwt' or 'application/at+jwt' with a preference of 'at+jwt'.
func IsJWTProfileAccessToken(token *jwt.Token) bool {
var (
raw any
typ string
ok bool
)

if raw, ok = token.Header[jwt.JWTHeaderKeyValueType]; !ok {
return false
}

typ, ok = raw.(string)

return ok && (typ == jwt.JWTHeaderTypeValueAccessTokenJWT || typ == "application/at+jwt")
}

// AccessTokenJWTToRequest tries to reconstruct oauth2.Request from a JWT.
func AccessTokenJWTToRequest(token *jwt.Token) oauth2.Requester {
mapClaims := token.Claims
Expand Down Expand Up @@ -68,43 +108,3 @@ func AccessTokenJWTToRequest(token *jwt.Token) oauth2.Requester {
GrantedAudience: claims.Audience,
}
}

func (v *StatelessJWTValidator) IntrospectToken(ctx context.Context, token string, tokenUse oauth2.TokenUse, accessRequest oauth2.AccessRequester, scopes []string) (oauth2.TokenUse, error) {
t, err := validate(ctx, v.Signer, token)
if err != nil {
return "", err
}

if !IsJWTProfileAccessToken(t) {
return "", errorsx.WithStack(oauth2.ErrRequestUnauthorized.WithDebug("The provided token is not a valid RFC9068 JWT Profile Access Token as it is missing the header 'typ' value of 'at+jwt' "))
}

requester := AccessTokenJWTToRequest(t)

if err := matchScopes(v.Config.GetScopeStrategy(ctx), requester.GetGrantedScopes(), scopes); err != nil {
return oauth2.AccessToken, err
}

accessRequest.Merge(requester)

return oauth2.AccessToken, nil
}

// IsJWTProfileAccessToken validates a *jwt.Token is actually a RFC9068 JWT Profile Access Token by checking the
// relevant header as per https://datatracker.ietf.org/doc/html/rfc9068#section-2.1 which explicitly states that
// the header MUST include a typ of 'at+jwt' or 'application/at+jwt' with a preference of 'at+jwt'.
func IsJWTProfileAccessToken(token *jwt.Token) bool {
var (
raw any
typ string
ok bool
)

if raw, ok = token.Header[jwt.JWTHeaderKeyValueType]; !ok {
return false
}

typ, ok = raw.(string)

return ok && (typ == jwt.JWTHeaderTypeValueAccessTokenJWT || typ == "application/at+jwt")
}
138 changes: 138 additions & 0 deletions introspection_request_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"testing"
"time"

"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
Expand All @@ -18,9 +19,11 @@ import (
"authelia.com/provider/oauth2"
. "authelia.com/provider/oauth2"
"authelia.com/provider/oauth2/compose"
"authelia.com/provider/oauth2/handler/openid"
"authelia.com/provider/oauth2/internal"
"authelia.com/provider/oauth2/internal/consts"
"authelia.com/provider/oauth2/storage"
"authelia.com/provider/oauth2/token/jwt"
)

func TestIntrospectionResponseTokenUse(t *testing.T) {
Expand Down Expand Up @@ -221,3 +224,138 @@ func TestNewIntrospectionRequest(t *testing.T) {
})
}
}

func TestIntrospectionResponseToMap(t *testing.T) {
testCases := []struct {
name string
have IntrospectionResponder
expectedaud []string
expected map[string]any
}{
{
"ShouldDecodeInactive",
&IntrospectionResponse{},
nil,
map[string]any{consts.ClaimActive: false},
},
{
"ShouldReturnActiveWithoutAccessRequester",
&IntrospectionResponse{
Active: true,
},
nil,
map[string]any{consts.ClaimActive: true},
},
{
"ShouldReturnActiveWithAccessRequester",
&IntrospectionResponse{
Active: true,
AccessRequester: &AccessRequest{
Request: Request{
RequestedAt: time.Unix(100000, 0).UTC(),
GrantedScope: Arguments{consts.ScopeOpenID, "profile"},
GrantedAudience: Arguments{"https://example.com", "aclient"},
Client: &DefaultClient{ID: "aclient"},
},
},
},
nil,
map[string]any{
consts.ClaimActive: true,
consts.ClaimScope: "openid profile",
consts.ClaimAudience: []string{"https://example.com", "aclient"},
consts.ClaimIssuedAt: int64(100000),
consts.ClaimClientIdentifier: "aclient",
},
},
{
"ShouldReturnActiveWithAccessRequesterAndSession",
&IntrospectionResponse{
Active: true,
AccessRequester: &AccessRequest{
Request: Request{
RequestedAt: time.Unix(100000, 0).UTC(),
GrantedScope: Arguments{consts.ScopeOpenID, "profile"},
GrantedAudience: Arguments{"https://example.com", "aclient"},
Client: &DefaultClient{ID: "aclient"},
Session: &openid.DefaultSession{
ExpiresAt: map[TokenType]time.Time{
AccessToken: time.Unix(1000000, 0).UTC(),
},
Subject: "asubj",
Claims: &jwt.IDTokenClaims{
Extra: map[string]any{
"aclaim": 1,
consts.ClaimExpirationTime: 0,
},
},
},
},
},
},
nil,
map[string]any{
consts.ClaimActive: true,
consts.ClaimScope: "openid profile",
consts.ClaimAudience: []string{"https://example.com", "aclient"},
consts.ClaimIssuedAt: int64(100000),
consts.ClaimClientIdentifier: "aclient",
//"aclaim": 1,
//consts.ClaimSubject: "asubj",
//consts.ClaimExpirationTime: int64(1000000),
},
},
{
"ShouldReturnActiveWithAccessRequesterAndSessionWithIDTokenClaimsAndUsername",
&IntrospectionResponse{
Client: &DefaultClient{
ID: "rclient",
Audience: []string{"https://rs.example.com"},
},
Active: true,
AccessRequester: &AccessRequest{
Request: Request{
RequestedAt: time.Unix(100000, 0).UTC(),
GrantedScope: Arguments{consts.ScopeOpenID, "profile"},
GrantedAudience: Arguments{"https://example.com", "aclient"},
Client: &DefaultClient{ID: "aclient"},
Session: &openid.DefaultSession{
ExpiresAt: map[TokenType]time.Time{
AccessToken: time.Unix(1000000, 0).UTC(),
},
Username: "auser",
Claims: &jwt.IDTokenClaims{
Subject: "asubj",
Extra: map[string]any{
"aclaim": 1,
consts.ClaimExpirationTime: 0,
},
},
},
},
},
},
[]string{"rclient"},
map[string]any{
consts.ClaimActive: true,
consts.ClaimScope: "openid profile",
consts.ClaimAudience: []string{"https://example.com", "aclient"},
consts.ClaimIssuedAt: int64(100000),
consts.ClaimClientIdentifier: "aclient",
//"aclaim": 1,
//consts.ClaimSubject: "asubj",
//consts.ClaimExpirationTime: int64(1000000),
//consts.ClaimUsername: "auser",
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
aud, introspection := tc.have.ToMap()

assert.Equal(t, tc.expectedaud, aud)
assert.Equal(t, tc.expected, introspection)
})
}
}

0 comments on commit 963c25f

Please sign in to comment.