-
-
Notifications
You must be signed in to change notification settings - Fork 364
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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{} { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
} | ||
} |
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") | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
There was a problem hiding this comment.
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.