Skip to content

Commit

Permalink
all: composable factories, better token validation, better scope hand…
Browse files Browse the repository at this point in the history
…ling and simplify structure

* readme: add gitter chat badge closes #67
* handler: flatten packages closes #70
* openid: don't autogrant openid scope - closes #68
* all: clean up scopes / arguments - closes #66
* all: composable factories - closes #64
* all: refactor token validation - closes #63
* all: remove mandatory scope - closes #62
  • Loading branch information
arekkas authored Aug 6, 2016
1 parent 6c40c08 commit a92c755
Show file tree
Hide file tree
Showing 100 changed files with 2,038 additions and 1,594 deletions.
21 changes: 21 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
This is a list of breaking changes. As long as `1.0.0` is not released, breaking changes will be addressed as minor version
bumps (`0.1.0` -> `0.2.0`).

## 0.2.0

Breaking changes:

* Token validation refactored: `ValidateRequestAuthorization` is now `Validate` and does not require a http request
but instead a token and a token hint. A token can be anything, including authorization codes, refresh tokens,
id tokens, ...
* Remove mandatory scope: The mandatory scope (`fosite`) has been removed as it has proven impractical.
* Allowed OAuth2 Client scopes are now being set with `scope` instead of `granted_scopes` when using the DefaultClient.
* There is now a scope matching strategy that can be replaced.
* OAuth2 Client scopes are now checked on every grant type.
* Handler subpackages such as `core/client` or `oidc/explicit` have been merged and moved one level up
* `handler/oidc` is now `handler/openid`
* `handler/core` is now `handler/oauth2`

## 0.1.0

Initial release
297 changes: 207 additions & 90 deletions README.md

Large diffs are not rendered by default.

8 changes: 1 addition & 7 deletions access_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (f *Fosite) NewAccessRequest(ctx context.Context, r *http.Request, session
return accessRequest, errors.New("Session must not be nil")
}

