Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handler/oauth2: allow stateless introspection of jwt access tokens #141

Merged
merged 4 commits into from
Feb 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions authorize_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,22 @@ func TestDoesClientWhiteListRedirect(t *testing.T) {
isError: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some random go fmt changes in this file.

},
{
client: &DefaultClient{RedirectURIs: []string{"wta://auth"}},
url: "wta://auth",
client: &DefaultClient{RedirectURIs: []string{"wta://auth"}},
url: "wta://auth",
expected: "wta://auth",
isError: false,
isError: false,
},
{
client: &DefaultClient{RedirectURIs: []string{"wta:///auth"}},
url: "wta:///auth",
client: &DefaultClient{RedirectURIs: []string{"wta:///auth"}},
url: "wta:///auth",
expected: "wta:///auth",
isError: false,
isError: false,
},
{
client: &DefaultClient{RedirectURIs: []string{"wta://foo/auth"}},
url: "wta://foo/auth",
client: &DefaultClient{RedirectURIs: []string{"wta://foo/auth"}},
url: "wta://foo/auth",
expected: "wta://foo/auth",
isError: false,
isError: false,
},
{
client: &DefaultClient{RedirectURIs: []string{"https://bar.com/cb"}},
Expand Down Expand Up @@ -131,10 +131,10 @@ func TestDoesClientWhiteListRedirect(t *testing.T) {
}

func TestIsRedirectURISecure(t *testing.T) {
for d, c := range []struct{
u string
for d, c := range []struct {
u string
err bool
} {
}{
{u: "http://google.com", err: true},
{u: "https://google.com", err: false},
{u: "http://localhost", err: false},
Expand All @@ -144,4 +144,4 @@ func TestIsRedirectURISecure(t *testing.T) {
require.Nil(t, err)
assert.Equal(t, !c.err, IsRedirectURISecure(uu), "case %d", d)
}
}
}
8 changes: 4 additions & 4 deletions compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/ory-am/fosite"
)

type handler func(config *Config, storage interface{}, strategy interface{}) interface{}
type Factory func(config *Config, storage interface{}, strategy interface{}) interface{}

// Compose takes a config, a storage, a strategy and handlers to instantiate an OAuth2Provider:
//
Expand All @@ -30,7 +30,7 @@ type handler func(config *Config, storage interface{}, strategy interface{}) int
// )
//
// Compose makes use of interface{} types in order to be able to handle a all types of stores, strategies and handlers.
func Compose(config *Config, storage interface{}, strategy interface{}, handlers ...handler) fosite.OAuth2Provider {
func Compose(config *Config, storage interface{}, strategy interface{}, factories ...Factory) fosite.OAuth2Provider {
f := &fosite.Fosite{
Store: storage.(fosite.Storage),
AuthorizeEndpointHandlers: fosite.AuthorizeEndpointHandlers{},
Expand All @@ -41,8 +41,8 @@ func Compose(config *Config, storage interface{}, strategy interface{}, handlers
ScopeStrategy: fosite.HierarchicScopeStrategy,
}

for _, h := range handlers {
res := h(config, storage, strategy)
for _, factory := range factories {
res := factory(config, storage, strategy)
if ah, ok := res.(fosite.AuthorizeEndpointHandler); ok {
f.AuthorizeEndpointHandlers.Append(ah)
}
Expand Down
14 changes: 14 additions & 0 deletions compose/compose_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,17 @@ func OAuth2TokenIntrospectionFactory(config *Config, storage interface{}, strate
ScopeStrategy: fosite.HierarchicScopeStrategy,
}
}

// OAuth2StatelessJWTIntrospectionFactory creates an OAuth2 token introspection handler and
// registers an access token validator. This can only be used to validate JWTs and does so
// statelessly, meaning it uses only the data available in the JWT itself, and does not access the
// storage implementation at all.
//
// Due to the stateless nature of this factory, THE BUILT-IN REVOCATION MECHANISMS WILL NOT WORK.
// If you need revocation, you can validate JWTs statefully, using the other factories.
func OAuth2StatelessJWTIntrospectionFactory(config *Config, storage interface{}, strategy interface{}) interface{} {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely agreed that there's an increased risk of misconfiguration. Hoping renaming this to emphasize the statelessness and adding a cautionary doc block will minimize that risk.

return &oauth2.StatelessJWTValidator{
JWTAccessTokenStrategy: strategy.(oauth2.JWTAccessTokenStrategy),
ScopeStrategy: fosite.HierarchicScopeStrategy,
}
}
43 changes: 43 additions & 0 deletions handler/oauth2/introspector_jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package oauth2

import (
"github.com/ory-am/fosite"
"github.com/pkg/errors"
"golang.org/x/net/context"
)

type JWTAccessTokenStrategy interface {
AccessTokenStrategy
JWTStrategy
}

type StatelessJWTValidator struct {
JWTAccessTokenStrategy
ScopeStrategy fosite.ScopeStrategy
}

func (v *StatelessJWTValidator) IntrospectToken(ctx context.Context, token string, tokenType fosite.TokenType, accessRequest fosite.AccessRequester, scopes []string) (err error) {
or, err := v.JWTAccessTokenStrategy.ValidateJWT(fosite.AccessToken, token)
if err != nil {
return err
}

for _, scope := range scopes {
if scope == "" {
continue
}

if !v.ScopeStrategy(or.GetGrantedScopes(), scope) {
return errors.WithStack(fosite.ErrInvalidScope)
}
}

accessRequest.Merge(or)
return nil
}

// Revocation is not supported with the stateless validator. If you need revocation, use the
// CoreValidator struct instead.
func (v *StatelessJWTValidator) RevokeToken(ctx context.Context, token string, tokenType fosite.TokenType) error {
return errors.Wrap(fosite.ErrMisconfiguration, "Token revocation is not supported")
}
127 changes: 127 additions & 0 deletions handler/oauth2/introspector_jwt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package oauth2

import (
"encoding/base64"
"strings"
"testing"

"github.com/ory-am/fosite"
"github.com/ory-am/fosite/internal"
"github.com/ory-am/fosite/token/jwt"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)

func TestIntrospectJWT(t *testing.T) {
strat := &RS256JWTStrategy{
RS256JWTStrategy: &jwt.RS256JWTStrategy{
PrivateKey: internal.MustRSAKey(),
},
}

v := &StatelessJWTValidator{
JWTAccessTokenStrategy: strat,
ScopeStrategy: fosite.HierarchicScopeStrategy,
}

for k, c := range []struct {
description string
token func() string
expectErr error
scopes []string
}{
{
description: "should fail because jwt is expired",
token: func() string {
jwt := jwtExpiredCase(fosite.AccessToken)
token, _, err := strat.GenerateAccessToken(nil, jwt)
assert.NoError(t, err)
return token
},
expectErr: fosite.ErrTokenExpired,
},
{
description: "should pass because scope was granted",
token: func() string {
jwt := jwtValidCase(fosite.AccessToken)
jwt.GrantedScopes = []string{"foo", "bar"}
token, _, err := strat.GenerateAccessToken(nil, jwt)
assert.NoError(t, err)
return token
},
scopes: []string{"foo"},
},
{
description: "should fail because scope was not granted",
token: func() string {
jwt := jwtValidCase(fosite.AccessToken)
token, _, err := strat.GenerateAccessToken(nil, jwt)
assert.NoError(t, err)
return token
},
scopes: []string{"foo"},
expectErr: fosite.ErrInvalidScope,
},
{
description: "should fail because signature is invalid",
token: func() string {
jwt := jwtValidCase(fosite.AccessToken)
token, _, err := strat.GenerateAccessToken(nil, jwt)
assert.NoError(t, err)
parts := strings.Split(token, ".")
dec, err := base64.RawURLEncoding.DecodeString(parts[1])
assert.NoError(t, err)
s := strings.Replace(string(dec), "peter", "piper", -1)
parts[1] = base64.RawURLEncoding.EncodeToString([]byte(s))
return strings.Join(parts, ".")
},
expectErr: fosite.ErrTokenSignatureMismatch,
},
{
description: "should pass",
token: func() string {
jwt := jwtValidCase(fosite.AccessToken)
token, _, err := strat.GenerateAccessToken(nil, jwt)
assert.NoError(t, err)
return token
},
},
} {
if c.scopes == nil {
c.scopes = []string{}
}
areq := fosite.NewAccessRequest(nil)
err := v.IntrospectToken(nil, c.token(), fosite.AccessToken, areq, c.scopes)

assert.True(t, errors.Cause(err) == c.expectErr, "(%d) %s\n%s\n%s", k, c.description, err, c.expectErr)

if err == nil {
assert.Equal(t, "peter", areq.Session.GetSubject())
}

t.Logf("Passed test case %d", k)
}
}

func BenchmarkIntrospectJWT(b *testing.B) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There aren't any other benchmarks, so maybe performance is a non-goal of the library, but this was of interest to me. Understood if you want to delete in the interest of staying slim. It's certainly not useful if no one ever runs it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

strat := &RS256JWTStrategy{
RS256JWTStrategy: &jwt.RS256JWTStrategy{
PrivateKey: internal.MustRSAKey(),
},
}

v := &StatelessJWTValidator{
JWTAccessTokenStrategy: strat,
}

jwt := jwtValidCase(fosite.AccessToken)
token, _, err := strat.GenerateAccessToken(nil, jwt)
assert.NoError(b, err)
areq := fosite.NewAccessRequest(nil)

for n := 0; n < b.N; n++ {
err = v.IntrospectToken(nil, token, fosite.AccessToken, areq, []string{})
}

assert.NoError(b, err)
}
4 changes: 4 additions & 0 deletions handler/oauth2/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ type CoreStrategy interface {
AuthorizeCodeStrategy
}

type JWTStrategy interface {
ValidateJWT(tokenType fosite.TokenType, token string) (requester fosite.Requester, err error)
}

type AccessTokenStrategy interface {
AccessTokenSignature(token string) string
GenerateAccessToken(ctx context.Context, requester fosite.Requester) (token string, signature string, err error)
Expand Down
Loading