accessRequest.Scopes = removeEmpty(strings.Split(r.PostForm.Get("scope"), " "))
accessRequest.SetRequestedScopes(removeEmpty(strings.Split(r.PostForm.Get("scope"), " ")))
accessRequest.GrantTypes = removeEmpty(strings.Split(r.PostForm.Get("grant_type"), " "))
if len(accessRequest.GrantTypes) < 1 {
return accessRequest, errors.Wrap(ErrInvalidRequest, "No grant type given")
Expand Down Expand Up @@ -83,11 +83,5 @@ func (f *Fosite) NewAccessRequest(ctx context.Context, r *http.Request, session
if !found {
return nil, errors.Wrap(ErrInvalidRequest, "")
}

if !accessRequest.GetScopes().Has(f.GetMandatoryScope()) {
return accessRequest, errors.Wrap(ErrInvalidScope, "")
}

accessRequest.GrantScope(f.GetMandatoryScope())
return accessRequest, nil
}
24 changes: 1 addition & 23 deletions access_request_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/ory-am/fosite/internal"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)

func TestNewAccessRequest(t *testing.T) {
Expand Down Expand Up @@ -140,28 +139,7 @@ func TestNewAccessRequest(t *testing.T) {
store.EXPECT().GetClient(gomock.Eq("foo")).Return(client, nil)
client.EXPECT().GetHashedSecret().Return([]byte("foo"))
hasher.EXPECT().Compare(gomock.Eq([]byte("foo")), gomock.Eq([]byte("bar"))).Return(nil)
handler.EXPECT().HandleTokenEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(_ context.Context, _ *http.Request, a AccessRequester) {
a.SetScopes([]string{"asdfasdf"})
}).Return(nil)
},
handlers: TokenEndpointHandlers{handler},
expectErr: ErrInvalidScope,
},
{
header: http.Header{
"Authorization": {basicAuth("foo", "bar")},
},
method: "POST",
form: url.Values{
"grant_type": {"foo"},
},
mock: func() {
store.EXPECT().GetClient(gomock.Eq("foo")).Return(client, nil)
client.EXPECT().GetHashedSecret().Return([]byte("foo"))
hasher.EXPECT().Compare(gomock.Eq([]byte("foo")), gomock.Eq([]byte("bar"))).Return(nil)
handler.EXPECT().HandleTokenEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(_ context.Context, _ *http.Request, a AccessRequester) {
a.SetScopes([]string{DefaultMandatoryScope})
}).Return(nil)
handler.EXPECT().HandleTokenEndpointRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
},
handlers: TokenEndpointHandlers{handler},
expect: &AccessRequest{
Expand Down
8 changes: 1 addition & 7 deletions authorize_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,6 @@ func (c *Fosite) NewAuthorizeRequest(ctx context.Context, r *http.Request) (Auth
request.State = state

// Remove empty items from arrays
request.Scopes = removeEmpty(strings.Split(r.Form.Get("scope"), " "))

if !request.Scopes.Has(c.GetMandatoryScope()) {
return request, errors.Wrap(ErrInvalidScope, "mandatory scope is missing")
}
request.GrantScope(c.GetMandatoryScope())

request.SetRequestedScopes(removeEmpty(strings.Split(r.Form.Get("scope"), " ")))
return request, nil
}
19 changes: 2 additions & 17 deletions authorize_request_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,21 +133,6 @@ func TestNewAuthorizeRequest(t *testing.T) {
},
},
/* success case */
{
desc: "should pass",
conf: &Fosite{Store: store},
query: url.Values{
"redirect_uri": {"https://foo.bar/cb"},
"client_id": {"1234"},
"response_type": {"code"},
"state": {"strong-state"},
"scope": {"foo bar"},
},
mock: func() {
store.EXPECT().GetClient("1234").Return(&DefaultClient{RedirectURIs: []string{"https://foo.bar/cb"}}, nil)
},
expectedError: ErrInvalidScope,
},
{
desc: "should pass",
conf: &Fosite{Store: store},
Expand All @@ -156,7 +141,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
"client_id": {"1234"},
"response_type": {"code token"},
"state": {"strong-state"},
"scope": {DefaultMandatoryScope + " foo bar"},
"scope": {"foo bar"},
},
mock: func() {
store.EXPECT().GetClient("1234").Return(&DefaultClient{RedirectURIs: []string{"https://foo.bar/cb"}}, nil)
Expand All @@ -167,7 +152,7 @@ func TestNewAuthorizeRequest(t *testing.T) {
State: "strong-state",
Request: Request{
Client: &DefaultClient{RedirectURIs: []string{"https://foo.bar/cb"}},
Scopes: []string{DefaultMandatoryScope, "foo", "bar"},
Scopes: []string{"foo", "bar"},
},
},
},
Expand Down
6 changes: 3 additions & 3 deletions authorize_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,15 @@ func TestAuthorizeRequest(t *testing.T) {
assert.Equal(t, c.ar.RedirectURI, c.ar.GetRedirectURI(), "%d", k)
assert.Equal(t, c.ar.RequestedAt, c.ar.GetRequestedAt(), "%d", k)
assert.Equal(t, c.ar.ResponseTypes, c.ar.GetResponseTypes(), "%d", k)
assert.Equal(t, c.ar.Scopes, c.ar.GetScopes(), "%d", k)
assert.Equal(t, c.ar.Scopes, c.ar.GetRequestedScopes(), "%d", k)
assert.Equal(t, c.ar.State, c.ar.GetState(), "%d", k)
assert.Equal(t, c.isRedirValid, c.ar.IsRedirectURIValid(), "%d", k)

c.ar.GrantScope("foo")
c.ar.SetSession(&struct{}{})
c.ar.SetScopes([]string{"foo"})
c.ar.SetRequestedScopes([]string{"foo"})
assert.True(t, c.ar.GetGrantedScopes().Has("foo"))
assert.True(t, c.ar.GetScopes().Has("foo"))
assert.True(t, c.ar.GetRequestedScopes().Has("foo"))
assert.Equal(t, &struct{}{}, c.ar.GetSession())
}
}
54 changes: 5 additions & 49 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
package fosite

import "strings"

// Scopes is a list of scopes.
type Scopes interface {
// Fulfill returns true if requestScope is fulfilled by the scope list.
Grant(requestScope string) bool
}

// Client represents a client or an app.
type Client interface {
// GetID returns the client ID.
Expand All @@ -28,8 +20,8 @@ type Client interface {
// Returns the client's owner.
GetOwner() string

// Returns the scopes this client was granted.
GetGrantedScopes() Scopes
// Returns the scopes this client is allowed to request.
GetScopes() Arguments
}

// DefaultClient is a simple default implementation of the Client interface.
Expand All @@ -40,7 +32,7 @@ type DefaultClient struct {
RedirectURIs []string `json:"redirect_uris" gorethink:"redirect_uris"`
GrantTypes []string `json:"grant_types" gorethink:"grant_types"`
ResponseTypes []string `json:"response_types" gorethink:"response_types"`
GrantedScopes []string `json:"granted_scopes" gorethink:"granted_scopes"`
Scopes []string `json:"scopes" gorethink:"scopes"`
Owner string `json:"owner" gorethink:"owner"`
PolicyURI string `json:"policy_uri" gorethink:"policy_uri"`
TermsOfServiceURI string `json:"tos_uri" gorethink:"tos_uri"`
Expand All @@ -49,40 +41,6 @@ type DefaultClient struct {
Contacts []string `json:"contacts" gorethink:"contacts"`
}

type DefaultScopes struct {
Scopes []string
}

func (s *DefaultScopes) Grant(requestScope string) bool {
for _, scope := range s.Scopes {
// foo == foo -> true
if scope == requestScope {
return true
}

// picture.read > picture -> false (scope picture includes read, write, ...)
if len(scope) > len(requestScope) {
continue
}

needles := strings.Split(requestScope, ".")
haystack := strings.Split(scope, ".")
haystackLen := len(haystack) - 1
for k, needle := range needles {
if haystackLen < k {
return true
}

current := haystack[k]
if current != needle {
continue
}
}
}

return false
}

func (c *DefaultClient) GetID() string {
return c.ID
}
Expand All @@ -95,10 +53,8 @@ func (c *DefaultClient) GetHashedSecret() []byte {
return c.Secret
}

func (c *DefaultClient) GetGrantedScopes() Scopes {
return &DefaultScopes{
Scopes: c.GrantedScopes,
}
func (c *DefaultClient) GetScopes() Arguments {
return c.Scopes
}

func (c *DefaultClient) GetGrantTypes() Arguments {
Expand Down
29 changes: 3 additions & 26 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,18 @@ func TestDefaultClient(t *testing.T) {
RedirectURIs: []string{"foo", "bar"},
ResponseTypes: []string{"foo", "bar"},
GrantTypes: []string{"foo", "bar"},
Scopes: []string{"fooscope"},
}

assert.Equal(t, sc.ID, sc.GetID())
assert.Equal(t, sc.RedirectURIs, sc.GetRedirectURIs())
assert.Equal(t, sc.Secret, sc.GetHashedSecret())
assert.EqualValues(t, sc.ResponseTypes, sc.GetResponseTypes())
assert.EqualValues(t, sc.GrantTypes, sc.GetGrantTypes())

assert.False(t, sc.GetGrantedScopes().Grant("foo.bar.baz"))
assert.False(t, sc.GetGrantedScopes().Grant("foo.bar"))
assert.False(t, sc.GetGrantedScopes().Grant("foo"))

sc.GrantedScopes = []string{"foo.bar", "bar.baz", "baz.baz.1", "baz.baz.2", "baz.baz.3", "baz.baz.baz"}
assert.True(t, sc.GetGrantedScopes().Grant("foo.bar.baz"))
assert.True(t, sc.GetGrantedScopes().Grant("baz.baz.baz"))
assert.True(t, sc.GetGrantedScopes().Grant("foo.bar"))
assert.False(t, sc.GetGrantedScopes().Grant("foo"))

assert.True(t, sc.GetGrantedScopes().Grant("bar.baz"))
assert.True(t, sc.GetGrantedScopes().Grant("bar.baz.zad"))
assert.False(t, sc.GetGrantedScopes().Grant("bar"))

assert.False(t, sc.GetGrantedScopes().Grant("baz"))

sc.GrantedScopes = []string{"fosite.keys.create", "fosite.keys.get", "fosite.keys.delete", "fosite.keys.update"}
assert.True(t, sc.GetGrantedScopes().Grant("fosite.keys.delete"))
assert.True(t, sc.GetGrantedScopes().Grant("fosite.keys.get"))
assert.True(t, sc.GetGrantedScopes().Grant("fosite.keys.get"))
assert.True(t, sc.GetGrantedScopes().Grant("fosite.keys.update"))
assert.EqualValues(t, sc.Scopes, sc.GetScopes())

sc.GrantTypes = []string{}
sc.ResponseTypes = []string{}
assert.Equal(t, "code", sc.GetResponseTypes()[0])
assert.Equal(t, "authorization_code", sc.GetGrantTypes()[0])
}

func TestDefaultScope(t *testing.T) {

}
79 changes: 79 additions & 0 deletions compose/compose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package compose

import (
"crypto/rsa"

"github.com/Sirupsen/logrus"
"github.com/ory-am/fosite"
"github.com/ory-am/fosite/hash"
)

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

// Compose takes a config, a storage, a strategy and handlers to instantiate an OAuth2Provider:
//
// import "github.com/ory-am/fosite/compose"
//
// // var storage = new(MyFositeStorage)
// var config = Config {
// AccessTokenLifespan: time.Minute * 30,
// // check Config for further configuration options
// }
//
// var strategy = NewOAuth2HMACStrategy(config)
//
// var oauth2Provider = Compose(
// config,
// storage,
// strategy,
// NewOAuth2AuthorizeExplicitHandler,
// OAuth2ClientCredentialsGrantFactory,
// // for a complete list refer to the docs of this package
// )
func Compose(config *Config, storage interface{}, strategy interface{}, handlers ...handler) fosite.OAuth2Provider {
f := &fosite.Fosite{
Store: storage.(fosite.Storage),
AuthorizeEndpointHandlers: fosite.AuthorizeEndpointHandlers{},
TokenEndpointHandlers: fosite.TokenEndpointHandlers{},
TokenValidators: fosite.TokenValidators{},
Hasher: &hash.BCrypt{WorkFactor: config.GetHashCost()},
Logger: &logrus.Logger{},
ScopeStrategy: fosite.HierarchicScopeStrategy,
}

for _, h := range handlers {
res := h(config, storage, strategy)
if ah, ok := res.(fosite.AuthorizeEndpointHandler); ok {
f.AuthorizeEndpointHandlers.Append(ah)
}
if th, ok := res.(fosite.TokenEndpointHandler); ok {
f.TokenEndpointHandlers.Append(th)
}
if tv, ok := res.(fosite.TokenValidator); ok {
f.TokenValidators.Append(tv)
}
}

return f
}

// ComposeAllEnabled returns a fosite instance with all OAuth2 and OpenID Connect handlers enabled.
func ComposeAllEnabled(config *Config, storage interface{}, secret []byte, key *rsa.PrivateKey) fosite.OAuth2Provider {
return Compose(
config,
storage,
&CommonStrategy{
CoreStrategy: NewOAuth2HMACStrategy(config, secret),
OpenIDConnectTokenStrategy: NewOpenIDConnectStrategy(key),
},
OAuth2AuthorizeExplicitFactory,
OAuth2AuthorizeImplicitFactory,
OAuth2ClientCredentialsGrantFactory,
OAuth2RefreshTokenGrantFactory,
OAuth2ResourceOwnerPasswordCredentialsFactory,

OpenIDConnectExplicit,
OpenIDConnectImplicit,
OpenIDConnectHybrid,
)
}
Loading

0 comments on commit a92c755

Please sign in to comment.